minor optimization to tint getting

This commit is contained in:
James Seibel
2025-10-28 07:35:26 -05:00
parent d61dcfaab6
commit 97130d1535
6 changed files with 164 additions and 84 deletions
@@ -18,9 +18,12 @@ import net.minecraft.world.level.biome.Biome;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
#if MC_VER >= MC_1_18_2
import net.minecraft.core.Holder;
#endif
@@ -30,26 +33,36 @@ public abstract class AbstractDhTintGetter implements BlockAndTintGetter
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
protected final BiomeWrapper biomeWrapper;
protected final int smoothingRadiusInBlocks;
protected final FullDataSourceV2 fullDataSource;
protected final IClientLevelWrapper clientLevelWrapper;
#if MC_VER < MC_1_18_2
public static final ConcurrentMap<String, Biome> BIOME_BY_RESOURCE_STRING = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, Biome> BIOME_BY_RESOURCE_STRING = new ConcurrentHashMap<>();
#else
public static final ConcurrentMap<String, Holder<Biome>> BIOME_BY_RESOURCE_STRING = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, Holder<Biome>> BIOME_BY_RESOURCE_STRING = new ConcurrentHashMap<>();
#endif
private static final ConcurrentHashMap<Biome, Integer> COLOR_BY_BIOME = new ConcurrentHashMap<>();
/** returned if the color cache is incomplete */
public static final int INVALID_COLOR = Integer.MIN_VALUE;
protected BiomeWrapper biomeWrapper;
protected FullDataSourceV2 fullDataSource;
protected int smoothingRadiusInBlocks;
protected IClientLevelWrapper clientLevelWrapper;
//=============//
// constructor //
//=============//
public AbstractDhTintGetter(BiomeWrapper biomeWrapper, FullDataSourceV2 fullDataSource, IClientLevelWrapper clientLevelWrapper)
public AbstractDhTintGetter() { }
/**
* Mutates this getter so we can access the necessary
* variables for tint getting.
*/
public void update(BiomeWrapper biomeWrapper, FullDataSourceV2 fullDataSource, IClientLevelWrapper clientLevelWrapper)
{
this.biomeWrapper = biomeWrapper;
this.fullDataSource = fullDataSource;
@@ -63,8 +76,27 @@ public abstract class AbstractDhTintGetter implements BlockAndTintGetter
// shared methods //
//================//
/** Called by MC's tint getter */
@Override
public int getBlockTint(BlockPos blockPos, ColorResolver colorResolver)
public int getBlockTint(@NotNull BlockPos blockPos, @NotNull ColorResolver colorResolver)
{
DhBlockPosMutable mutableBlockPos = new DhBlockPosMutable(blockPos.getX(), blockPos.getY(), blockPos.getZ());
return this.tryGetBlockTint(mutableBlockPos, colorResolver);
}
/**
* Can be called by DH directly, skipping some of MC's logic
* to speed up tint getting slightly.
*
* @return {@link AbstractDhTintGetter#INVALID_COLOR} if any of the biomes needed for this position
* were not cached. In that case calling {@link AbstractDhTintGetter#getBlockTint(BlockPos, ColorResolver)}
* will need to be called by MC's ColorResolver so we can
* populate the color cache.
*/
public int tryGetBlockTint(DhBlockPosMutable mutableBlockPos)
{ return this.tryGetBlockTint(mutableBlockPos, null); }
private int tryGetBlockTint(DhBlockPosMutable mutableBlockPos, @Nullable ColorResolver colorResolver)
{
// determine how wide this data source is so we can determine
// if blending should be used
@@ -78,7 +110,7 @@ public abstract class AbstractDhTintGetter implements BlockAndTintGetter
if (this.smoothingRadiusInBlocks == 0
|| dataSourceLodWidthInBlocks > this.smoothingRadiusInBlocks)
{
return colorResolver.getColor(unwrapClientBiome(this.biomeWrapper.biome, this.clientLevelWrapper), blockPos.getX(), blockPos.getZ());
return this.tryGetClientBiomeColor(colorResolver, this.biomeWrapper);
}
@@ -88,13 +120,13 @@ public abstract class AbstractDhTintGetter implements BlockAndTintGetter
int rollingGreen = 0;
int rollingBlue = 0;
int xMin = blockPos.getX() - this.smoothingRadiusInBlocks;
int xMax = blockPos.getX() + this.smoothingRadiusInBlocks;
int xMin = mutableBlockPos.getX() - this.smoothingRadiusInBlocks;
int xMax = mutableBlockPos.getX() + this.smoothingRadiusInBlocks;
int zMin = mutableBlockPos.getZ() - this.smoothingRadiusInBlocks;
int zMax = mutableBlockPos.getZ() + this.smoothingRadiusInBlocks;
int zMin = blockPos.getZ() - this.smoothingRadiusInBlocks;
int zMax = blockPos.getZ() + this.smoothingRadiusInBlocks;
DhBlockPosMutable mutableBlockPos = new DhBlockPosMutable(0, blockPos.getY(), 0);
for (int x = xMin; x < xMax; x++)
{
for (int z = zMin; z < zMax; z++)
@@ -114,7 +146,11 @@ public abstract class AbstractDhTintGetter implements BlockAndTintGetter
// get the color for this nearby position
int id = FullDataPointUtil.getId(dataPoint);
BiomeWrapper biomeWrapper = (BiomeWrapper) this.fullDataSource.mapping.getBiomeWrapper(id);
int color = colorResolver.getColor(unwrapClientBiome(biomeWrapper.biome, this.clientLevelWrapper), mutableBlockPos.getX(), mutableBlockPos.getZ());
int color = this.tryGetClientBiomeColor(colorResolver, biomeWrapper);
if (color == INVALID_COLOR)
{
return INVALID_COLOR;
}
// rolling average
@@ -131,7 +167,7 @@ public abstract class AbstractDhTintGetter implements BlockAndTintGetter
// just use the default center's color
if (dataPointCount == 0)
{
return colorResolver.getColor(unwrapClientBiome(this.biomeWrapper.biome, this.clientLevelWrapper), blockPos.getX(), blockPos.getZ());
return this.tryGetClientBiomeColor(colorResolver, this.biomeWrapper);
}
int colorInt = ColorUtil.argbToInt(
@@ -142,14 +178,38 @@ public abstract class AbstractDhTintGetter implements BlockAndTintGetter
return colorInt;
}
protected static Biome unwrapClientBiome(#if MC_VER >= MC_1_18_2 Holder<Biome> #else Biome #endif biome, IClientLevelWrapper clientLevelWrapper)
/**
* If given a ColorResolver this will always succeed. <Br>
* If not it will attempt to use the cached color.
*/
private int tryGetClientBiomeColor(@Nullable ColorResolver colorResolver, BiomeWrapper biomeWrapper)
{
BiomeWrapper biomeWrapper = (BiomeWrapper)BiomeWrapper.getBiomeWrapper(biome, clientLevelWrapper);
// use the cached color if possible
int cachedColor = COLOR_BY_BIOME.getOrDefault(unwrapClientBiome(biomeWrapper), INVALID_COLOR);
if (cachedColor != INVALID_COLOR)
{
return cachedColor;
}
if (colorResolver == null)
{
// no color resolver is present,
// the cache needs to be populated before
// we can use the fast path
return INVALID_COLOR;
}
return COLOR_BY_BIOME.computeIfAbsent(unwrapClientBiome(biomeWrapper),
// in James' testing the block position isn't needed so we can just default to (0,0)
(unwrappedBiome) -> colorResolver.getColor(unwrappedBiome, 0, 0));
}
protected static Biome unwrapClientBiome(BiomeWrapper biomeWrapper)
{
String biomeString = biomeWrapper.getSerialString();
if (biomeString == null
|| biomeString.isEmpty()
|| biomeString.equals(BiomeWrapper.EMPTY_BIOME_STRING))
|| biomeString.isEmpty()
|| biomeString.equals(BiomeWrapper.EMPTY_BIOME_STRING))
{
// default to "plains" for empty/invalid biomes
biomeString = "minecraft:plains";
@@ -203,6 +263,21 @@ public abstract class AbstractDhTintGetter implements BlockAndTintGetter
*/
private static #if MC_VER < MC_1_18_2 Biome #else Holder<Biome> #endif getClientBiome(String biomeResourceString)
{
#if MC_VER < MC_1_18_2
Biome biome;
#else
Holder<Biome> biome;
#endif
// calling get instead of compute is slightly faster for already
// computed values
biome = BIOME_BY_RESOURCE_STRING.get(biomeResourceString);
if (biome != null)
{
return biome;
}
// cache the client biomes so we don't have to re-parse the resource location every time
return BIOME_BY_RESOURCE_STRING.compute(biomeResourceString,
(resourceString, existingBiome) ->
@@ -23,13 +23,13 @@ import com.seibel.distanthorizons.common.wrappers.McObjectConverter;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable;
import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.core.Direction;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.FlowerBlock;
import net.minecraft.world.level.block.LeavesBlock;
@@ -64,8 +64,6 @@ public class ClientBlockStateColorCache
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
// TODO it isn't that we need the level, but that we need the adjacent data
// maybe we can pass in the full data source?
private static final HashSet<BlockState> BLOCK_STATES_THAT_NEED_LEVEL = new HashSet<>();
private static final HashSet<BlockState> BROKEN_BLOCK_STATES = new HashSet<>();
@@ -95,14 +93,10 @@ public class ClientBlockStateColorCache
#endif
private final IClientLevelWrapper clientLevelWrapper;
private final BlockStateWrapper blockStateWrapper;
private final BlockState blockState;
private final LevelReader level;
private boolean isColorResolved = false;
private int baseColor = 0;
private boolean needShade = true;
private boolean needPostTinting = false;
private int tintIndex = 0;
@@ -170,6 +164,9 @@ public class ClientBlockStateColorCache
0.93011117f, 0.9386859f, 0.9473069f, 0.9559735f, 0.9646866f, 0.9734455f, 0.98225087f, 0.9911022f, 1.0f
};
private static final ThreadLocal<TintWithoutLevelOverrider> TintWithoutLevelOverrideGetter = ThreadLocal.withInitial(() -> new TintWithoutLevelOverrider());
private static final ThreadLocal<TintGetterOverride> TintOverrideGetter = ThreadLocal.withInitial(() -> new TintGetterOverride());
//=============//
@@ -180,8 +177,6 @@ public class ClientBlockStateColorCache
{
this.blockState = blockState;
this.clientLevelWrapper = samplingLevel;
this.level = (LevelReader) samplingLevel.getWrappedMcObject();
this.blockStateWrapper = BlockStateWrapper.fromBlockState(this.blockState, this.clientLevelWrapper);
this.resolveColors();
}
@@ -235,32 +230,29 @@ public class ClientBlockStateColorCache
this.needPostTinting = firstQuad.isTinted();
#if MC_VER <= MC_1_21_4
this.needShade = firstQuad.isShade();
this.tintIndex = firstQuad.getTintIndex();
#else
this.needShade = firstQuad.shade();
this.tintIndex = firstQuad.tintIndex();
#endif
#if MC_VER < MC_1_17_1
this.baseColor = calculateColorFromTexture(
firstQuad.sprite,
ColorMode.getColorMode(this.blockState.getBlock()));
EColorMode.getColorMode(this.blockState.getBlock()));
#elif MC_VER < MC_1_21_5
this.baseColor = calculateColorFromTexture(
firstQuad.getSprite(),
ColorMode.getColorMode(this.blockState.getBlock()));
EColorMode.getColorMode(this.blockState.getBlock()));
#else
this.baseColor = calculateColorFromTexture(
firstQuad.sprite(),
ColorMode.getColorMode(this.blockState.getBlock()));
EColorMode.getColorMode(this.blockState.getBlock()));
#endif
}
else
{
// Backup method.
this.needPostTinting = false;
this.needShade = false;
this.tintIndex = 0;
this.baseColor = this.getParticleIconColor();
}
@@ -269,19 +261,11 @@ public class ClientBlockStateColorCache
{
// Liquid Block
this.needPostTinting = true;
this.needShade = false;
this.tintIndex = 0;
this.baseColor = this.getParticleIconColor();
}
//String serialString = this.blockStateWrapper.getSerialString();
//if (serialString.contains("minecraft:water")
// || serialString.contains("grass"))
//{
// BLOCK_STATES_THAT_NEED_LEVEL.add(this.blockState);
//}
this.isColorResolved = true;
}
finally
@@ -319,7 +303,7 @@ public class ClientBlockStateColorCache
}
//TODO: Perhaps make this not just use the first frame?
private static int calculateColorFromTexture(TextureAtlasSprite texture, ColorMode colorMode)
private static int calculateColorFromTexture(TextureAtlasSprite texture, EColorMode colorMode)
{
int count = 0;
int alpha = 0;
@@ -329,8 +313,8 @@ public class ClientBlockStateColorCache
int tempColor;
// don't render Chiseled blocks.
// Since ColorMode is set per block, you only need to check this once.
if (colorMode != ColorMode.Chisel)
// Since EColorMode is set per block, you only need to check this once.
if (colorMode != EColorMode.Chisel)
{
// textures normally use u and v instead of x and y
for (int v = 0; v < getTextureHeight(texture); v++)
@@ -348,7 +332,7 @@ public class ClientBlockStateColorCache
int b = (tempColor & 0x00FF0000) >>> 16;
int a = (tempColor & 0xFF000000) >>> 24;
int scale = 1;
if (colorMode == ColorMode.Leaves)
if (colorMode == EColorMode.Leaves)
{
//switch (//FIXME add config option)
// case BLACK:
@@ -367,11 +351,11 @@ public class ClientBlockStateColorCache
// break; //do nothing, let it count towards transparency
}
else if (a == 0 && colorMode != ColorMode.Glass)
else if (a == 0 && colorMode != EColorMode.Glass)
{
continue;
}
else if (colorMode == ColorMode.Flower && (g + 25 < b || g + 25 < r))
else if (colorMode == EColorMode.Flower && (g + 25 < b || g + 25 < r))
{
scale = FLOWER_COLOR_SCALE;
}
@@ -454,7 +438,7 @@ public class ClientBlockStateColorCache
{
return calculateColorFromTexture(
Minecraft.getInstance().getModelManager().getBlockModelShaper().getParticleIcon(this.blockState),
ColorMode.getColorMode(this.blockState.getBlock()));
EColorMode.getColorMode(this.blockState.getBlock()));
}
@@ -463,7 +447,7 @@ public class ClientBlockStateColorCache
// public getter //
//===============//
public int getColor(BiomeWrapper biomeWrapper, FullDataSourceV2 fullDataSource, DhBlockPos pos)
public int getColor(BiomeWrapper biomeWrapper, FullDataSourceV2 fullDataSource, DhBlockPos blockPos)
{
// only get the tint if the block needs to be tinted
if (!this.needPostTinting)
@@ -487,16 +471,27 @@ public class ClientBlockStateColorCache
{
try
{
tintColor = Minecraft.getInstance().getBlockColors()
.getColor(this.blockState,
new TintWithoutLevelOverrider(biomeWrapper, fullDataSource, this.clientLevelWrapper), // TODO can this object be cached?
McObjectConverter.Convert(pos),
this.tintIndex);
TintWithoutLevelOverrider tintOverride = TintWithoutLevelOverrideGetter.get();
tintOverride.update(biomeWrapper, fullDataSource, this.clientLevelWrapper);
// try using DH's cached tint values first if possible
tintColor = tintOverride.tryGetBlockTint(new DhBlockPosMutable(blockPos));
if (tintColor == AbstractDhTintGetter.INVALID_COLOR)
{
// one or more tint values weren't calculated,
// we need MC's color resolver
tintColor = Minecraft.getInstance()
.getBlockColors()
.getColor(this.blockState,
tintOverride,
McObjectConverter.Convert(blockPos),
this.tintIndex);
}
}
catch (UnsupportedOperationException e)
{
// this exception generally occurs if the tint requires other blocks besides itself
LOGGER.debug("Unable to use ["+ TintWithoutLevelOverrider.class.getSimpleName()+"] to get the block tint for block: [" + this.blockState + "] and biome: [" + biomeWrapper + "] at pos: " + pos + ". Error: [" + e.getMessage() + "]. Attempting to use backup method...", e);
LOGGER.debug("Unable to use ["+ TintWithoutLevelOverrider.class.getSimpleName()+"] to get the block tint for block: [" + this.blockState + "] and biome: [" + biomeWrapper + "] at pos: " + blockPos + ". Error: [" + e.getMessage() + "]. Attempting to use backup method...", e);
BLOCK_STATES_THAT_NEED_LEVEL.add(this.blockState);
}
}
@@ -504,13 +499,22 @@ public class ClientBlockStateColorCache
// use the level logic only if requested
if (BLOCK_STATES_THAT_NEED_LEVEL.contains(this.blockState))
{
// this logic can't be used all the time due to it breaking some blocks tinting
// the level shouldn't be used all the time due to it breaking some blocks tinting
// specifically oceans don't render correctly
tintColor = Minecraft.getInstance().getBlockColors()
.getColor(this.blockState,
new TintGetterOverride(this.level, biomeWrapper, fullDataSource, this.clientLevelWrapper), // TODO can this object be cached?
McObjectConverter.Convert(pos),
this.tintIndex);
TintGetterOverride tintOverride = TintOverrideGetter.get();
tintOverride.update(biomeWrapper, fullDataSource, this.clientLevelWrapper);
tintColor = tintOverride.tryGetBlockTint(new DhBlockPosMutable(blockPos));
if (tintColor == AbstractDhTintGetter.INVALID_COLOR)
{
tintColor = Minecraft.getInstance()
.getBlockColors()
.getColor(this.blockState,
tintOverride,
McObjectConverter.Convert(blockPos),
this.tintIndex);
}
}
}
catch (Exception e)
@@ -518,7 +522,7 @@ public class ClientBlockStateColorCache
// only display the error once per block/biome type to reduce log spam
if (!BROKEN_BLOCK_STATES.contains(this.blockState))
{
LOGGER.warn("Failed to get block color for block: [" + this.blockState + "] and biome: [" + biomeWrapper + "] at pos: " + pos + ". Error: ["+e.getMessage() + "]. Note: future errors for this block/biome will be ignored.", e);
LOGGER.warn("Failed to get block color for block: [" + this.blockState + "] and biome: [" + biomeWrapper + "] at pos: " + blockPos + ". Error: ["+e.getMessage() + "]. Note: future errors for this block/biome will be ignored.", e);
BROKEN_BLOCK_STATES.add(this.blockState);
}
}
@@ -542,7 +546,7 @@ public class ClientBlockStateColorCache
// helper classes //
//================//
enum ColorMode
private enum EColorMode
{
Default,
Flower,
@@ -550,7 +554,7 @@ public class ClientBlockStateColorCache
Chisel,
Glass;
static ColorMode getColorMode(Block block)
static EColorMode getColorMode(Block block)
{
if (block instanceof LeavesBlock) return Leaves;
if (block instanceof FlowerBlock) return Flower;
@@ -41,7 +41,7 @@ import java.util.stream.Stream;
public class TintGetterOverride extends AbstractDhTintGetter
{
private final LevelReader parent;
private LevelReader parent;
@@ -49,9 +49,11 @@ public class TintGetterOverride extends AbstractDhTintGetter
// constructor //
//=============//
public TintGetterOverride(LevelReader parent, BiomeWrapper biomeWrapper, FullDataSourceV2 fullDataSource, IClientLevelWrapper clientLevelWrapper)
{
super(biomeWrapper, fullDataSource, clientLevelWrapper);
public TintGetterOverride() { }
public void update(LevelReader parent, BiomeWrapper biomeWrapper, FullDataSourceV2 fullDataSource, IClientLevelWrapper clientLevelWrapper)
{
super.update(biomeWrapper, fullDataSource, clientLevelWrapper);
this.parent = parent;
}
@@ -23,6 +23,7 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSour
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.lighting.LevelLightEngine;
@@ -36,8 +37,8 @@ public class TintWithoutLevelOverrider extends AbstractDhTintGetter
// constructor //
//=============//
public TintWithoutLevelOverrider(BiomeWrapper biomeWrapper, FullDataSourceV2 fullDataSource, IClientLevelWrapper clientLevelWrapper)
{ super(biomeWrapper, fullDataSource, clientLevelWrapper); }
public TintWithoutLevelOverrider()
{ }
@@ -64,10 +64,10 @@ public class ClientLevelWrapper implements IClientLevelWrapper
private static final Minecraft MINECRAFT = Minecraft.getInstance();
private final ClientLevel level;
private final ConcurrentHashMap<BlockState, ClientBlockStateColorCache> blockCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<BlockState, ClientBlockStateColorCache> blockColorCacheByBlockState = new ConcurrentHashMap<>();
/** cached method reference to reduce GC overhead */
private final Function<BlockState, ClientBlockStateColorCache> cachedBlockColorCacheFunction = (blockState) -> this.createBlockColorCache(blockState);
private final Function<BlockState, ClientBlockStateColorCache> createCachedBlockColorCacheFunc = (blockState) -> new ClientBlockStateColorCache(blockState, this);
private BlockStateWrapper dirtBlockWrapper;
@@ -201,14 +201,12 @@ public class ClientLevelWrapper implements IClientLevelWrapper
@Override
public int getBlockColor(DhBlockPos pos, IBiomeWrapper biome, FullDataSourceV2 fullDataSource, IBlockStateWrapper blockWrapper)
{
ClientBlockStateColorCache blockColorCache = this.blockCache.computeIfAbsent(
ClientBlockStateColorCache blockColorCache = this.blockColorCacheByBlockState.computeIfAbsent(
((BlockStateWrapper) blockWrapper).blockState,
this.cachedBlockColorCacheFunction);
this.createCachedBlockColorCacheFunc);
return blockColorCache.getColor((BiomeWrapper) biome, fullDataSource, pos);
}
/** used by {@link ClientLevelWrapper#cachedBlockColorCacheFunction} */
private ClientBlockStateColorCache createBlockColorCache(BlockState block) { return new ClientBlockStateColorCache(block, this); }
@Override
@@ -232,7 +230,7 @@ public class ClientLevelWrapper implements IClientLevelWrapper
}
@Override
public void clearBlockColorCache() { this.blockCache.clear(); }
public void clearBlockColorCache() { this.blockColorCacheByBlockState.clear(); }
@Override
public IDimensionTypeWrapper getDimensionType() { return DimensionTypeWrapper.getDimensionTypeWrapper(this.level.dimensionType()); }