Skip to content

Commit 7165b37

Browse files
authored
Significantly optimize updating RenderChunk positions in ViewFrustum (#782)
* Significantly optimize updating RenderChunk positions in ViewFrustum Rather than simply performing the updates asynchronously in a different thread (which introduces potential race condition and just offloads the issue to another core), this actually makes the RenderChunk position updates significantly faster in most common cases. We observe that under normal gameplay circumstances, the camera rarely moves more than one cube per frame. By detecting this common case, we can efficiently skip RenderChunks whose position hasn't changed, as when the camera moves by one cube in a given direction, only one 2D slice/plane of RenderChunks are actually changed. On my machine, with a horizontal render distance of 48 chunks and a vertical render distance of 16 cubes, and while flying around at maximum speed in spectator mode, this change reduces ViewFrustum#updateChunkPositions() from ~22% of the total client thread CPU time to ~2.4%, nearly an order of magnitude performance improvement. * Update GitHub PR workflow step versions
1 parent 4911e62 commit 7165b37

File tree

2 files changed

+139
-39
lines changed

2 files changed

+139
-39
lines changed

.github/workflows/build_pr.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ jobs:
66
build:
77
runs-on: ubuntu-latest
88
steps:
9-
- uses: actions/checkout@v1
10-
- uses: actions/cache@v2
9+
- uses: actions/checkout@v4
10+
- uses: actions/cache@v4
1111
with:
1212
path: |
1313
~/.gradle/caches

src/main/java/io/github/opencubicchunks/cubicchunks/core/asm/mixin/core/client/MixinViewFrustum_RenderHeightFix.java

+137-37
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@
4343
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
4444
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
4545

46-
import java.util.concurrent.ExecutorService;
47-
import java.util.concurrent.Executors;
46+
import java.util.function.BooleanSupplier;
4847

4948
import javax.annotation.ParametersAreNonnullByDefault;
5049

@@ -57,19 +56,16 @@
5756
@Mixin(ViewFrustum.class)
5857
public class MixinViewFrustum_RenderHeightFix {
5958

60-
@Unique private static final ExecutorService BACKGROUND_EXECUTOR = Executors.newSingleThreadExecutor((runnable) -> {
61-
Thread t = new Thread(runnable);
62-
t.setDaemon(true);
63-
t.setName("ViewFrustum RenderChunk position updater (CubicChunks)");
64-
return t;
65-
});
66-
6759
@Shadow @Final protected World world;
6860
@SuppressWarnings("MismatchedReadAndWriteOfArray") @Shadow public RenderChunk[] renderChunks;
6961
@Shadow protected int countChunksX;
7062
@Shadow protected int countChunksY;
7163
@Shadow protected int countChunksZ;
7264

65+
@Unique private int cubicchunks_oldViewX = Integer.MAX_VALUE; //sufficiently large default value that it can never intersect with real values
66+
@Unique private int cubicchunks_oldViewY = Integer.MAX_VALUE;
67+
@Unique private int cubicchunks_oldViewZ = Integer.MAX_VALUE;
68+
7369
@Inject(method = "updateChunkPositions", at = @At(value = "HEAD"), cancellable = true, require = 1)
7470
private void updateChunkPositionsInject(double viewEntityX, double viewEntityZ, CallbackInfo cbi) {
7571
if (!((ICubicWorld) world).isCubicWorld()) {
@@ -85,44 +81,148 @@ private void updateChunkPositionsInject(double viewEntityX, double viewEntityZ,
8581
int dz = countChunksZ;
8682
RenderChunk[] chunks = this.renderChunks;
8783

88-
BACKGROUND_EXECUTOR.submit(() -> {
89-
int minX = viewX - (dx >> 1);
90-
int minY = viewY - (dy >> 1);
91-
int minZ = viewZ - (dz >> 1);
92-
int px = MathHelper.intFloorDiv(minX, dx) * dx;
93-
int py = MathHelper.intFloorDiv(minY, dy) * dy;
94-
int pz = MathHelper.intFloorDiv(minZ, dz) * dz;
95-
96-
for (int zIndex = 0; zIndex < this.countChunksZ; zIndex++) {
97-
int blockZ = pz + zIndex;
98-
if (blockZ < minZ) {
99-
blockZ += dz;
84+
//the coordinate of the RenderChunk in the lowest corner
85+
int minX = viewX - (dx >> 1);
86+
int minY = viewY - (dy >> 1);
87+
int minZ = viewZ - (dz >> 1);
88+
89+
//the coordinate of a RenderChunk which sits at the origin. Wraps around within the min/max range
90+
int px = MathHelper.intFloorDiv(minX, dx) * dx;
91+
int py = MathHelper.intFloorDiv(minY, dy) * dy;
92+
int pz = MathHelper.intFloorDiv(minZ, dz) * dz;
93+
94+
//use longs here just in case the int values overflow (they shouldn't ever, but i want to play it safe)
95+
long changeX = (long) viewX - this.cubicchunks_oldViewX;
96+
long changeY = (long) viewY - this.cubicchunks_oldViewY;
97+
long changeZ = (long) viewZ - this.cubicchunks_oldViewZ;
98+
this.cubicchunks_oldViewX = viewX;
99+
this.cubicchunks_oldViewY = viewY;
100+
this.cubicchunks_oldViewZ = viewZ;
101+
102+
if (Math.abs(changeX) <= 1 && Math.abs(changeY) <= 1 && Math.abs(changeZ) <= 1) {
103+
//fast-path: the camera has moved by at most one cube so we only need to perform updates along a 2d plane
104+
105+
/*
106+
* d: 4
107+
*
108+
* 0123456789 0123456789 0123456789 0123456789 0123456789 .
109+
* min: # min: # min: # min: # min: # .
110+
* p: # p: # p: # p: # p: # .
111+
* 0+p: # 0+p: * # 0+p: * # 0+p: * # 0+p: # .
112+
* 1+p: # 1+p: # 1+p: * # 1+p: * # 1+p: # .
113+
* 2+p: # 2+p: # 2+p: # 2+p: * # 2+p: # .
114+
* 3+p: # 3+p: # 3+p: # 3+p: # 3+p: # .
115+
*/
116+
117+
if (changeX != 0) { //we'll need to update one layer of RenderChunks perpendicular to the YZ plane
118+
int xIndex = Math.floorMod(changeX < 0 ? minX - px : minX - px - 1, dx);
119+
int blockX = cubicchunks_getBlockCoord(xIndex, dx, px, minX);
120+
121+
for (int zIndex = 0; zIndex < dz; zIndex++) {
122+
int blockZ = cubicchunks_getBlockCoord(zIndex, dz, pz, minZ);
123+
int idxZ = zIndex * dy * dx;
124+
125+
for (int yIndex = 0; yIndex < dy; yIndex++) {
126+
int blockY = cubicchunks_getBlockCoord(yIndex, dy, py, minY);
127+
int idxYZ = idxZ + yIndex * dx;
128+
129+
chunks[idxYZ + xIndex].setPosition(blockX, blockY, blockZ);
130+
}
131+
}
132+
}
133+
134+
if (changeY != 0) { //we'll need to update one layer of RenderChunks perpendicular to the XZ plane
135+
int yIndex = Math.floorMod(changeY < 0 ? minY - py : minY - py - 1, dy);
136+
int blockY = cubicchunks_getBlockCoord(yIndex, dy, py, minY);
137+
138+
for (int zIndex = 0; zIndex < dz; zIndex++) {
139+
int blockZ = cubicchunks_getBlockCoord(zIndex, dz, pz, minZ);
140+
int idxZ = zIndex * dy * dx;
141+
142+
int idxYZ = idxZ + yIndex * dx;
143+
144+
for (int xIndex = 0; xIndex < dx; xIndex++) {
145+
int blockX = cubicchunks_getBlockCoord(xIndex, dx, px, minX);
146+
147+
chunks[idxYZ + xIndex].setPosition(blockX, blockY, blockZ);
148+
}
100149
}
101-
blockZ <<= 4;
102-
int idxZ = zIndex * this.countChunksY * this.countChunksX;
150+
}
103151

104-
for (int yIndex = 0; yIndex < this.countChunksY; yIndex++) {
105-
int blockY = py + yIndex;
106-
if (blockY < minY) {
107-
blockY += dy;
152+
if (changeZ != 0) { //we'll need to update one layer of RenderChunks perpendicular to the XY plane
153+
int zIndex = Math.floorMod(changeZ < 0 ? minZ - pz : minZ - pz - 1, dz);
154+
int blockZ = cubicchunks_getBlockCoord(zIndex, dz, pz, minZ);
155+
int idxZ = zIndex * dy * dx;
156+
157+
for (int yIndex = 0; yIndex < dy; yIndex++) {
158+
int blockY = cubicchunks_getBlockCoord(yIndex, dy, py, minY);
159+
int idxYZ = idxZ + yIndex * dx;
160+
161+
for (int xIndex = 0; xIndex < dx; xIndex++) {
162+
int blockX = cubicchunks_getBlockCoord(xIndex, dx, px, minX);
163+
164+
chunks[idxYZ + xIndex].setPosition(blockX, blockY, blockZ);
108165
}
109-
blockY <<= 4;
110-
int idxYZ = idxZ + yIndex * this.countChunksX;
111-
for (int xIndex = 0; xIndex < this.countChunksX; xIndex++) {
112-
int blockX = px + xIndex;
113-
if (blockX < minX) {
114-
blockX += dx;
166+
}
167+
}
168+
169+
//run the original loop to double-check that all RenderChunks are in the correct position
170+
// (doing this cancels out any benefits from skipping unchanged RenderChunks, but only runs with assertions enabled)
171+
assert ((BooleanSupplier) () -> {
172+
for (int zIndex = 0; zIndex < dz; zIndex++) {
173+
int blockZ = cubicchunks_getBlockCoord(zIndex, dz, pz, minZ);
174+
int idxZ = zIndex * dy * dx;
175+
176+
for (int yIndex = 0; yIndex < dy; yIndex++) {
177+
int blockY = cubicchunks_getBlockCoord(yIndex, dy, py, minY);
178+
int idxYZ = idxZ + yIndex * dx;
179+
180+
for (int xIndex = 0; xIndex < dx; xIndex++) {
181+
int blockX = cubicchunks_getBlockCoord(xIndex, dx, px, minX);
182+
BlockPos pos = chunks[idxYZ + xIndex].getPosition();
183+
184+
if (pos.getX() != blockX || pos.getY() != blockY || pos.getZ() != blockZ) {
185+
return false;
186+
}
115187
}
116-
blockX <<= 4;
117-
RenderChunk renderer = chunks[idxYZ + xIndex];
118-
renderer.setPosition(blockX, blockY, blockZ);
188+
}
189+
}
190+
return true;
191+
}).getAsBoolean() : "Not all RenderChunks are in the correct position!";
192+
} else {
193+
//slow path, this behaves like the original vanilla code.
194+
//loop over all RenderChunks and set their position.
195+
196+
//original loop, cleaned up:
197+
for (int zIndex = 0; zIndex < dz; zIndex++) {
198+
int blockZ = cubicchunks_getBlockCoord(zIndex, dz, pz, minZ);
199+
int idxZ = zIndex * dy * dx;
200+
201+
for (int yIndex = 0; yIndex < dy; yIndex++) {
202+
int blockY = cubicchunks_getBlockCoord(yIndex, dy, py, minY);
203+
int idxYZ = idxZ + yIndex * dx;
204+
205+
for (int xIndex = 0; xIndex < dx; xIndex++) {
206+
int blockX = cubicchunks_getBlockCoord(xIndex, dx, px, minX);
207+
208+
chunks[idxYZ + xIndex].setPosition(blockX, blockY, blockZ);
119209
}
120210
}
121211
}
122-
});
212+
}
213+
123214
cbi.cancel();
124215
}
125216

217+
@Unique
218+
private static int cubicchunks_getBlockCoord(int index, int d, int p, int min) {
219+
int coord = p + index;
220+
if (coord < min) {
221+
coord += d;
222+
}
223+
return coord << 4;
224+
}
225+
126226
@Inject(method = "getRenderChunk", at = @At(value = "HEAD"), cancellable = true, require = 1)
127227
private void getRenderChunkInject(BlockPos pos, CallbackInfoReturnable<RenderChunk> cbi) {
128228
if (!((ICubicWorld) world).isCubicWorld()) {

0 commit comments

Comments
 (0)