Stutter when reading large number B&C block entities on the client
SquidDev opened this issue · 0 comments
I've been drying to debug some slight FPS stutter I've been having on the client. One of the (many!) causes of this appears to be due to deserialising B&C block entities on the client.
Taking a client-side profile using F3+L (plus some custom instrumentation to also track BE deserialisation) shows that reading 503 bitsandchisels:bits_block_entity
s takes approximately 204ms.
Version: 1.18.2
Time span: 583 ms
Tick span: 1 ticks
// This is approximately 1.71 ticks per second. It should be 20 ticks per second
--- BEGIN PROFILE DUMP ---
[00] scheduledExecutables(1/1) - 94.71%/94.71%
[01] | #Loading ecotones:treetap 10/10
[01] | #Loading computercraft:computer_advanced 2/2
[01] | #Loading bitsandchisels:bits_block_entity 508/508
[01] | #Loading botania:agricarnation 1/1
[01] | unspecified(1/1) - 61.82%/58.55%
[01] | BlockEntity deserialisation(521/521) - 38.14%/36.12%
[02] | | bitsandchisels:bits_block_entity(508/508) - 98.62%/35.62%
[02] | | unspecified(521/521) - 1.34%/0.49%
[02] | | ecotones:treetap(10/10) - 0.02%/0.01%
[02] | | computercraft:computer_advanced(2/2) - 0.01%/0.00%
[02] | | botania:agricarnation(1/1) - 0.00%/0.00%
[01] | Purple Water Chunk Sync(90/90) - 0.05%/0.04%
This only happens on a single frame when moving across chunk boundaries, and so this doesn't show up on normally on Spark/VisualVM profiles. Using spark with a very low sampling interval and setting --only-ticks-over 500
gave me a report like this.
Some ideas for optimisations
I made a couple of small changes in the patch below, which helped a bit but not as much as I might have liked:
0001-Some-micro-optimisations.patch
commit 65d67a808eeb43e9ae8c2af8976e594e09ca1af3
Author: Jonathan Coates <[email protected]>
Date: Sat Jun 11 23:37:30 2022 +0100
Some micro-optimisations
diff --git a/src/main/java/io/github/coolmineman/bitsandchisels/BitMeshes.java b/src/main/java/io/github/coolmineman/bitsandchisels/BitMeshes.java
index f235d78..96855f5 100644
--- a/src/main/java/io/github/coolmineman/bitsandchisels/BitMeshes.java
+++ b/src/main/java/io/github/coolmineman/bitsandchisels/BitMeshes.java
@@ -27,7 +27,7 @@ public class BitMeshes {
private static final Direction[] X_DIRECTIONS = {Direction.EAST, Direction.WEST};
private static final Direction[] Y_DIRECTIONS = {Direction.UP, Direction.DOWN};
private static final Direction[] Z_DIRECTIONS = {Direction.SOUTH, Direction.NORTH};
- private static final float ONE_PIXEL = 1f/16f;
+ private static final float ONE_PIXEL = 1f/16f;
private BitMeshes() { }
@@ -35,7 +35,7 @@ public class BitMeshes {
MeshBuilder builder = RendererAccess.INSTANCE.getRenderer().meshBuilder();
QuadEmitter emitter = builder.getEmitter();
Vec3f tmp = new Vec3f();
- boolean[][] used = new boolean[16][16];
+ short[] used = new short[16];
//X
for (Direction d : X_DIRECTIONS) {
@@ -44,12 +44,12 @@ public class BitMeshes {
for (int cy = 0; cy < 16; cy++) {
for (int cz = 0; cz < 16; cz++) {
BlockState state = states[cx][cy][cz];
- if (state.isAir() || used[cy][cz] || !quadNeeded(states, d, cx, cy, cz)) continue;
+ if (state.isAir() || isUsed(used, cy, cz) || !quadNeeded(states, d, cx, cy, cz)) continue;
int cy2 = cy;
int cz2 = cz;
//Greed Y
for (int ty = cy; ty < 16; ty++) {
- if (states[cx][ty][cz] == state && !used[ty][cz] && quadNeeded(states, d, cx, ty, cz)) {
+ if (states[cx][ty][cz] == state && !isUsed(used, ty, cz) && quadNeeded(states, d, cx, ty, cz)) {
cy2 = ty;
} else {
break;
@@ -58,7 +58,7 @@ public class BitMeshes {
// Greed Z
greedz: for (int tz = cz; tz < 16; tz++) {
for (int ty = cy; ty <= cy2; ty++) {
- if (states[cx][ty][tz] != state || used[ty][tz] || !quadNeeded(states, d, cx, ty, tz)) {
+ if (states[cx][ty][tz] != state || isUsed(used, ty, tz) || !quadNeeded(states, d, cx, ty, tz)) {
break greedz;
}
}
@@ -66,7 +66,7 @@ public class BitMeshes {
}
for (int qy = cy; qy <= cy2; qy++) {
for (int qz = cz; qz <= cz2; qz++) {
- used[qy][qz] = true;
+ used[qy] |= 1 << qz;
}
}
@@ -83,12 +83,12 @@ public class BitMeshes {
for (int cx = 0; cx < 16; cx++) {
for (int cz = 0; cz < 16; cz++) {
BlockState state = states[cx][cy][cz];
- if (state.isAir() || used[cx][cz] || !quadNeeded(states, d, cx, cy, cz)) continue;
+ if (state.isAir() || isUsed(used, cx, cz) || !quadNeeded(states, d, cx, cy, cz)) continue;
int cx2 = cx;
int cz2 = cz;
//Greed X
for (int tx = cx; tx < 16; tx++) {
- if (states[tx][cy][cz] == state && !used[tx][cz] && quadNeeded(states, d, tx, cy, cz)) {
+ if (states[tx][cy][cz] == state && !isUsed(used, tx, cz) && quadNeeded(states, d, tx, cy, cz)) {
cx2 = tx;
} else {
break;
@@ -97,7 +97,7 @@ public class BitMeshes {
// Greed Z
greedz: for (int tz = cz; tz < 16; tz++) {
for (int tx = cx; tx <= cx2; tx++) {
- if (states[tx][cy][tz] != state || used[tx][tz] || !quadNeeded(states, d, tx, cy, tz)) {
+ if (states[tx][cy][tz] != state || isUsed(used, tx, tz) || !quadNeeded(states, d, tx, cy, tz)) {
break greedz;
}
}
@@ -105,7 +105,7 @@ public class BitMeshes {
}
for (int qx = cx; qx <= cx2; qx++) {
for (int qz = cz; qz <= cz2; qz++) {
- used[qx][qz] = true;
+ used[qx] |= 1 << qz;
}
}
@@ -122,12 +122,12 @@ public class BitMeshes {
for (int cx = 0; cx < 16; cx++) {
for (int cy = 0; cy < 16; cy++) {
BlockState state = states[cx][cy][cz];
- if (state.isAir() || used[cx][cy] || !quadNeeded(states, d, cx, cy, cz)) continue;
+ if (state.isAir() || isUsed(used, cx, cy) || !quadNeeded(states, d, cx, cy, cz)) continue;
int cx2 = cx;
int cy2 = cy;
//Greed X
for (int tx = cx; tx < 16; tx++) {
- if (states[tx][cy][cz] == state && !used[tx][cz] && quadNeeded(states, d, tx, cy, cz)) {
+ if (states[tx][cy][cz] == state && !isUsed(used, tx, cz) && quadNeeded(states, d, tx, cy, cz)) {
cx2 = tx;
} else {
break;
@@ -136,7 +136,7 @@ public class BitMeshes {
// Greed Y
greedy: for (int ty = cy; ty < 16; ty++) {
for (int tx = cx; tx <= cx2; tx++) {
- if (states[tx][ty][cz] != state || used[tx][ty] || !quadNeeded(states, d, tx, ty, cz)) {
+ if (states[tx][ty][cz] != state || isUsed(used, tx, ty) || !quadNeeded(states, d, tx, ty, cz)) {
break greedy;
}
}
@@ -144,7 +144,7 @@ public class BitMeshes {
}
for (int qx = cx; qx <= cx2; qx++) {
for (int qy = cy; qy <= cy2; qy++) {
- used[qx][qy] = true;
+ used[qx] |= 1 << qy;
}
}
@@ -173,32 +173,46 @@ public class BitMeshes {
private static boolean quadNeeded(BlockState[][][] states, Direction d, int x, int y, int z) {
switch (d) {
- case UP:
- if (y <= 14) return states[x][y + 1][z].isAir() || (RenderLayers.getBlockLayer(states[x][y + 1][z]) != RenderLayer.getSolid() && states[x][y][z] != states[x][y + 1][z]);
- return true;
- case DOWN:
- if (y >= 1) return states[x][y - 1][z].isAir() || (RenderLayers.getBlockLayer(states[x][y - 1][z]) != RenderLayer.getSolid() && states[x][y][z] != states[x][y - 1][z]);
- return true;
- case SOUTH:
- if (z <= 14) return states[x][y][z + 1].isAir() || (RenderLayers.getBlockLayer(states[x][y][z + 1]) != RenderLayer.getSolid() && states[x][y][z] != states[x][y][z + 1]);
- return true;
- case NORTH:
- if (z >= 1) return states[x][y][z - 1].isAir() || (RenderLayers.getBlockLayer(states[x][y][z - 1]) != RenderLayer.getSolid() && states[x][y][z] != states[x][y][z - 1]);
- return true;
- case EAST:
- if (x <= 14) return states[x + 1][y][z].isAir() || (RenderLayers.getBlockLayer(states[x + 1][y][z]) != RenderLayer.getSolid() && states[x][y][z] != states[x + 1][y][z]);
- return true;
- case WEST:
- if (x >= 1) return states[x - 1][y][z].isAir() || (RenderLayers.getBlockLayer(states[x - 1][y][z]) != RenderLayer.getSolid() && states[x][y][z] != states[x - 1][y][z]);
- return true;
+ case UP: {
+ if (y > 14) return true;
+ BlockState state = states[x][y + 1][z];
+ return state.isAir() || (states[x][y][z] != state && RenderLayers.getBlockLayer(state) != RenderLayer.getSolid());
+ }
+ case DOWN: {
+ if (y < 1) return true;
+ BlockState state = states[x][y - 1][z];
+ return state.isAir() || (states[x][y][z] != state && RenderLayers.getBlockLayer(state) != RenderLayer.getSolid());
+ }
+ case SOUTH: {
+ if (z > 14) return true;
+ BlockState state = states[x][y][z + 1];
+ return state.isAir() || (states[x][y][z] != state && RenderLayers.getBlockLayer(state) != RenderLayer.getSolid());
+ }
+ case NORTH: {
+ if (z < 1) return true;
+ BlockState state = states[x][y][z - 1];
+ return state.isAir() || (states[x][y][z] != state && RenderLayers.getBlockLayer(state) != RenderLayer.getSolid());
+ }
+ case EAST: {
+ if (x > 14) return true;
+ BlockState state = states[x + 1][y][z];
+ return state.isAir() || (states[x][y][z] != state && RenderLayers.getBlockLayer(state) != RenderLayer.getSolid());
+ }
+ case WEST: {
+ if (x < 1) return true;
+ BlockState state = states[x - 1][y][z];
+ return state.isAir() || (states[x][y][z] != state && RenderLayers.getBlockLayer(state) != RenderLayer.getSolid());
+ }
}
return true;
}
- private static void clear(boolean[][] a) {
- for(int i = 0; i < a.length; i++) {
- Arrays.fill(a[i], false);
- }
+ private static void clear(short[] a) {
+ Arrays.fill(a, (short) 0);
+ }
+
+ private static boolean isUsed(short[] a, int x, int y) {
+ return (a[x] & (1 << y)) != 0;
}
private static boolean canCull(Direction d, int x, int y, int z) {
@@ -268,5 +282,5 @@ public class BitMeshes {
float diff = Math.abs(desiredValue - actualValue);
return diff < 0.01f;
}
-
+
}
diff --git a/src/main/java/io/github/coolmineman/bitsandchisels/BitsBlockEntity.java b/src/main/java/io/github/coolmineman/bitsandchisels/BitsBlockEntity.java
index 76fcaed..19c7fd5 100644
--- a/src/main/java/io/github/coolmineman/bitsandchisels/BitsBlockEntity.java
+++ b/src/main/java/io/github/coolmineman/bitsandchisels/BitsBlockEntity.java
@@ -22,6 +22,8 @@ import net.minecraft.util.shape.BitSetVoxelSet;
import net.minecraft.util.shape.VoxelShape;
import net.minecraft.util.shape.VoxelShapes;
+import java.util.Arrays;
+
public class BitsBlockEntity extends BlockEntity implements RenderAttachmentBlockEntity {
private BlockState[][][] states;
@Environment(EnvType.CLIENT)
@@ -38,9 +40,7 @@ public class BitsBlockEntity extends BlockEntity implements RenderAttachmentBloc
this(new BlockState[16][16][16], pos, state, alive);
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
- for (int k = 0; k < 16; k++) {
- states[i][j][k] = fillState;
- }
+ Arrays.fill(states[i][j], fillState);
}
}
}
- Use a
short[]
instead of aboolean[][16]
. Tiny micro-opimisation, but does actually help a bit with clearing the array. - Recorder conditionals in
quadNeeded
to calls toRenderLayers.getBlockLayer
.
There's some other possible changes I can think of which might help1 but are more invasive:
-
Switch to using a
BlockState[16 * 16 * 16]
instead of aBlockState[16][16][16]
. -
Don't call
BitsBlockEntity.rebuildNbtCache
on the client (actually, this isn't invasive and is probably an easy win!) -
Move model baking off-thread. I don't actually know how feasible this is - what I'm thinking is make
getRenderAttachmentData
return a object which computes (and caches!) the model as part of chunk baking instead of on the main thread.This would of course massively complicate things - you'd still need to compute block colours on the render thread. It may just not be possible or not give you any performance gains.
Footnotes
-
Emphasis on might! I'm afraid I haven't implemented any of these, let alone profiled them. ↩