mojira.dev
MC-306157

Getting the average water color of blocks takes up about half of the chunk render compile times

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

Thank you for helping us improve Minecraft! We saved your files:

[media]

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

BiomeColors.getAverageWaterColor

81.74% (98.88%)

9.99% (98.88%)

211,028

211,212

1

Vanilla

LiquidBlockRenderer.tesselate

82.66% (99.88%)

10.11% (99.88%)

213,402

213,600

2

Vanilla

BiomeColors.getAverageWaterColor

81.76% (98.84%)

13.03% (98.83%)

204,557

240,717

2

Vanilla

LiquidBlockRenderer.tesselate

82.71% (99.88%)

13.18% (99.88%)

243,357

243,565

3

Vanilla

BiomeColors.getAverageWaterColor

82.36% (98.99%)

14.66% (98.98%)

233,273

233,494

3

Vanilla

LiquidBlockRenderer.tesselate

83.19% (99.89%)

14.81% (99.89%)

235,635

235,886

1

Mixin

BiomeColors.getAverageWaterColor

15.34% (96.24%)

0.48% (96.10%)

8,297

8,297

1

Mixin

LiquidBlockRenderer.tesselate

15.94% (99.86%)

0.50% (99.86%)

8,621

8,633

2

Mixin

BiomeColors.getAverageWaterColor

15.55% (95.10%)

0.44% (95.10%)

8,833

8,833

2

Mixin

LiquidBlockRenderer.tesselate

16.36% (100.00%)

0.46% (100.00%)

9,288

9,288

3

Mixin

BiomeColors.getAverageWaterColor

13.68% (84.07%)

0.43% (83.63%)

7,229

7,241

3

Mixin

LiquidBlockRenderer.tesselate

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();
		}
	}
}

haubner

(Unassigned)

Plausible

Platform

Normal

Performance

26.1 Snapshot 5, 26.1 Snapshot 6

26.1 Snapshot 7

Retrieved