From d9a074aa8dfdc69088178d3f2a17e089645bf80a Mon Sep 17 00:00:00 2001 From: DaPorkchop_ Date: Tue, 11 Mar 2025 16:40:10 +0100 Subject: [PATCH 1/2] 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. --- .../MixinViewFrustum_RenderHeightFix.java | 174 ++++++++++++++---- 1 file changed, 137 insertions(+), 37 deletions(-) diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/core/asm/mixin/core/client/MixinViewFrustum_RenderHeightFix.java b/src/main/java/io/github/opencubicchunks/cubicchunks/core/asm/mixin/core/client/MixinViewFrustum_RenderHeightFix.java index 1f03a8532..830c33e9b 100644 --- a/src/main/java/io/github/opencubicchunks/cubicchunks/core/asm/mixin/core/client/MixinViewFrustum_RenderHeightFix.java +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/core/asm/mixin/core/client/MixinViewFrustum_RenderHeightFix.java @@ -43,8 +43,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.function.BooleanSupplier; import javax.annotation.ParametersAreNonnullByDefault; @@ -57,19 +56,16 @@ @Mixin(ViewFrustum.class) public class MixinViewFrustum_RenderHeightFix { - @Unique private static final ExecutorService BACKGROUND_EXECUTOR = Executors.newSingleThreadExecutor((runnable) -> { - Thread t = new Thread(runnable); - t.setDaemon(true); - t.setName("ViewFrustum RenderChunk position updater (CubicChunks)"); - return t; - }); - @Shadow @Final protected World world; @SuppressWarnings("MismatchedReadAndWriteOfArray") @Shadow public RenderChunk[] renderChunks; @Shadow protected int countChunksX; @Shadow protected int countChunksY; @Shadow protected int countChunksZ; + @Unique private int cubicchunks_oldViewX = Integer.MAX_VALUE; //sufficiently large default value that it can never intersect with real values + @Unique private int cubicchunks_oldViewY = Integer.MAX_VALUE; + @Unique private int cubicchunks_oldViewZ = Integer.MAX_VALUE; + @Inject(method = "updateChunkPositions", at = @At(value = "HEAD"), cancellable = true, require = 1) private void updateChunkPositionsInject(double viewEntityX, double viewEntityZ, CallbackInfo cbi) { if (!((ICubicWorld) world).isCubicWorld()) { @@ -85,44 +81,148 @@ private void updateChunkPositionsInject(double viewEntityX, double viewEntityZ, int dz = countChunksZ; RenderChunk[] chunks = this.renderChunks; - BACKGROUND_EXECUTOR.submit(() -> { - int minX = viewX - (dx >> 1); - int minY = viewY - (dy >> 1); - int minZ = viewZ - (dz >> 1); - int px = MathHelper.intFloorDiv(minX, dx) * dx; - int py = MathHelper.intFloorDiv(minY, dy) * dy; - int pz = MathHelper.intFloorDiv(minZ, dz) * dz; - - for (int zIndex = 0; zIndex < this.countChunksZ; zIndex++) { - int blockZ = pz + zIndex; - if (blockZ < minZ) { - blockZ += dz; + //the coordinate of the RenderChunk in the lowest corner + int minX = viewX - (dx >> 1); + int minY = viewY - (dy >> 1); + int minZ = viewZ - (dz >> 1); + + //the coordinate of a RenderChunk which sits at the origin. Wraps around within the min/max range + int px = MathHelper.intFloorDiv(minX, dx) * dx; + int py = MathHelper.intFloorDiv(minY, dy) * dy; + int pz = MathHelper.intFloorDiv(minZ, dz) * dz; + + //use longs here just in case the int values overflow (they shouldn't ever, but i want to play it safe) + long changeX = (long) viewX - this.cubicchunks_oldViewX; + long changeY = (long) viewY - this.cubicchunks_oldViewY; + long changeZ = (long) viewZ - this.cubicchunks_oldViewZ; + this.cubicchunks_oldViewX = viewX; + this.cubicchunks_oldViewY = viewY; + this.cubicchunks_oldViewZ = viewZ; + + if (Math.abs(changeX) <= 1 && Math.abs(changeY) <= 1 && Math.abs(changeZ) <= 1) { + //fast-path: the camera has moved by at most one cube so we only need to perform updates along a 2d plane + + /* + * d: 4 + * + * 0123456789 0123456789 0123456789 0123456789 0123456789 . + * min: # min: # min: # min: # min: # . + * p: # p: # p: # p: # p: # . + * 0+p: # 0+p: * # 0+p: * # 0+p: * # 0+p: # . + * 1+p: # 1+p: # 1+p: * # 1+p: * # 1+p: # . + * 2+p: # 2+p: # 2+p: # 2+p: * # 2+p: # . + * 3+p: # 3+p: # 3+p: # 3+p: # 3+p: # . + */ + + if (changeX != 0) { //we'll need to update one layer of RenderChunks perpendicular to the YZ plane + int xIndex = Math.floorMod(changeX < 0 ? minX - px : minX - px - 1, dx); + int blockX = cubicchunks_getBlockCoord(xIndex, dx, px, minX); + + for (int zIndex = 0; zIndex < dz; zIndex++) { + int blockZ = cubicchunks_getBlockCoord(zIndex, dz, pz, minZ); + int idxZ = zIndex * dy * dx; + + for (int yIndex = 0; yIndex < dy; yIndex++) { + int blockY = cubicchunks_getBlockCoord(yIndex, dy, py, minY); + int idxYZ = idxZ + yIndex * dx; + + chunks[idxYZ + xIndex].setPosition(blockX, blockY, blockZ); + } + } + } + + if (changeY != 0) { //we'll need to update one layer of RenderChunks perpendicular to the XZ plane + int yIndex = Math.floorMod(changeY < 0 ? minY - py : minY - py - 1, dy); + int blockY = cubicchunks_getBlockCoord(yIndex, dy, py, minY); + + for (int zIndex = 0; zIndex < dz; zIndex++) { + int blockZ = cubicchunks_getBlockCoord(zIndex, dz, pz, minZ); + int idxZ = zIndex * dy * dx; + + int idxYZ = idxZ + yIndex * dx; + + for (int xIndex = 0; xIndex < dx; xIndex++) { + int blockX = cubicchunks_getBlockCoord(xIndex, dx, px, minX); + + chunks[idxYZ + xIndex].setPosition(blockX, blockY, blockZ); + } } - blockZ <<= 4; - int idxZ = zIndex * this.countChunksY * this.countChunksX; + } - for (int yIndex = 0; yIndex < this.countChunksY; yIndex++) { - int blockY = py + yIndex; - if (blockY < minY) { - blockY += dy; + if (changeZ != 0) { //we'll need to update one layer of RenderChunks perpendicular to the XY plane + int zIndex = Math.floorMod(changeZ < 0 ? minZ - pz : minZ - pz - 1, dz); + int blockZ = cubicchunks_getBlockCoord(zIndex, dz, pz, minZ); + int idxZ = zIndex * dy * dx; + + for (int yIndex = 0; yIndex < dy; yIndex++) { + int blockY = cubicchunks_getBlockCoord(yIndex, dy, py, minY); + int idxYZ = idxZ + yIndex * dx; + + for (int xIndex = 0; xIndex < dx; xIndex++) { + int blockX = cubicchunks_getBlockCoord(xIndex, dx, px, minX); + + chunks[idxYZ + xIndex].setPosition(blockX, blockY, blockZ); } - blockY <<= 4; - int idxYZ = idxZ + yIndex * this.countChunksX; - for (int xIndex = 0; xIndex < this.countChunksX; xIndex++) { - int blockX = px + xIndex; - if (blockX < minX) { - blockX += dx; + } + } + + //run the original loop to double-check that all RenderChunks are in the correct position + // (doing this cancels out any benefits from skipping unchanged RenderChunks, but only runs with assertions enabled) + assert ((BooleanSupplier) () -> { + for (int zIndex = 0; zIndex < dz; zIndex++) { + int blockZ = cubicchunks_getBlockCoord(zIndex, dz, pz, minZ); + int idxZ = zIndex * dy * dx; + + for (int yIndex = 0; yIndex < dy; yIndex++) { + int blockY = cubicchunks_getBlockCoord(yIndex, dy, py, minY); + int idxYZ = idxZ + yIndex * dx; + + for (int xIndex = 0; xIndex < dx; xIndex++) { + int blockX = cubicchunks_getBlockCoord(xIndex, dx, px, minX); + BlockPos pos = chunks[idxYZ + xIndex].getPosition(); + + if (pos.getX() != blockX || pos.getY() != blockY || pos.getZ() != blockZ) { + return false; + } } - blockX <<= 4; - RenderChunk renderer = chunks[idxYZ + xIndex]; - renderer.setPosition(blockX, blockY, blockZ); + } + } + return true; + }).getAsBoolean() : "Not all RenderChunks are in the correct position!"; + } else { + //slow path, this behaves like the original vanilla code. + //loop over all RenderChunks and set their position. + + //original loop, cleaned up: + for (int zIndex = 0; zIndex < dz; zIndex++) { + int blockZ = cubicchunks_getBlockCoord(zIndex, dz, pz, minZ); + int idxZ = zIndex * dy * dx; + + for (int yIndex = 0; yIndex < dy; yIndex++) { + int blockY = cubicchunks_getBlockCoord(yIndex, dy, py, minY); + int idxYZ = idxZ + yIndex * dx; + + for (int xIndex = 0; xIndex < dx; xIndex++) { + int blockX = cubicchunks_getBlockCoord(xIndex, dx, px, minX); + + chunks[idxYZ + xIndex].setPosition(blockX, blockY, blockZ); } } } - }); + } + cbi.cancel(); } + @Unique + private static int cubicchunks_getBlockCoord(int index, int d, int p, int min) { + int coord = p + index; + if (coord < min) { + coord += d; + } + return coord << 4; + } + @Inject(method = "getRenderChunk", at = @At(value = "HEAD"), cancellable = true, require = 1) private void getRenderChunkInject(BlockPos pos, CallbackInfoReturnable cbi) { if (!((ICubicWorld) world).isCubicWorld()) { From 5e56b613e2912cba5f3569978f1c3609b84c2a60 Mon Sep 17 00:00:00 2001 From: DaPorkchop_ Date: Tue, 11 Mar 2025 16:52:00 +0100 Subject: [PATCH 2/2] Update GitHub PR workflow step versions --- .github/workflows/build_pr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_pr.yml b/.github/workflows/build_pr.yml index 808fc3418..72f1831f2 100644 --- a/.github/workflows/build_pr.yml +++ b/.github/workflows/build_pr.yml @@ -6,8 +6,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions/cache@v2 + - uses: actions/checkout@v4 + - uses: actions/cache@v4 with: path: | ~/.gradle/caches