BiomeColors.getAverageWaterColor takes up roughly half of the chunk compile times (which can cause stutter in sync situations) due to it sampling the biome color for every water block (not just the surfaces). This is especially noticable with high Biome Blend values. Moving BiomeColors.getAverageWaterColor into the if statement (whenever an actual surface is created) reduces the load on the CPU by a lot.
Class in question: net.minecraft.client.renderer.block.LiquidBlockRenderer
public void tesselate(
final BlockAndTintGetter level, final BlockPos pos, final VertexConsumer builder, final BlockState blockState, final FluidState fluidState
) {
boolean isLava = fluidState.is(FluidTags.LAVA);
TextureAtlasSprite stillSprite = isLava ? this.lavaStill : this.waterStill;
TextureAtlasSprite flowingSprite = isLava ? this.lavaFlowing : this.waterFlowing;
int col = isLava ? 16777215 : BiomeColors.getAverageWaterColor(level, pos);
float r = (col >> 16 & 0xFF) / 255.0F;
float g = (col >> 8 & 0xFF) / 255.0F;
float b = (col & 0xFF) / 255.0F;
BlockState blockStateDown = level.getBlockState(pos.relative(Direction.DOWN));
FluidState fluidStateDown = blockStateDown.getFluidState();
BlockState blockStateUp = level.getBlockState(pos.relative(Direction.UP));
FluidState fluidStateUp = blockStateUp.getFluidState();
BlockState blockStateNorth = level.getBlockState(pos.relative(Direction.NORTH));
FluidState fluidStateNorth = blockStateNorth.getFluidState();
BlockState blockStateSouth = level.getBlockState(pos.relative(Direction.SOUTH));
FluidState fluidStateSouth = blockStateSouth.getFluidState();
BlockState blockStateWest = level.getBlockState(pos.relative(Direction.WEST));
FluidState fluidStateWest = blockStateWest.getFluidState();
BlockState blockStateEast = level.getBlockState(pos.relative(Direction.EAST));
FluidState fluidStateEast = blockStateEast.getFluidState();
boolean renderUp = !isNeighborSameFluid(fluidState, fluidStateUp);
boolean renderDown = shouldRenderFace(fluidState, blockState, Direction.DOWN, fluidStateDown)
&& !isFaceOccludedByNeighbor(Direction.DOWN, 0.8888889F, blockStateDown);
boolean renderNorth = shouldRenderFace(fluidState, blockState, Direction.NORTH, fluidStateNorth);
boolean renderSouth = shouldRenderFace(fluidState, blockState, Direction.SOUTH, fluidStateSouth);
boolean renderWest = shouldRenderFace(fluidState, blockState, Direction.WEST, fluidStateWest);
boolean renderEast = shouldRenderFace(fluidState, blockState, Direction.EAST, fluidStateEast);
if (renderUp || renderDown || renderEast || renderWest || renderNorth || renderSouth) {
float c10 = level.getShade(Direction.DOWN, true);
float c11 = level.getShade(Direction.UP, true);
float c2 = level.getShade(Direction.NORTH, true);
float c3 = level.getShade(Direction.WEST, true);Attachments
Comments 2
Can confirm, tested with IntelliJ Profiler (CPU and Total time), 26.1 Snapshot 6
Setup:
FOV 90, unlimited max framerate, biome blend 15x15, render distance 16
Superflat world, water world preset, no strutures, peaceful
Load world, do not move mouse or player, wait 10 seconds for initial chunk load, press
F3 + A, wait additional 15 seconds (10+15 for 25 in total), then quit
Details:
I’ve tested this with a mixin addressing the issue. There are multiple ways to go about this issue, all I did was confirm that that moving the water color sampling behind the face render check removes most of the time spent in BiomeColors.getAverageWaterColor.
Profiling results:
Note: I can also attach every run's .jfr profiler results to view in IntelliJ if needed
Run | Mode | Method | CPU % (parent %) | Total % (parent %) | CPU ms | Total ms |
|---|---|---|---|---|---|---|
1 | Vanilla |
| 81.74% (98.88%) | 9.99% (98.88%) | 211,028 | 211,212 |
1 | Vanilla |
| 82.66% (99.88%) | 10.11% (99.88%) | 213,402 | 213,600 |
2 | Vanilla |
| 81.76% (98.84%) | 13.03% (98.83%) | 204,557 | 240,717 |
2 | Vanilla |
| 82.71% (99.88%) | 13.18% (99.88%) | 243,357 | 243,565 |
3 | Vanilla |
| 82.36% (98.99%) | 14.66% (98.98%) | 233,273 | 233,494 |
3 | Vanilla |
| 83.19% (99.89%) | 14.81% (99.89%) | 235,635 | 235,886 |
1 | Mixin |
| 15.34% (96.24%) | 0.48% (96.10%) | 8,297 | 8,297 |
1 | Mixin |
| 15.94% (99.86%) | 0.50% (99.86%) | 8,621 | 8,633 |
2 | Mixin |
| 15.55% (95.10%) | 0.44% (95.10%) | 8,833 | 8,833 |
2 | Mixin |
| 16.36% (100.00%) | 0.46% (100.00%) | 9,288 | 9,288 |
3 | Mixin |
| 13.68% (84.07%) | 0.43% (83.63%) | 7,229 | 7,241 |
3 | Mixin |
| 16.27% (99.58%) | 0.51% (99.58%) | 8,598 | 8,658 |
Summary:
This confirms that with a high biome blend, BiomeColors.getAverageWaterColor takes up most of a chunks render compile time because its called for every water block including fully occluded ones. Moving the sampling behind the face render check, or early returning when no faces render removes most of that time.
Mixin used:
package com.example.mixin.client;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.renderer.block.LiquidBlockRenderer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FluidState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(LiquidBlockRenderer.class)
public abstract class LiquidBlockRendererMixin {
@Shadow
private static boolean isNeighborSameFluid(FluidState fluidState, FluidState neighborFluidState) {
return false;
}
@Shadow
private static boolean isFaceOccludedByNeighbor(Direction direction, float height, BlockState neighborState) {
return false;
}
@Shadow
public static boolean shouldRenderFace(
FluidState fluidState, BlockState blockState, Direction direction, FluidState neighborFluidState
) {
return false;
}
@Inject(method = "tesselate", at = @At("HEAD"), cancellable = true)
private void templateMod$skipFullyOccludedFluids(
BlockAndTintGetter level,
BlockPos pos,
VertexConsumer builder,
BlockState blockState,
FluidState fluidState,
CallbackInfo info
) {
BlockState blockStateDown = level.getBlockState(pos.relative(Direction.DOWN));
FluidState fluidStateDown = blockStateDown.getFluidState();
BlockState blockStateUp = level.getBlockState(pos.relative(Direction.UP));
FluidState fluidStateUp = blockStateUp.getFluidState();
BlockState blockStateNorth = level.getBlockState(pos.relative(Direction.NORTH));
FluidState fluidStateNorth = blockStateNorth.getFluidState();
BlockState blockStateSouth = level.getBlockState(pos.relative(Direction.SOUTH));
FluidState fluidStateSouth = blockStateSouth.getFluidState();
BlockState blockStateWest = level.getBlockState(pos.relative(Direction.WEST));
FluidState fluidStateWest = blockStateWest.getFluidState();
BlockState blockStateEast = level.getBlockState(pos.relative(Direction.EAST));
FluidState fluidStateEast = blockStateEast.getFluidState();
boolean renderUp = !isNeighborSameFluid(fluidState, fluidStateUp);
boolean renderDown = shouldRenderFace(fluidState, blockState, Direction.DOWN, fluidStateDown)
&& !isFaceOccludedByNeighbor(Direction.DOWN, 0.8888889F, blockStateDown);
boolean renderNorth = shouldRenderFace(fluidState, blockState, Direction.NORTH, fluidStateNorth);
boolean renderSouth = shouldRenderFace(fluidState, blockState, Direction.SOUTH, fluidStateSouth);
boolean renderWest = shouldRenderFace(fluidState, blockState, Direction.WEST, fluidStateWest);
boolean renderEast = shouldRenderFace(fluidState, blockState, Direction.EAST, fluidStateEast);
if (!(renderUp || renderDown || renderEast || renderWest || renderNorth || renderSouth)) {
info.cancel();
}
}
}
Thank you for helping us improve Minecraft! We saved your files: