Reduce world overlay render overhead

This commit is contained in:
MoePus committed 2026-09-02 15:12:08 +09:00
1 parent 598bb95ff9
commit 5a4c4d914b
10 files changed
+691 -241

No files matched your search

@@ -44,10 +44,14 @@ final class WorldEntityFlameClusters {
mspt, Math.max(mspt, maximum), samples);
var anchor = new Anchor(centerX(input.bounds()), input.bounds().maxY,
centerZ(input.bounds()));
var previousAnchor = new Anchor(
anchor.x() + input.previousOffsetX(),
anchor.y() + input.previousOffsetY(),
anchor.z() + input.previousOffsetZ());
var visual = new Visual(ObjectPerformanceScale.averageSamples(
metric, measurementTicks(input, tickCount)),
updatePhase(0x811C9DC5, input.stableId()) & 0x7FFF, 1);
return new Marker(anchor, metric, visual);
return new Marker(previousAnchor, anchor, metric, visual);
}
private static Marker marker(List<Input> group, long tickCount) {
@@ -60,6 +64,9 @@ final class WorldEntityFlameClusters {
double x = 0.0D;
double z = 0.0D;
double y = -Double.MAX_VALUE;
double previousX = 0.0D;
double previousZ = 0.0D;
double previousY = -Double.MAX_VALUE;
int phase = 0x811C9DC5;
for (Input input : group) {
double mspt = positiveFinite(input.metric().mspt());
@@ -71,14 +78,23 @@ final class WorldEntityFlameClusters {
long inputTicks = measurementTicks(input, tickCount);
averageSamples += ObjectPerformanceScale.averageSamples(input.metric(), inputTicks);
totalWeight += weight;
x += centerX(input.bounds()) * weight;
z += centerZ(input.bounds()) * weight;
double inputX = centerX(input.bounds());
double inputZ = centerZ(input.bounds());
x += inputX * weight;
z += inputZ * weight;
y = Math.max(y, input.bounds().maxY);
previousX += (inputX + input.previousOffsetX()) * weight;
previousZ += (inputZ + input.previousOffsetZ()) * weight;
previousY = Math.max(previousY,
input.bounds().maxY + input.previousOffsetY());
phase = updatePhase(phase, input.stableId());
}
PerfReportData.Metric metric = new PerfReportData.Metric(
totalMspt, Math.max(totalMspt, totalMax), totalSamples);
return new Marker(new Anchor(x / totalWeight, y, z / totalWeight), metric,
var previousAnchor = new Anchor(
previousX / totalWeight, previousY, previousZ / totalWeight);
var anchor = new Anchor(x / totalWeight, y, z / totalWeight);
return new Marker(previousAnchor, anchor, metric,
new Visual(averageSamples, phase & 0x7FFF, group.size()));
}
@@ -111,9 +127,15 @@ final class WorldEntityFlameClusters {
}
record Input(AABB bounds, PerfReportData.Metric metric,
long measurementTicks, int stableId) {
long measurementTicks, int stableId,
double previousOffsetX, double previousOffsetY, double previousOffsetZ) {
Input(AABB bounds, PerfReportData.Metric metric, int stableId) {
this(bounds, metric, 0L, stableId);
this(bounds, metric, 0L, stableId, 0.0D, 0.0D, 0.0D);
}
Input(AABB bounds, PerfReportData.Metric metric,
long measurementTicks, int stableId) {
this(bounds, metric, measurementTicks, stableId, 0.0D, 0.0D, 0.0D);
}
}
@@ -123,7 +145,12 @@ final class WorldEntityFlameClusters {
record Visual(double flameStrength, int phase, int memberCount) {
}
record Marker(Anchor anchor, PerfReportData.Metric metric, Visual visual) {
record Marker(Anchor previousAnchor, Anchor anchor,
PerfReportData.Metric metric, Visual visual) {
Marker(Anchor anchor, PerfReportData.Metric metric, Visual visual) {
this(anchor, anchor, metric, visual);
}
double x() {
return anchor.x();
}
@@ -147,6 +174,18 @@ final class WorldEntityFlameClusters {
int memberCount() {
return visual.memberCount();
}
double previousX() {
return previousAnchor.x();
}
double previousY() {
return previousAnchor.y();
}
double previousZ() {
return previousAnchor.z();
}
}
private static final class ClusterBuilder {
@@ -12,6 +12,8 @@ public final class WorldOverlayMath {
public static final double LABEL_TRANSITION_END = 12.0D;
public static final double LABEL_FADE_START = 20.0D;
public static final double LABEL_FADE_END = 40.0D;
static final double SPARK_FADE_START = 20.0D;
static final double SPARK_FADE_END = 24.0D;
public static final double MAX_FLAME_VIEWPORT_FRACTION = 0.18D;
public static final int MAX_DISPLAY_MICROS = 999_999_999;
public static final int OVERFLOW_MICROS = MAX_DISPLAY_MICROS + 1;
@@ -0,0 +1,109 @@
package com.moepus.serverwarashi.client.world;
import net.minecraft.world.phys.Vec3;
import java.util.List;
final class WorldPerfMarkerBuckets {
static final int BUCKET_WIDTH = 8;
static final int BUCKET_COUNT = (int) (WorldOverlayMath.OBJECT_FADE_END / BUCKET_WIDTH);
private static final int SPARK_BUCKET_COUNT =
(int) (WorldOverlayMath.SPARK_FADE_END / BUCKET_WIDTH);
private static final int LABEL_BUCKET_COUNT =
(int) (WorldOverlayMath.LABEL_FADE_END / BUCKET_WIDTH);
private static final double MAX_DISTANCE_SQUARED =
WorldOverlayMath.OBJECT_FADE_END * WorldOverlayMath.OBJECT_FADE_END;
private WorldPerfMarkerBuckets() {
}
static Batch order(List<WorldEntityFlameClusters.Marker> markers, Vec3 camera) {
byte[] markerBuckets = new byte[markers.size()];
int[] counts = new int[BUCKET_COUNT];
int visibleCount = classify(markers, camera, markerBuckets, counts);
WorldEntityFlameClusters.Marker[] ordered =
new WorldEntityFlameClusters.Marker[visibleCount];
int[] writeOffsets = writeOffsets(counts);
for (int index = 0; index < markers.size(); index++) {
int bucket = markerBuckets[index];
if (bucket >= 0) {
ordered[writeOffsets[bucket]++] = markers.get(index);
}
}
int sparkBase = visibleCount - nearCount(counts, SPARK_BUCKET_COUNT);
int labelBase = visibleCount - nearCount(counts, LABEL_BUCKET_COUNT);
return new Batch(ordered, sparkBase, labelBase);
}
static int bucketForDistanceSquared(double distanceSquared) {
if (!Double.isFinite(distanceSquared) || distanceSquared < 0.0D
|| distanceSquared > MAX_DISTANCE_SQUARED) {
return -1;
}
if (distanceSquared == MAX_DISTANCE_SQUARED) {
return BUCKET_COUNT - 1;
}
int low = 0;
int high = BUCKET_COUNT;
while (low < high) {
int middle = (low + high) >>> 1;
double boundary = (middle + 1) * BUCKET_WIDTH;
if (distanceSquared < boundary * boundary) {
high = middle;
} else {
low = middle + 1;
}
}
return low;
}
private static int classify(List<WorldEntityFlameClusters.Marker> markers,
Vec3 camera,
byte[] markerBuckets,
int[] counts) {
int visibleCount = 0;
for (int index = 0; index < markers.size(); index++) {
WorldEntityFlameClusters.Marker marker = markers.get(index);
int bucket = bucketForDistanceSquared(distanceSquared(marker, camera));
markerBuckets[index] = (byte) bucket;
if (bucket >= 0) {
counts[bucket]++;
visibleCount++;
}
}
return visibleCount;
}
private static int[] writeOffsets(int[] counts) {
int[] offsets = new int[BUCKET_COUNT];
int offset = 0;
for (int bucket = BUCKET_COUNT - 1; bucket >= 0; bucket--) {
offsets[bucket] = offset;
offset += counts[bucket];
}
return offsets;
}
private static int nearCount(int[] counts, int bucketCount) {
int count = 0;
for (int bucket = 0; bucket < bucketCount; bucket++) {
count += counts[bucket];
}
return count;
}
private static double distanceSquared(WorldEntityFlameClusters.Marker marker, Vec3 camera) {
double dx = marker.x() - camera.x;
double dy = marker.y() - camera.y;
double dz = marker.z() - camera.z;
return dx * dx + dy * dy + dz * dz;
}
record Batch(WorldEntityFlameClusters.Marker[] markers,
int sparkBase,
int labelBase) {
int size() {
return markers.length;
}
}
}
@@ -3,6 +3,7 @@ package com.moepus.serverwarashi.client.world;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.BufferUploader;
import com.moepus.serverwarashi.common.modules.performance.report.PerfReportLimits;
import net.minecraft.client.renderer.ShaderInstance;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix4f;
@@ -20,71 +21,113 @@ import java.nio.FloatBuffer;
import java.util.List;
final class WorldPerfObjectBuffers implements AutoCloseable {
static final int MAX_MARKER_COUNT = 4_096;
static final int TEXELS_PER_MARKER = 3;
static final int MAX_MARKER_COUNT = PerfReportLimits.MAX_WORLD_HOTSPOT_ENTRIES;
static final int TEXELS_PER_MARKER = 4;
static final int FLOATS_PER_MARKER = TEXELS_PER_MARKER * 4;
private static final int QUAD_VERTEX_COUNT = 4;
private static final int FLOAT_BYTES = Float.BYTES;
private static final int MARKER_TEXTURE_UNIT = 2;
private static final String MARKER_SAMPLER = "MarkerData";
private final MarkerBuffer staticMarkers = new MarkerBuffer();
private final MarkerBuffer dynamicMarkers = new MarkerBuffer();
private int quadVao = -1;
private int quadVertexBuffer = -1;
private int markerBuffer = -1;
private int markerTexture = -1;
private FloatBuffer markerStaging;
private int markerCapacity;
private int markerCount;
private int stagingCapacity;
private ShaderInstance samplerShader;
private int markerSamplerLocation = -1;
void replace(List<Entry> entries) {
void replace(Source source,
List<WorldEntityFlameClusters.Marker> markers,
Vec3 camera) {
RenderSystem.assertOnRenderThread();
int entryCount = Math.min(entries.size(), MAX_MARKER_COUNT);
if (entryCount == 0) {
clear();
if (markers.size() > MAX_MARKER_COUNT) {
throw new IllegalArgumentException("Too many world overlay markers: " + markers.size());
}
WorldPerfMarkerBuckets.Batch ordered = WorldPerfMarkerBuckets.order(markers, camera);
MarkerBuffer target = buffer(source);
if (ordered.size() == 0) {
target.clear();
return;
}
ensureMarkerCapacity(entryCount);
ensureGpuObjects();
ensureStagingCapacity(ordered.size());
ensureGpuObjects(target);
target.originX = camera.x;
target.originZ = camera.z;
UploadOrigin origin = new UploadOrigin(target.originX, target.originZ);
markerStaging.clear();
for (int index = 0; index < entryCount; index++) {
encodeMarker(entries.get(index), markerStaging);
for (WorldEntityFlameClusters.Marker marker : ordered.markers()) {
encodeMarker(marker, origin, markerStaging);
}
markerStaging.flip();
upload(target);
target.markerCount = ordered.size();
target.sparkMarkerBase = ordered.sparkBase();
target.labelMarkerBase = ordered.labelBase();
}
private void upload(MarkerBuffer target) {
int previousBuffer = GL11.glGetInteger(GL15.GL_ARRAY_BUFFER_BINDING);
try {
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, markerBuffer);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, target.markerBuffer);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, markerStaging, GL15.GL_STREAM_DRAW);
} finally {
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, previousBuffer);
}
markerCount = entryCount;
}
void clear() {
markerCount = 0;
staticMarkers.clear();
dynamicMarkers.clear();
}
void draw(Batch batch, DrawContext context) {
if (markerCount == 0) {
void clear(Source source) {
buffer(source).clear();
}
boolean isEmpty() {
return staticMarkers.markerCount == 0 && dynamicMarkers.markerCount == 0;
}
boolean isEmpty(Source source) {
return buffer(source).markerCount == 0;
}
double originX(Source source) {
return buffer(source).originX;
}
double originZ(Source source) {
return buffer(source).originZ;
}
void draw(Source source, Batch batch, DrawContext context) {
MarkerBuffer target = buffer(source);
int markerBase = markerBase(target, batch);
int drawMarkerCount = target.markerCount - markerBase;
if (drawMarkerCount == 0) {
return;
}
ShaderInstance shader = context.shader();
setDrawUniforms(batch, context);
setDrawUniforms(batch, context, markerBase);
shader.setSampler("Sampler0", RenderSystem.getShaderTexture(0));
shader.setSampler("Sampler1", RenderSystem.getShaderTexture(1));
shader.apply();
batch.applyBlend();
drawInstances(target, shader, batch.instancesFor(drawMarkerCount));
}
private void drawInstances(MarkerBuffer target, ShaderInstance shader, int instanceCount) {
int previousVao = GL11.glGetInteger(GL30.GL_VERTEX_ARRAY_BINDING);
int previousActiveTexture = GlStateManager._getActiveTexture();
GlStateManager._activeTexture(GL13.GL_TEXTURE0 + MARKER_TEXTURE_UNIT);
int previousMarkerTexture = GL11.glGetInteger(GL31.GL_TEXTURE_BINDING_BUFFER);
try {
bindMarkerData(shader);
bindMarkerData(target, shader);
GL30.glBindVertexArray(quadVao);
GL31.glDrawArraysInstanced(GL11.GL_TRIANGLE_STRIP, 0, QUAD_VERTEX_COUNT,
batch.instancesFor(markerCount));
GL31.glDrawArraysInstanced(
GL11.GL_TRIANGLE_STRIP, 0, QUAD_VERTEX_COUNT, instanceCount);
} finally {
GL30.glBindVertexArray(previousVao);
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, previousMarkerTexture);
@@ -94,11 +137,20 @@ final class WorldPerfObjectBuffers implements AutoCloseable {
}
}
private void setDrawUniforms(Batch batch, DrawContext context) {
private static int markerBase(MarkerBuffer target, Batch batch) {
return switch (batch) {
case SPARK -> target.sparkMarkerBase;
case LABEL -> target.labelMarkerBase;
default -> 0;
};
}
private static void setDrawUniforms(Batch batch, DrawContext context, int markerBase) {
ShaderInstance shader = context.shader();
shader.safeGetUniform("RenderPass").set(batch.renderPass());
shader.safeGetUniform("ParticleStart").set(batch.particleStart());
shader.safeGetUniform("SlotsPerMarker").set(batch.slotsPerMarker());
shader.safeGetUniform("MarkerBase").set(markerBase);
if (shader.MODEL_VIEW_MATRIX != null) {
shader.MODEL_VIEW_MATRIX.set(context.modelView());
}
@@ -107,7 +159,7 @@ final class WorldPerfObjectBuffers implements AutoCloseable {
}
}
private void bindMarkerData(ShaderInstance shader) {
private void bindMarkerData(MarkerBuffer target, ShaderInstance shader) {
if (samplerShader != shader) {
int program = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM);
markerSamplerLocation = GL20.glGetUniformLocation(program, MARKER_SAMPLER);
@@ -116,12 +168,12 @@ final class WorldPerfObjectBuffers implements AutoCloseable {
}
samplerShader = shader;
}
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, markerTexture);
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, target.markerTexture);
GL20.glUniform1i(markerSamplerLocation, MARKER_TEXTURE_UNIT);
}
private void ensureMarkerCapacity(int required) {
if (required <= markerCapacity) {
private void ensureStagingCapacity(int required) {
if (required <= stagingCapacity) {
return;
}
int nextCapacity = Math.max(16, Integer.highestOneBit(required - 1) << 1);
@@ -134,25 +186,20 @@ final class WorldPerfObjectBuffers implements AutoCloseable {
FloatBuffer nextStaging = MemoryUtil.memAllocFloat(nextCapacity * FLOATS_PER_MARKER);
MemoryUtil.memFree(markerStaging);
markerStaging = nextStaging;
markerCapacity = nextCapacity;
stagingCapacity = nextCapacity;
}
private void ensureGpuObjects() {
if (quadVao >= 0) {
return;
}
private void ensureGpuObjects(MarkerBuffer target) {
int previousVao = GL11.glGetInteger(GL30.GL_VERTEX_ARRAY_BINDING);
int previousArrayBuffer = GL11.glGetInteger(GL15.GL_ARRAY_BUFFER_BINDING);
int previousMarkerTexture = GL11.glGetInteger(GL31.GL_TEXTURE_BINDING_BUFFER);
try {
createQuad();
markerBuffer = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, markerBuffer);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER,
(long) FLOATS_PER_MARKER * FLOAT_BYTES, GL15.GL_STREAM_DRAW);
markerTexture = GL11.glGenTextures();
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, markerTexture);
GL31.glTexBuffer(GL31.GL_TEXTURE_BUFFER, GL30.GL_RGBA32F, markerBuffer);
if (quadVao < 0) {
createQuad();
}
if (target.markerBuffer < 0) {
createMarkerBuffer(target);
}
} finally {
GL30.glBindVertexArray(previousVao);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, previousArrayBuffer);
@@ -161,6 +208,16 @@ final class WorldPerfObjectBuffers implements AutoCloseable {
}
}
private void createMarkerBuffer(MarkerBuffer target) {
target.markerBuffer = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, target.markerBuffer);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER,
(long) FLOATS_PER_MARKER * FLOAT_BYTES, GL15.GL_STREAM_DRAW);
target.markerTexture = GL11.glGenTextures();
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, target.markerTexture);
GL31.glTexBuffer(GL31.GL_TEXTURE_BUFFER, GL30.GL_RGBA32F, target.markerBuffer);
}
private void createQuad() {
quadVao = GL30.glGenVertexArrays();
quadVertexBuffer = GL15.glGenBuffers();
@@ -179,31 +236,35 @@ final class WorldPerfObjectBuffers implements AutoCloseable {
3 * FLOAT_BYTES, 0L);
}
static void encodeMarker(Entry entry, FloatBuffer target) {
WorldEntityFlameClusters.Marker marker = entry.marker();
static void encodeMarker(WorldEntityFlameClusters.Marker marker,
UploadOrigin origin,
FloatBuffer target) {
int rgb = WorldOverlayMath.colorRgb(marker.flameStrength());
int strength = WorldOverlayMath.packFlameStrength(marker.flameStrength());
WorldOverlayMath.PackedLabel label = WorldOverlayMath.packLabel(
WorldOverlayMath.roundedMicros(marker.metric()));
target.put((float) entry.x()).put((float) (entry.y() + 0.04D))
.put((float) entry.z()).put((float) strength);
target.put((float) (marker.x() - origin.x())).put((float) (marker.y() + 0.04D))
.put((float) (marker.z() - origin.z())).put((float) strength);
target.put((rgb >> 16 & 0xFF) / 255.0F).put((rgb >> 8 & 0xFF) / 255.0F)
.put((rgb & 0xFF) / 255.0F)
.put((float) WorldFlameParticleLayout.phaseWord(marker.phase()));
target.put((float) label.lowWord()).put((float) label.markedHighWord())
.put(WorldOverlayMath.packLabelStrength(marker.flameStrength()) / 255.0F)
.put(0.0F);
target.put((float) (marker.previousX() - origin.x()))
.put((float) (marker.previousY() + 0.04D))
.put((float) (marker.previousZ() - origin.z())).put(0.0F);
}
private MarkerBuffer buffer(Source source) {
return source == Source.STATIC ? staticMarkers : dynamicMarkers;
}
@Override
public void close() {
RenderSystem.assertOnRenderThread();
if (markerTexture >= 0) {
GL11.glDeleteTextures(markerTexture);
}
if (markerBuffer >= 0) {
GL15.glDeleteBuffers(markerBuffer);
}
deleteMarkerBuffer(staticMarkers);
deleteMarkerBuffer(dynamicMarkers);
if (quadVertexBuffer >= 0) {
GL15.glDeleteBuffers(quadVertexBuffer);
}
@@ -216,18 +277,30 @@ final class WorldPerfObjectBuffers implements AutoCloseable {
reset();
}
private static void deleteMarkerBuffer(MarkerBuffer target) {
if (target.markerTexture >= 0) {
GL11.glDeleteTextures(target.markerTexture);
}
if (target.markerBuffer >= 0) {
GL15.glDeleteBuffers(target.markerBuffer);
}
target.reset();
}
private void reset() {
quadVao = -1;
quadVertexBuffer = -1;
markerBuffer = -1;
markerTexture = -1;
markerStaging = null;
markerCapacity = 0;
markerCount = 0;
stagingCapacity = 0;
samplerShader = null;
markerSamplerLocation = -1;
}
enum Source {
STATIC,
DYNAMIC
}
enum Batch {
SMOKE(2, WorldFlameParticleLayout.ORANGE_PARTICLE_START,
WorldFlameParticleLayout.ORANGE_PARTICLE_COUNT, BlendMode.PREMULTIPLIED),
@@ -294,19 +367,33 @@ final class WorldPerfObjectBuffers implements AutoCloseable {
}
}
record Entry(WorldEntityFlameClusters.Marker marker,
double x,
double y,
double z,
double distanceSquared) {
static Entry from(WorldEntityFlameClusters.Marker marker,
Vec3 camera,
double distanceSquared) {
return new Entry(marker, marker.x() - camera.x, marker.y() - camera.y,
marker.z() - camera.z, distanceSquared);
}
record UploadOrigin(double x, double z) {
}
record DrawContext(ShaderInstance shader, Matrix4f modelView, Matrix4f projection) {
}
private static final class MarkerBuffer {
private int markerBuffer = -1;
private int markerTexture = -1;
private int markerCount;
private int sparkMarkerBase;
private int labelMarkerBase;
private double originX;
private double originZ;
private void clear() {
markerCount = 0;
sparkMarkerBase = 0;
labelMarkerBase = 0;
}
private void reset() {
markerBuffer = -1;
markerTexture = -1;
clear();
originX = 0.0D;
originZ = 0.0D;
}
}
}
@@ -13,7 +13,6 @@ import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.ShaderInstance;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.level.block.entity.BlockEntity;
@@ -23,20 +22,31 @@ import net.neoforged.neoforge.client.event.RenderLevelStageEvent;
import org.joml.Matrix4f;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
final class WorldPerfObjectRenderer {
private static final double MAX_DISTANCE_SQUARED =
WorldOverlayMath.OBJECT_FADE_END * WorldOverlayMath.OBJECT_FADE_END;
private static final double FLAME_FRUSTUM_MARGIN = 1.2D;
private static final double FLAME_FRUSTUM_HEIGHT = 4.0D;
private static final long STATIC_REFRESH_TICKS = 20L;
private static final WorldPerfObjectBuffers.Batch[] DRAW_ORDER = {
WorldPerfObjectBuffers.Batch.SMOKE,
WorldPerfObjectBuffers.Batch.NORMAL,
WorldPerfObjectBuffers.Batch.GLOW,
WorldPerfObjectBuffers.Batch.SPARK,
WorldPerfObjectBuffers.Batch.LABEL
};
static final float LABEL_PIXELS = 12.0F;
private final WorldPerfObjectBuffers buffers = new WorldPerfObjectBuffers();
private List<Candidate> candidates = List.of();
private final BlockPos.MutableBlockPos lookupPos = new BlockPos.MutableBlockPos();
private final Matrix4f staticModelView = new Matrix4f();
private final Matrix4f dynamicModelView = new Matrix4f();
private List<Candidate> staticCandidates = List.of();
private List<Candidate> dynamicCandidates = List.of();
private long reportRevision = Long.MIN_VALUE;
private long staticRefreshTick = Long.MIN_VALUE;
private long dynamicRefreshTick = Long.MIN_VALUE;
private String dimension;
private ClientConfig.WorldOverlayFilter filter;
private int markerLimit;
void render(RenderLevelStageEvent event,
ClientLevel level,
@@ -47,132 +57,166 @@ final class WorldPerfObjectRenderer {
return;
}
String currentDimension = level.dimension().location().toString();
refreshCandidates(snapshot, currentDimension);
List<WorldPerfObjectBuffers.Entry> flames = resolve(event, level, snapshot.tickCount());
if (flames.isEmpty()) {
buffers.clear();
boolean sourceChanged = refreshCandidates(snapshot, currentDimension);
long clientTick = level.getGameTime();
Vec3 camera = event.getCamera().getPosition();
refreshStaticIfNeeded(level, camera, clientTick, sourceChanged);
refreshDynamicIfNeeded(event, level, snapshot.tickCount(), camera,
clientTick, sourceChanged);
if (buffers.isEmpty()) {
return;
}
buffers.replace(flames);
draw(event, shader);
}
private void refreshCandidates(ClientPerfWorldState.Snapshot snapshot, String currentDimension) {
private boolean refreshCandidates(
ClientPerfWorldState.Snapshot snapshot, String currentDimension) {
ClientConfig.WorldOverlayFilter currentFilter = ClientConfig.worldOverlayFilter();
int currentMarkerLimit = ClientConfig.worldOverlayMarkerLimit();
if (snapshot.clientRevision() == reportRevision && currentDimension.equals(dimension)
&& currentFilter == filter) {
return;
&& currentFilter == filter && currentMarkerLimit == markerLimit) {
return false;
}
reportRevision = snapshot.clientRevision();
dimension = currentDimension;
filter = currentFilter;
List<Candidate> next = new ArrayList<>();
markerLimit = currentMarkerLimit;
splitCandidates(snapshot, currentDimension, currentFilter, currentMarkerLimit);
return true;
}
private void splitCandidates(ClientPerfWorldState.Snapshot snapshot,
String currentDimension,
ClientConfig.WorldOverlayFilter currentFilter,
int currentMarkerLimit) {
if (!currentDimension.equals(snapshot.dimension())) {
candidates = List.of();
staticCandidates = List.of();
dynamicCandidates = List.of();
return;
}
int initialCapacity = Math.min(snapshot.hotspots().size(), currentMarkerLimit);
List<Candidate> nextStatic = new ArrayList<>(initialCapacity);
List<Candidate> nextDynamic = new ArrayList<>();
int selected = 0;
for (ClientPerfWorldState.Hotspot hotspot : snapshot.hotspots()) {
double flameStrength = ObjectPerformanceScale.averageSamples(
hotspot.metric(), hotspot.measurementTicks());
if (WorldOverlayMath.shouldRender(flameStrength, currentFilter)) {
next.add(new Candidate(hotspot, hotspot.metric(), flameStrength));
if (next.size() == WorldPerfObjectBuffers.MAX_MARKER_COUNT) {
break;
}
if (!WorldOverlayMath.shouldRender(flameStrength, currentFilter)) {
continue;
}
Candidate candidate = new Candidate(hotspot, hotspot.metric(), flameStrength);
if (hotspot.key().kind() == PerfWorldHotspotIndex.Kind.ENTITY) {
nextDynamic.add(candidate);
} else {
nextStatic.add(candidate);
}
selected++;
if (selected == currentMarkerLimit) {
break;
}
}
candidates = List.copyOf(next);
staticCandidates = nextStatic;
dynamicCandidates = nextDynamic;
}
private List<WorldPerfObjectBuffers.Entry> resolve(RenderLevelStageEvent event,
ClientLevel level,
long tickCount) {
Vec3 camera = event.getCamera().getPosition();
private void refreshStaticIfNeeded(ClientLevel level,
Vec3 camera,
long clientTick,
boolean sourceChanged) {
if (!sourceChanged && (staticCandidates.isEmpty()
|| !staticRefreshDue(staticRefreshTick, clientTick))) {
return;
}
buffers.replace(WorldPerfObjectBuffers.Source.STATIC,
resolveBlockEntities(level), camera);
staticRefreshTick = clientTick;
}
private void refreshDynamicIfNeeded(RenderLevelStageEvent event,
ClientLevel level,
long reportTickCount,
Vec3 camera,
long clientTick,
boolean sourceChanged) {
if (!sourceChanged && (dynamicCandidates.isEmpty()
|| !dynamicRefreshDue(dynamicRefreshTick, clientTick))) {
return;
}
buffers.replace(WorldPerfObjectBuffers.Source.DYNAMIC,
resolveEntities(event, level, reportTickCount), camera);
dynamicRefreshTick = clientTick;
}
private List<WorldEntityFlameClusters.Marker> resolveBlockEntities(ClientLevel level) {
List<WorldEntityFlameClusters.Marker> markers =
new ArrayList<>(staticCandidates.size());
for (Candidate candidate : staticCandidates) {
long packedPosition = candidate.hotspot().key().blockPosition();
lookupPos.set(BlockPos.getX(packedPosition), BlockPos.getY(packedPosition),
BlockPos.getZ(packedPosition));
if (!level.hasChunkAt(lookupPos)) {
continue;
}
BlockEntity blockEntity = level.getBlockEntity(lookupPos);
if (blockEntity != null && !blockEntity.isRemoved()
&& matchesBlockEntity(candidate.hotspot(), blockEntity)) {
markers.add(blockEntityMarker(candidate, packedPosition));
}
}
return markers;
}
private List<WorldEntityFlameClusters.Marker> resolveEntities(
RenderLevelStageEvent event, ClientLevel level, long tickCount) {
List<WorldEntityFlameClusters.Input> itemEntities = new ArrayList<>();
List<WorldEntityFlameClusters.Marker> markers = new ArrayList<>();
for (Candidate candidate : candidates) {
AABB bounds = candidate.hotspot().key().kind() == PerfWorldHotspotIndex.Kind.ENTITY
? entityBounds(event, level, candidate.hotspot())
: blockEntityBounds(level, candidate.hotspot());
if (bounds == null) {
List<WorldEntityFlameClusters.Marker> markers =
new ArrayList<>(dynamicCandidates.size());
for (Candidate candidate : dynamicCandidates) {
Entity entity = level.getEntity(candidate.hotspot().key().entityId());
if (!isVisibleEntity(event, candidate.hotspot(), entity)) {
continue;
}
double distanceSquared = bounds.distanceToSqr(camera);
if (distanceSquared > MAX_DISTANCE_SQUARED
|| !event.getFrustum().isVisible(flameBounds(bounds))) {
continue;
}
if (candidate.hotspot().key().kind() == PerfWorldHotspotIndex.Kind.ENTITY) {
var input = new WorldEntityFlameClusters.Input(
bounds, candidate.metric(), candidate.hotspot().measurementTicks(),
candidate.hotspot().key().entityId());
if (shouldClusterEntity(candidate.hotspot().detailKind())) {
itemEntities.add(input);
} else {
markers.add(WorldEntityFlameClusters.single(input, tickCount));
}
AABB bounds = entity.getBoundingBox();
var input = new WorldEntityFlameClusters.Input(
bounds, candidate.metric(), candidate.hotspot().measurementTicks(),
candidate.hotspot().key().entityId(), entity.xo - entity.getX(),
entity.yo - entity.getY(), entity.zo - entity.getZ());
if (shouldClusterEntity(candidate.hotspot().detailKind())) {
itemEntities.add(input);
} else {
markers.add(blockEntityMarker(candidate, bounds));
markers.add(WorldEntityFlameClusters.single(input, tickCount));
}
}
markers.addAll(WorldEntityFlameClusters.cluster(itemEntities, tickCount));
List<WorldPerfObjectBuffers.Entry> result = markers.stream()
.map(marker -> WorldPerfObjectBuffers.Entry.from(
marker, camera, distanceSquared(marker, camera)))
.sorted(Comparator.comparingDouble(
WorldPerfObjectBuffers.Entry::distanceSquared).reversed())
.toList();
return result;
return markers;
}
private static AABB flameBounds(AABB bounds) {
return new AABB(bounds.minX - FLAME_FRUSTUM_MARGIN, bounds.minY,
bounds.minZ - FLAME_FRUSTUM_MARGIN, bounds.maxX + FLAME_FRUSTUM_MARGIN,
bounds.maxY + FLAME_FRUSTUM_HEIGHT, bounds.maxZ + FLAME_FRUSTUM_MARGIN);
private static boolean isVisibleEntity(RenderLevelStageEvent event,
ClientPerfWorldState.Hotspot hotspot,
Entity entity) {
return entity != null && !entity.isRemoved() && matchesEntity(hotspot, entity)
&& (event.getCamera().isDetached() || entity != event.getCamera().getEntity());
}
private static WorldEntityFlameClusters.Marker blockEntityMarker(
Candidate candidate, AABB bounds) {
int phase = PackedPositionHash.hash(
candidate.hotspot().key().blockPosition()) & 0x7FFF;
Candidate candidate, long packedPosition) {
int phase = PackedPositionHash.hash(packedPosition) & 0x7FFF;
var anchor = new WorldEntityFlameClusters.Anchor(
(bounds.minX + bounds.maxX) * 0.5D, bounds.maxY,
(bounds.minZ + bounds.maxZ) * 0.5D);
BlockPos.getX(packedPosition) + 0.5D,
BlockPos.getY(packedPosition) + 1.0D,
BlockPos.getZ(packedPosition) + 0.5D);
var visual = new WorldEntityFlameClusters.Visual(
candidate.flameStrength(), phase, 1);
return new WorldEntityFlameClusters.Marker(anchor, candidate.metric(), visual);
}
private static double distanceSquared(WorldEntityFlameClusters.Marker marker, Vec3 camera) {
double dx = marker.x() - camera.x;
double dy = marker.y() - camera.y;
double dz = marker.z() - camera.z;
return dx * dx + dy * dy + dz * dz;
static boolean staticRefreshDue(long previousTick, long currentTick) {
return previousTick == Long.MIN_VALUE || currentTick < previousTick
|| currentTick - previousTick >= STATIC_REFRESH_TICKS;
}
private static AABB entityBounds(RenderLevelStageEvent event,
ClientLevel level,
ClientPerfWorldState.Hotspot hotspot) {
Entity entity = level.getEntity(hotspot.key().entityId());
if (entity == null || entity.isRemoved() || !matchesEntity(hotspot, entity)
|| (!event.getCamera().isDetached() && entity == event.getCamera().getEntity())) {
return null;
}
float partialTick = event.getPartialTick().getGameTimeDeltaPartialTick(false);
double x = Mth.lerp(partialTick, entity.xo, entity.getX());
double y = Mth.lerp(partialTick, entity.yo, entity.getY());
double z = Mth.lerp(partialTick, entity.zo, entity.getZ());
return entity.getBoundingBox().move(x - entity.getX(), y - entity.getY(), z - entity.getZ());
}
private static AABB blockEntityBounds(ClientLevel level, ClientPerfWorldState.Hotspot hotspot) {
BlockPos pos = BlockPos.of(hotspot.key().blockPosition());
if (!level.hasChunkAt(pos)) {
return null;
}
BlockEntity blockEntity = level.getBlockEntity(pos);
return blockEntity != null && !blockEntity.isRemoved() && matchesBlockEntity(hotspot, blockEntity)
? new AABB(pos) : null;
static boolean dynamicRefreshDue(long previousTick, long currentTick) {
return previousTick != currentTick;
}
static boolean shouldClusterEntity(TickDetailKind detailKind) {
@@ -204,9 +248,29 @@ final class WorldPerfObjectRenderer {
int previousNoiseTexture = RenderSystem.getShaderTexture(1);
RenderSystem.setShaderTexture(0, WorldGlyphAtlas.textureId());
RenderSystem.setShaderTexture(1, WorldFlameNoiseTexture.textureId());
setFrameUniforms(event, shader, minecraft);
RenderSystem.enableBlend();
RenderSystem.disableDepthTest();
RenderSystem.depthMask(false);
RenderSystem.enableCull();
try {
drawBuffers(event, shader);
} finally {
RenderSystem.depthMask(true);
RenderSystem.enableDepthTest();
RenderSystem.defaultBlendFunc();
RenderSystem.disableBlend();
RenderSystem.setShaderTexture(0, previousGlyphTexture);
RenderSystem.setShaderTexture(1, previousNoiseTexture);
}
}
private static void setFrameUniforms(
RenderLevelStageEvent event, ShaderInstance shader, Minecraft minecraft) {
shader.safeGetUniform("ScreenSize").set(
(float) minecraft.getWindow().getWidth(), (float) minecraft.getWindow().getHeight());
shader.safeGetUniform("LabelPixels").set(LABEL_PIXELS * (float) minecraft.getWindow().getGuiScale());
shader.safeGetUniform("LabelPixels").set(
LABEL_PIXELS * (float) minecraft.getWindow().getGuiScale());
shader.safeGetUniform("FadeDistances").set(
(float) WorldOverlayMath.OBJECT_FADE_START,
(float) WorldOverlayMath.OBJECT_FADE_END);
@@ -216,6 +280,13 @@ final class WorldPerfObjectRenderer {
shader.safeGetUniform("LabelFadeDistances").set(
(float) WorldOverlayMath.LABEL_FADE_START,
(float) WorldOverlayMath.LABEL_FADE_END);
shader.safeGetUniform("PartialTick").set(
event.getPartialTick().getGameTimeDeltaPartialTick(false));
setPackedDataUniforms(shader);
setAnimationUniforms(event, shader, minecraft);
}
private static void setPackedDataUniforms(ShaderInstance shader) {
shader.safeGetUniform("OverflowValue").set(WorldOverlayMath.OVERFLOW_MICROS);
shader.safeGetUniform("LabelWordBase").set(WorldOverlayMath.LABEL_WORD_BASE);
shader.safeGetUniform("GlyphCount").set((float) WorldGlyphAtlas.GLYPH_COUNT);
@@ -225,35 +296,55 @@ final class WorldPerfObjectRenderer {
(float) WorldOverlayMath.MAX_PACKED_FLAME_STRENGTH);
shader.safeGetUniform("MaxFlameViewportFraction").set(
(float) WorldOverlayMath.MAX_FLAME_VIEWPORT_FRACTION);
}
private static void setAnimationUniforms(
RenderLevelStageEvent event, ShaderInstance shader, Minecraft minecraft) {
long gameTime = minecraft.level == null ? 0L : minecraft.level.getGameTime();
WorldFlameAnimationClock.Snapshot clock = WorldFlameAnimationClock.sample(
gameTime, event.getPartialTick().getGameTimeDeltaPartialTick(false));
shader.safeGetUniform("AnimationTime").set(clock.noiseSeconds());
shader.safeGetUniform("ParticleTime").set(clock.particleSeconds());
shader.safeGetUniform("ParticleEpoch").set(clock.particleEpoch());
Matrix4f modelView = viewRotationMatrix(event);
var drawContext = new WorldPerfObjectBuffers.DrawContext(
shader, modelView, event.getProjectionMatrix());
RenderSystem.enableBlend();
RenderSystem.disableDepthTest();
RenderSystem.depthMask(false);
RenderSystem.enableCull();
try {
buffers.draw(WorldPerfObjectBuffers.Batch.SMOKE, drawContext);
buffers.draw(WorldPerfObjectBuffers.Batch.NORMAL, drawContext);
buffers.draw(WorldPerfObjectBuffers.Batch.GLOW, drawContext);
buffers.draw(WorldPerfObjectBuffers.Batch.SPARK, drawContext);
buffers.draw(WorldPerfObjectBuffers.Batch.LABEL, drawContext);
} finally {
RenderSystem.depthMask(true);
RenderSystem.enableDepthTest();
RenderSystem.defaultBlendFunc();
RenderSystem.disableBlend();
RenderSystem.setShaderTexture(0, previousGlyphTexture);
RenderSystem.setShaderTexture(1, previousNoiseTexture);
}
private void drawBuffers(RenderLevelStageEvent event, ShaderInstance shader) {
WorldPerfObjectBuffers.DrawContext staticContext = drawContext(
event, shader, WorldPerfObjectBuffers.Source.STATIC);
WorldPerfObjectBuffers.DrawContext dynamicContext = drawContext(
event, shader, WorldPerfObjectBuffers.Source.DYNAMIC);
for (WorldPerfObjectBuffers.Batch batch : DRAW_ORDER) {
if (staticContext != null) {
buffers.draw(WorldPerfObjectBuffers.Source.STATIC, batch, staticContext);
}
if (dynamicContext != null) {
buffers.draw(WorldPerfObjectBuffers.Source.DYNAMIC, batch, dynamicContext);
}
}
}
private WorldPerfObjectBuffers.DrawContext drawContext(
RenderLevelStageEvent event,
ShaderInstance shader,
WorldPerfObjectBuffers.Source source) {
if (buffers.isEmpty(source)) {
return null;
}
return new WorldPerfObjectBuffers.DrawContext(
shader, sourceModelView(event, source), event.getProjectionMatrix());
}
private Matrix4f sourceModelView(
RenderLevelStageEvent event, WorldPerfObjectBuffers.Source source) {
Vec3 camera = event.getCamera().getPosition();
Matrix4f target = source == WorldPerfObjectBuffers.Source.STATIC
? staticModelView : dynamicModelView;
return target.set(event.getModelViewMatrix()).translate(
(float) (buffers.originX(source) - camera.x),
(float) -camera.y,
(float) (buffers.originZ(source) - camera.z));
}
static Matrix4f cameraRelativeMatrix(
RenderLevelStageEvent event, double originX, double originZ) {
Vec3 camera = event.getCamera().getPosition();
@@ -269,10 +360,14 @@ final class WorldPerfObjectRenderer {
void close() {
buffers.close();
candidates = List.of();
staticCandidates = List.of();
dynamicCandidates = List.of();
reportRevision = Long.MIN_VALUE;
staticRefreshTick = Long.MIN_VALUE;
dynamicRefreshTick = Long.MIN_VALUE;
dimension = null;
filter = null;
markerLimit = 0;
}
private record Candidate(ClientPerfWorldState.Hotspot hotspot,
@@ -22,11 +22,13 @@
{"name": "ParticlePhaseBase", "type": "int", "count": 1, "values": [512]},
{"name": "MaxFlameStrength", "type": "float", "count": 1, "values": [31.9990234375]},
{"name": "MaxFlameViewportFraction", "type": "float", "count": 1, "values": [0.18]},
{"name": "PartialTick", "type": "float", "count": 1, "values": [0]},
{"name": "AnimationTime", "type": "float", "count": 1, "values": [0]},
{"name": "ParticleTime", "type": "float", "count": 1, "values": [0]},
{"name": "ParticleEpoch", "type": "int", "count": 1, "values": [0]},
{"name": "RenderPass", "type": "int", "count": 1, "values": [0]},
{"name": "ParticleStart", "type": "int", "count": 1, "values": [0]},
{"name": "SlotsPerMarker", "type": "int", "count": 1, "values": [1]}
{"name": "SlotsPerMarker", "type": "int", "count": 1, "values": [1]},
{"name": "MarkerBase", "type": "int", "count": 1, "values": [0]}
]
}
@@ -14,11 +14,13 @@ uniform float StrengthWordScale;
uniform int ParticlePhaseBase;
uniform float MaxFlameStrength;
uniform float MaxFlameViewportFraction;
uniform float PartialTick;
uniform float ParticleTime;
uniform int ParticleEpoch;
uniform int RenderPass;
uniform int ParticleStart;
uniform int SlotsPerMarker;
uniform int MarkerBase;
const int RED_PARTICLE_COUNT = 24;
const int ORANGE_PARTICLE_COUNT = 20;
const int YELLOW_PARTICLE_COUNT = 20;
@@ -31,7 +33,7 @@ const int SPARK_PARTICLE_START = GLOW_PARTICLE_START + GLOW_PARTICLE_COUNT;
const int SPARK_LAYER = 4;
const int LABEL_PASS = 3;
const int SPARK_PASS = 4;
const int MARKER_TEXELS = 3;
const int MARKER_TEXELS = 4;
// Flame shape tuning. Higher severity widens the full tapered envelope.
const float GREEN_BASE_WIDTH = 1.5;
const float GREEN_TIP_WIDTH = 0.8;
@@ -39,18 +41,22 @@ const float PURPLE_BASE_WIDTH = 2.4;
const float PURPLE_TIP_WIDTH = 1.0;
const float WIDTH_TAPER_START = 0.0;
const float WIDTH_TAPER_END = 0.88;
// Distance LOD tuning. Sparks disappear before the core particle LOD starts.
const float PARTICLE_LOD_START = 24.0;
const float PARTICLE_LOD_END = 96.0;
const float FIRE_LOD_MIN_SCALE = 0.28;
const float GLOW_LOD_MIN_SCALE = 0.16;
// Distance LOD tuning. Particle density drops after 16 blocks and settles at 32.
const float PARTICLE_LOD_START = 16.0;
const float PARTICLE_LOD_END = 32.0;
const float SPARK_FADE_START = 20.0;
const float SPARK_FADE_END = 24.0;
const float SMOKE_FADE_START = 24.0;
const float SMOKE_FADE_END = 32.0;
const float MIN_RED_COUNT = 16.0;
const float MIN_ORANGE_COUNT = 14.0;
const float MIN_YELLOW_COUNT = 14.0;
const float MIN_GLOW_COUNT = 10.0;
const float MIN_SPARK_COUNT = 10.0;
const float FAR_RED_COUNT = 4.0;
const float FAR_ORANGE_COUNT = 3.0;
const float FAR_YELLOW_COUNT = 3.0;
const float FAR_GLOW_COUNT = 1.0;
const vec2 SPARK_COUNT_RAMP = vec2(3.50, 5.0);
// Spark motion tuning. Sparks deliberately do not use the flame width envelope.
const vec2 SPARK_ASPECT = vec2(1.0, 1.0);
@@ -95,6 +101,7 @@ flat out float particleOpacity;
flat out float particleLife;
struct FlameMarker {
vec3 position;
vec3 previousPosition;
float packedStrength;
vec3 color;
int phaseWord;
@@ -107,7 +114,8 @@ FlameMarker markerAt(int markerIndex) {
vec4 positionStrength = texelFetch(MarkerData, base);
vec4 colorPhase = texelFetch(MarkerData, base + 1);
vec4 label = texelFetch(MarkerData, base + 2);
return FlameMarker(positionStrength.xyz, positionStrength.w,
vec4 previousPosition = texelFetch(MarkerData, base + 3);
return FlameMarker(positionStrength.xyz, previousPosition.xyz, positionStrength.w,
colorPhase.rgb, int(colorPhase.w), int(label.x), int(label.y), label.z);
}
int decimalDigits(int value) {
@@ -185,19 +193,27 @@ float minimumParticles(int layer) {
if (layer == 3) return MIN_GLOW_COUNT;
return MIN_SPARK_COUNT;
}
float farParticles(int layer) {
if (layer == 0) return FAR_RED_COUNT;
if (layer == 1) return FAR_ORANGE_COUNT;
if (layer == 2) return FAR_YELLOW_COUNT;
return FAR_GLOW_COUNT;
}
int visibleParticles(int layer, float strength, float distanceToCamera) {
float countAmount = layer == SPARK_LAYER ? smoothRange(
SPARK_COUNT_RAMP.x, SPARK_COUNT_RAMP.y, strength)
: severity(strength);
float nearCount = mix(minimumParticles(layer), float(layerCapacity(layer)), countAmount);
if (layer == SPARK_LAYER) {
return distanceToCamera >= SPARK_FADE_END
? 0 : int(floor(nearCount + 0.5));
float sparkLod = smoothRange(PARTICLE_LOD_START, SPARK_FADE_END,
distanceToCamera);
float sparkRemaining = 1.0 - sparkLod;
return int(floor(nearCount * sparkRemaining * sparkRemaining + 0.5));
}
float lodAmount = smoothRange(PARTICLE_LOD_START, PARTICLE_LOD_END,
distanceToCamera);
float minimumScale = layer == 3 ? GLOW_LOD_MIN_SCALE : FIRE_LOD_MIN_SCALE;
return max(1, int(floor(nearCount * mix(1.0, minimumScale, lodAmount) + 0.5)));
float fastLod = 1.0 - (1.0 - lodAmount) * (1.0 - lodAmount);
return int(floor(mix(nearCount, farParticles(layer), fastLod) + 0.5));
}
bool selectedSlot(int slot, int count, int capacity) {
int previous = int(floor(float(slot * count) / float(capacity)));
@@ -325,13 +341,17 @@ void renderParticleVertex(vec4 viewPosition, float distanceToCamera, int particl
int expectedPass = particleLayer == 3 ? 1 : (spark ? SPARK_PASS : 0);
int slot = particleIndex - layerStart(particleLayer);
int count = visibleParticles(particleLayer, flameStrength, distanceToCamera);
if ((smokePass && (particleLayer != 1 || distanceToCamera >= SMOKE_FADE_END))
|| (!smokePass && RenderPass != expectedPass)
|| !selectedParticleSlot(particleLayer, slot, count)) {
hidePrimitive();
return;
}
float elapsed;
int generation;
bool alive = particleState(particleLayer, slot, phaseWord,
particleLife, elapsed, generation);
if ((smokePass && particleLayer != 1)
|| (!smokePass && RenderPass != expectedPass)
|| !selectedParticleSlot(particleLayer, slot, count) || !alive) {
if (!alive) {
hidePrimitive();
return;
}
@@ -378,15 +398,20 @@ void renderParticleVertex(vec4 viewPosition, float distanceToCamera, int particl
viewPosition.xy += (offset + quadOffset) * viewportScale;
particleOpacity = spark
? 1.0 - smoothRange(SPARK_FADE_START, SPARK_FADE_END, distanceToCamera)
: 1.0;
: (smokePass
? 1.0 - smoothRange(SMOKE_FADE_START, SMOKE_FADE_END, distanceToCamera)
: 1.0);
gl_Position = ProjMat * viewPosition;
}
void main() {
bool label = RenderPass == LABEL_PASS;
int markerIndex = label ? gl_InstanceID : gl_InstanceID / SlotsPerMarker;
int slot = label ? 0 : gl_InstanceID - markerIndex * SlotsPerMarker;
int batchMarkerIndex = label ? gl_InstanceID : gl_InstanceID / SlotsPerMarker;
int markerIndex = MarkerBase + batchMarkerIndex;
int slot = label ? 0 : gl_InstanceID - batchMarkerIndex * SlotsPerMarker;
FlameMarker marker = markerAt(markerIndex);
vec4 viewPosition = ModelViewMat * vec4(marker.position, 1.0);
vec3 markerPosition = mix(marker.previousPosition, marker.position,
clamp(PartialTick, 0.0, 1.0));
vec4 viewPosition = ModelViewMat * vec4(markerPosition, 1.0);
float distanceToCamera = length(viewPosition.xyz);
localCoord = label ? vec2(Position.x, 1.0 - Position.y) : Position.xy;
vertexColor = vec4(label ? vec3(1.0) : marker.color,
@@ -92,6 +92,24 @@ class WorldEntityFlameClustersTest {
assertEquals(2.0D, marker.flameStrength(), 0.000_001D);
}
@Test
void combinesPreviousAndCurrentAnchorsForTickInterpolation() {
var first = new WorldEntityFlameClusters.Input(
bounds(new Point(0.0D, 0.0D)), metric(1.0D, 1.0D, 1L),
1L, 1, -1.0D, 0.0D, 0.5D);
var second = new WorldEntityFlameClusters.Input(
bounds(new Point(0.5D, 0.0D)), metric(1.0D, 1.0D, 1L),
1L, 2, -1.5D, -0.5D, 0.5D);
WorldEntityFlameClusters.Marker marker =
WorldEntityFlameClusters.cluster(List.of(first, second), 1L).get(0);
assertEquals(0.25D, marker.x(), 0.000_001D);
assertEquals(-1.0D, marker.previousX(), 0.000_001D);
assertEquals(0.5D, marker.previousZ(), 0.000_001D);
assertEquals(65.8D, marker.previousY(), 0.000_001D);
}
@Test
void clustersTheMaximumCoincidentInputBudgetWithoutPairMaterialization() {
List<WorldEntityFlameClusters.Input> inputs = new ArrayList<>();
@@ -1,11 +1,13 @@
package com.moepus.serverwarashi.client.world;
import com.moepus.serverwarashi.common.modules.performance.report.PerfReportData;
import com.moepus.serverwarashi.common.modules.performance.report.PerfReportLimits;
import com.moepus.serverwarashi.common.modules.performance.sampling.TickDetailKind;
import net.minecraft.world.phys.Vec3;
import org.junit.jupiter.api.Test;
import java.nio.FloatBuffer;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -22,8 +24,10 @@ class WorldPerfObjectRendererTest {
@Test
void drawsGpuParticlesInFiveBatchesWithOrangeReusedForSmoke() {
assertEquals(4_096, WorldPerfObjectBuffers.MAX_MARKER_COUNT);
assertEquals(5, WorldPerfObjectBuffers.Batch.values().length);
assertEquals(PerfReportLimits.MAX_WORLD_HOTSPOT_ENTRIES,
WorldPerfObjectBuffers.MAX_MARKER_COUNT);
assertEquals(4, WorldPerfObjectBuffers.TEXELS_PER_MARKER);
assertEquals(2, WorldPerfObjectBuffers.Source.values().length);
assertArrayEquals(new WorldPerfObjectBuffers.Batch[]{
WorldPerfObjectBuffers.Batch.SMOKE,
WorldPerfObjectBuffers.Batch.NORMAL,
@@ -39,11 +43,6 @@ class WorldPerfObjectRendererTest {
assertEquals(24, WorldPerfObjectBuffers.Batch.SMOKE.particleStart());
assertEquals(64, WorldPerfObjectBuffers.Batch.GLOW.particleStart());
assertEquals(80, WorldPerfObjectBuffers.Batch.SPARK.particleStart());
assertEquals(0, WorldPerfObjectBuffers.Batch.NORMAL.renderPass());
assertEquals(1, WorldPerfObjectBuffers.Batch.GLOW.renderPass());
assertEquals(2, WorldPerfObjectBuffers.Batch.SMOKE.renderPass());
assertEquals(3, WorldPerfObjectBuffers.Batch.LABEL.renderPass());
assertEquals(4, WorldPerfObjectBuffers.Batch.SPARK.renderPass());
}
@Test
@@ -61,46 +60,87 @@ class WorldPerfObjectRendererTest {
}
@Test
void convertsWorldAnchorsToCameraRelativeDoublesBeforeUpload() {
var marker = new WorldEntityFlameClusters.Marker(
new WorldEntityFlameClusters.Anchor(30_000_000.25D, 70.5D, -29_999_999.75D),
new PerfReportData.Metric(1.0D, 1.0D, 1L),
new WorldEntityFlameClusters.Visual(1.0D, 0, 1));
WorldPerfObjectBuffers.Entry entry = WorldPerfObjectBuffers.Entry.from(
marker, new Vec3(30_000_000.0D, 64.0D, -30_000_000.0D), 42.0D);
void ordersMarkersByStableEightBlockBuckets() {
List<WorldEntityFlameClusters.Marker> markers = List.of(
markerAtDistance(1.0D, 1),
markerAtDistance(20.0D, 2),
markerAtDistance(9.0D, 3),
markerAtDistance(90.0D, 4),
markerAtDistance(23.0D, 5),
markerAtDistance(24.0D, 6),
markerAtDistance(40.0D, 7),
markerAtDistance(96.0D, 8),
markerAtDistance(97.0D, 9));
assertEquals(0.25D, entry.x());
assertEquals(6.5D, entry.y());
assertEquals(0.25D, entry.z());
assertEquals(42.0D, entry.distanceSquared());
WorldPerfMarkerBuckets.Batch batch = WorldPerfMarkerBuckets.order(
markers, new Vec3(0.0D, 64.0D, 0.0D));
assertEquals(8, batch.size());
assertEquals(4, batch.sparkBase());
assertEquals(3, batch.labelBase());
assertArrayEquals(new int[]{4, 8, 7, 6, 2, 5, 3, 1}, phases(batch));
}
@Test
void encodesOneCompactMarkerRecordWithoutParticleVertices() {
void keepsDistanceBoundariesStrictForLimitedBatches() {
assertEquals(0, WorldPerfMarkerBuckets.bucketForDistanceSquared(0.0D));
assertEquals(0, WorldPerfMarkerBuckets.bucketForDistanceSquared(63.99D));
assertEquals(1, WorldPerfMarkerBuckets.bucketForDistanceSquared(64.0D));
assertEquals(3, WorldPerfMarkerBuckets.bucketForDistanceSquared(24.0D * 24.0D));
assertEquals(5, WorldPerfMarkerBuckets.bucketForDistanceSquared(40.0D * 40.0D));
assertEquals(11, WorldPerfMarkerBuckets.bucketForDistanceSquared(96.0D * 96.0D));
assertEquals(-1, WorldPerfMarkerBuckets.bucketForDistanceSquared(96.01D * 96.01D));
assertEquals(-1, WorldPerfMarkerBuckets.bucketForDistanceSquared(Double.NaN));
}
@Test
void encodesCurrentAndPreviousAnchorsRelativeToStableOrigin() {
var marker = new WorldEntityFlameClusters.Marker(
new WorldEntityFlameClusters.Anchor(9.25D, 70.0D, -5.25D),
new WorldEntityFlameClusters.Anchor(10.25D, 70.5D, -4.75D),
new PerfReportData.Metric(1.0D, 1.0D, 1L),
new WorldEntityFlameClusters.Visual(1.0D, 0x7FFF, 1));
WorldPerfObjectBuffers.Entry entry = WorldPerfObjectBuffers.Entry.from(
marker, new Vec3(10.0D, 64.0D, -5.0D), 1.0D);
FloatBuffer encoded = FloatBuffer.allocate(WorldPerfObjectBuffers.FLOATS_PER_MARKER);
WorldPerfObjectBuffers.encodeMarker(entry, encoded);
WorldPerfObjectBuffers.encodeMarker(marker,
new WorldPerfObjectBuffers.UploadOrigin(10.0D, -5.0D), encoded);
encoded.flip();
assertEquals(12, encoded.remaining());
assertEquals(16, encoded.remaining());
assertEquals(0.25F, encoded.get(), 0.0F);
assertEquals(6.54F, encoded.get(), 0.0001F);
assertEquals(70.54F, encoded.get(), 0.0001F);
assertEquals(0.25F, encoded.get(), 0.0F);
assertEquals(1_024.0F, encoded.get(), 0.0F);
assertEquals(0x40 / 255.0F, encoded.get(), 0.0001F);
assertEquals(0xDC / 255.0F, encoded.get(), 0.0001F);
assertEquals(0x78 / 255.0F, encoded.get(), 0.0001F);
assertEquals(511.0F, encoded.get(), 0.0F);
assertEquals(1_000.0F, encoded.get(), 0.0F);
assertEquals(-1.0F, encoded.get(), 0.0F);
assertEquals(WorldOverlayMath.packLabelStrength(1.0D) / 255.0F,
encoded.get(), 0.0001F);
encoded.position(12);
assertEquals(-0.75F, encoded.get(), 0.0F);
assertEquals(70.04F, encoded.get(), 0.0001F);
assertEquals(-0.25F, encoded.get(), 0.0F);
assertEquals(0.0F, encoded.get(), 0.0F);
}
@Test
void refreshesStaticAtOneSecondAndDynamicOncePerTick() {
assertTrue(WorldPerfObjectRenderer.staticRefreshDue(Long.MIN_VALUE, 100L));
assertFalse(WorldPerfObjectRenderer.staticRefreshDue(100L, 119L));
assertTrue(WorldPerfObjectRenderer.staticRefreshDue(100L, 120L));
assertTrue(WorldPerfObjectRenderer.staticRefreshDue(120L, 5L));
assertFalse(WorldPerfObjectRenderer.dynamicRefreshDue(100L, 100L));
assertTrue(WorldPerfObjectRenderer.dynamicRefreshDue(100L, 101L));
}
private static int[] phases(WorldPerfMarkerBuckets.Batch batch) {
int[] phases = new int[batch.size()];
for (int index = 0; index < phases.length; index++) {
phases[index] = batch.markers()[index].phase();
}
return phases;
}
private static WorldEntityFlameClusters.Marker markerAtDistance(
double distance, int phase) {
return new WorldEntityFlameClusters.Marker(
new WorldEntityFlameClusters.Anchor(distance, 64.0D, 0.0D),
new PerfReportData.Metric(1.0D, 1.0D, 1L),
new WorldEntityFlameClusters.Visual(1.0D, phase, 1));
}
}
@@ -43,11 +43,13 @@ class WorldPerfShaderResourceTest {
assertUniform(root, "MaxFlameStrength", WorldOverlayMath.MAX_PACKED_FLAME_STRENGTH);
assertUniform(root, "MaxFlameViewportFraction",
WorldOverlayMath.MAX_FLAME_VIEWPORT_FRACTION);
assertUniform(root, "PartialTick", 0.0D);
assertUniform(root, "ParticleTime", 0.0D);
assertUniform(root, "ParticleEpoch", 0.0D);
assertUniform(root, "RenderPass", 0.0D);
assertUniform(root, "ParticleStart", 0.0D);
assertUniform(root, "SlotsPerMarker", 1.0D);
assertUniform(root, "MarkerBase", 0.0D);
assertEquals("Position", root.getAsJsonArray("attributes").get(0).getAsString());
assertEquals(1, root.getAsJsonArray("attributes").size());
assertTrue(hasSampler(root, "MarkerData"));
@@ -58,8 +60,14 @@ class WorldPerfShaderResourceTest {
String vertex = resource("world_perf_object.vsh");
assertTrue(vertex.contains("uniform samplerBuffer MarkerData;"));
assertTrue(vertex.contains("texelFetch(MarkerData, base)"));
assertTrue(vertex.contains("texelFetch(MarkerData, base + 3)"));
assertEquals(4, shaderIntConstant(vertex, "MARKER_TEXELS"));
assertTrue(vertex.contains("gl_InstanceID / SlotsPerMarker"));
assertTrue(vertex.contains("MarkerBase + batchMarkerIndex"));
assertTrue(vertex.contains("gl_InstanceID - batchMarkerIndex * SlotsPerMarker"));
assertTrue(vertex.contains("ParticleStart + slot"));
assertTrue(vertex.contains("mix(marker.previousPosition, marker.position"));
assertTrue(vertex.contains("clamp(PartialTick, 0.0, 1.0)"));
assertEquals(WorldFlameParticleLayout.RED_PARTICLE_COUNT,
shaderIntConstant(vertex, "RED_PARTICLE_COUNT"));
assertEquals(WorldFlameParticleLayout.ORANGE_PARTICLE_COUNT,
@@ -80,14 +88,28 @@ class WorldPerfShaderResourceTest {
void objectShaderAppliesLodEnvelopeAndCoherentPlumeSparks() throws Exception {
String vertex = resource("world_perf_object.vsh");
String fragment = resource("world_perf_object.fsh");
assertTrue(vertex.contains("const float PARTICLE_LOD_START = 24.0;"));
assertTrue(vertex.contains("const float SPARK_FADE_END = 24.0;"));
assertTrue(vertex.contains("distanceToCamera >= SPARK_FADE_END"));
assertEquals(16.0D, shaderFloatConstant(vertex, "PARTICLE_LOD_START"));
assertEquals(32.0D, shaderFloatConstant(vertex, "PARTICLE_LOD_END"));
assertEquals(WorldOverlayMath.SPARK_FADE_START,
shaderFloatConstant(vertex, "SPARK_FADE_START"));
assertEquals(WorldOverlayMath.SPARK_FADE_END,
shaderFloatConstant(vertex, "SPARK_FADE_END"));
assertEquals(24.0D, shaderFloatConstant(vertex, "SMOKE_FADE_START"));
assertEquals(32.0D, shaderFloatConstant(vertex, "SMOKE_FADE_END"));
assertTrue(vertex.contains("const float MIN_RED_COUNT = 16.0;"));
assertTrue(vertex.contains("const float MIN_ORANGE_COUNT = 14.0;"));
assertTrue(vertex.contains("const float MIN_YELLOW_COUNT = 14.0;"));
assertTrue(vertex.contains("const float MIN_GLOW_COUNT = 10.0;"));
assertTrue(vertex.contains("const float MIN_SPARK_COUNT = 10.0;"));
assertEquals(4.0D, shaderFloatConstant(vertex, "FAR_RED_COUNT"));
assertEquals(3.0D, shaderFloatConstant(vertex, "FAR_ORANGE_COUNT"));
assertEquals(3.0D, shaderFloatConstant(vertex, "FAR_YELLOW_COUNT"));
assertEquals(1.0D, shaderFloatConstant(vertex, "FAR_GLOW_COUNT"));
assertTrue(vertex.contains("float fastLod = 1.0 - (1.0 - lodAmount)"));
assertTrue(vertex.contains("nearCount * sparkRemaining * sparkRemaining"));
assertTrue(vertex.contains("distanceToCamera >= SMOKE_FADE_END"));
assertTrue(vertex.indexOf("!selectedParticleSlot")
< vertex.indexOf("bool alive = particleState"));
assertTrue(vertex.contains("const vec2 SPARK_COUNT_RAMP = vec2(3.50, 5.0);"));
assertTrue(vertex.contains("const float GREEN_BASE_WIDTH = 1.5;"));
assertTrue(vertex.contains("const float GREEN_TIP_WIDTH = 0.8;"));
@@ -178,6 +200,17 @@ class WorldPerfShaderResourceTest {
return Integer.parseInt(shader.substring(valueStart, valueEnd));
}
private static double shaderFloatConstant(String shader, String name) {
String prefix = "const float " + name + " = ";
int valueStart = shader.indexOf(prefix);
if (valueStart < 0) {
throw new AssertionError("Missing shader constant " + name);
}
valueStart += prefix.length();
int valueEnd = shader.indexOf(';', valueStart);
return Double.parseDouble(shader.substring(valueStart, valueEnd));
}
private static void assertCompleteDefaults(JsonObject root) {
for (var uniform : root.getAsJsonArray("uniforms")) {
JsonObject object = uniform.getAsJsonObject();