Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Fixed
- **Merged faces rendering as one flat colour with ColorLight (GPU pipeline)**: ColorLight redirects
Sodium's terrain pipelines to its own copy of the shader (`colorlight:blocks/colored_terrain`), so
the greedy UV patch never reached it. The patch now also applies to that shader. Two related
problems in the injected code are fixed with it: the atlas sampler is now picked by name
(`u_BlockTex`) instead of "first `sampler2D`", which in ColorLight's copy is the 16x16 lightmap,
and the face marker carried in vertex alpha is cleared from ColorLight's `v_ColorBase` too.
- **ColorLight in vertex-colour mode** (pipeline switched off, unsupported Sodium build, or an Iris
pack active): merged quads bypass the model where ColorLight bakes its colour, so they came out
uncoloured. Blocks next to coloured light now stay on the per-block path in that mode; light-free
areas still merge. No effect while the GPU pipeline is active or ColorLight is absent.

### Performance (Sodium path)
- The per-thread work state is kept between section builds instead of being removed after each one,
which reallocated about 200 KiB of arrays per section.
- Face visibility, AO merge keys and per-corner smooth lighting read neighbouring blocks, light and
occlusion through a lazily filled 18x18x18 per-section cache instead of the world slice.
- Face layers (model walk, sprite lookup) are cached per block state and face until resources reload.
- Per-block eligibility checks are memoised for runs of the same block state.
- The merge sweep groups faces with a small hash table instead of scanning every group per cell.

## [0.6.1]

### Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package hi.sierra.greedy_meshing.client;

import net.fabricmc.loader.api.FabricLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

/**
* Soft compatibility with ColorLight (reflection only, no compile-time dependency).
*
* <p>ColorLight has two ways of colouring light:</p>
* <ul>
* <li><b>GPU pipeline</b>: lit per pixel in a shader from a world-space volume. Meshes are
* uncoloured, so merged quads are fine as long as the greedy UV patch is also applied to
* ColorLight's copy of the terrain shader (see {@code SodiumShaderLoaderMixin}).</li>
* <li><b>Vertex colours</b> (fallback when the pipeline is off, unsupported, or an Iris shader
* pack is active): ColorLight bakes the colour into each block's model quads while Sodium
* meshes them. Greedy quads never go through the model, so they would come out uncoloured.
* In that mode blocks that sit next to coloured light are left to the normal per-block path;
* everything in light-free areas still merges.</li>
* </ul>
*/
public final class GreedyColorLightCompat {
private static final Logger LOGGER = LoggerFactory.getLogger("greedy_meshing");
private static final boolean LOADED = FabricLoader.getInstance().isModLoaded("colorlight");

private static volatile boolean resolved;
private static volatile boolean broken;
private static MethodHandle gpuActive; // () -> boolean
private static MethodHandle engineGet; // () -> ColorLightEngine (may be null)
private static MethodHandle lightFreeAround; // (ColorLightEngine, int, int, int) -> boolean

private GreedyColorLightCompat() {
}

/**
* True when this block must keep going through the model path because ColorLight has to bake
* coloured light into its vertices and there is coloured light around it.
*/
public static boolean mustStayPerBlock(int worldX, int worldY, int worldZ) {
if (!LOADED || broken) {
return false;
}
if (!resolved) {
resolve();
if (broken) {
return false;
}
}
try {
if ((boolean) gpuActive.invokeExact()) {
return false;
}
Object engine = engineGet.invoke();
if (engine == null) {
return false;
}
return !(boolean) lightFreeAround.invoke(engine, worldX, worldY, worldZ);
} catch (Throwable t) {
markBroken(t);
return false;
}
}

private static synchronized void resolve() {
if (resolved) {
return;
}
try {
ClassLoader loader = GreedyColorLightCompat.class.getClassLoader();
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
Class<?> gpu = Class.forName("me.mrhikmen.colorlight.client.core.render.gpu.ColorLightGpu", true, loader);
Class<?> holder = Class.forName("me.mrhikmen.colorlight.client.core.light.engine.ColorLightEngineHolder", true, loader);
Class<?> engine = Class.forName("me.mrhikmen.colorlight.client.core.light.engine.ColorLightEngine", true, loader);
gpuActive = lookup.findStatic(gpu, "isActive", MethodType.methodType(boolean.class));
engineGet = lookup.findStatic(holder, "get", MethodType.methodType(engine));
lightFreeAround = lookup.findVirtual(engine, "isLightFreeAround",
MethodType.methodType(boolean.class, int.class, int.class, int.class))
.asType(MethodType.methodType(boolean.class, Object.class, int.class, int.class, int.class));
resolved = true;
} catch (Throwable t) {
markBroken(t);
}
}

private static void markBroken(Throwable t) {
if (!broken) {
broken = true;
resolved = true;
LOGGER.warn("Greedy Meshing: ColorLight detected but its API could not be read ({}); "
+ "vertex-colour mode will not keep lit blocks per-block.", t.toString());
}
}
}
95 changes: 64 additions & 31 deletions src/client/java/hi/sierra/greedy_meshing/client/GreedyLighting.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,41 +107,42 @@ public static void computeTileLighting(

BlockPos.MutableBlockPos samplePos = scratch.mutablePos;

samplePos.set(c0x, c0y, c0z);
BlockState state0 = world.getBlockState(samplePos);
int i = greedyMeshing$getLightColor(world, state0, samplePos);
BlockState state0 = sampleState(world, scratch, samplePos, c0x, c0y, c0z);
int i = sampleLight(world, scratch, samplePos, state0, c0x, c0y, c0z);
float f = state0.getShadeBrightness(world, samplePos);

samplePos.set(c1x, c1y, c1z);
BlockState state1 = world.getBlockState(samplePos);
int j = greedyMeshing$getLightColor(world, state1, samplePos);
BlockState state1 = sampleState(world, scratch, samplePos, c1x, c1y, c1z);
int j = sampleLight(world, scratch, samplePos, state1, c1x, c1y, c1z);
float g = state1.getShadeBrightness(world, samplePos);

samplePos.set(c2x, c2y, c2z);
BlockState state2 = world.getBlockState(samplePos);
int k = greedyMeshing$getLightColor(world, state2, samplePos);
BlockState state2 = sampleState(world, scratch, samplePos, c2x, c2y, c2z);
int k = sampleLight(world, scratch, samplePos, state2, c2x, c2y, c2z);
float h = state2.getShadeBrightness(world, samplePos);

samplePos.set(c3x, c3y, c3z);
BlockState state3 = world.getBlockState(samplePos);
int l = greedyMeshing$getLightColor(world, state3, samplePos);
BlockState state3 = sampleState(world, scratch, samplePos, c3x, c3y, c3z);
int l = sampleLight(world, scratch, samplePos, state3, c3x, c3y, c3z);
float m = state3.getShadeBrightness(world, samplePos);

boolean open0 = isOpen(world, samplePos, c0x + face.getStepX(), c0y + face.getStepY(), c0z + face.getStepZ());
boolean open1 = isOpen(world, samplePos, c1x + face.getStepX(), c1y + face.getStepY(), c1z + face.getStepZ());
boolean open2 = isOpen(world, samplePos, c2x + face.getStepX(), c2y + face.getStepY(), c2z + face.getStepZ());
boolean open3 = isOpen(world, samplePos, c3x + face.getStepX(), c3y + face.getStepY(), c3z + face.getStepZ());
int fsx = face.getStepX();
int fsy = face.getStepY();
int fsz = face.getStepZ();
boolean open0 = isOpenAt(world, scratch, samplePos, c0x + fsx, c0y + fsy, c0z + fsz);
boolean open1 = isOpenAt(world, scratch, samplePos, c1x + fsx, c1y + fsy, c1z + fsz);
boolean open2 = isOpenAt(world, scratch, samplePos, c2x + fsx, c2y + fsy, c2z + fsz);
boolean open3 = isOpenAt(world, scratch, samplePos, c3x + fsx, c3y + fsy, c3z + fsz);

float n;
int o;
if (!open2 && !open0) {
n = f;
o = i;
} else {
samplePos.set(c0x + corner2.getStepX(), c0y + corner2.getStepY(), c0z + corner2.getStepZ());
BlockState diagonalState = world.getBlockState(samplePos);
int dx = c0x + corner2.getStepX();
int dy = c0y + corner2.getStepY();
int dz = c0z + corner2.getStepZ();
BlockState diagonalState = sampleState(world, scratch, samplePos, dx, dy, dz);
n = diagonalState.getShadeBrightness(world, samplePos);
o = greedyMeshing$getLightColor(world, diagonalState, samplePos);
o = sampleLight(world, scratch, samplePos, diagonalState, dx, dy, dz);
}

float p;
Expand All @@ -150,10 +151,12 @@ public static void computeTileLighting(
p = f;
q = i;
} else {
samplePos.set(c0x + corner3.getStepX(), c0y + corner3.getStepY(), c0z + corner3.getStepZ());
BlockState diagonalState = world.getBlockState(samplePos);
int dx = c0x + corner3.getStepX();
int dy = c0y + corner3.getStepY();
int dz = c0z + corner3.getStepZ();
BlockState diagonalState = sampleState(world, scratch, samplePos, dx, dy, dz);
p = diagonalState.getShadeBrightness(world, samplePos);
q = greedyMeshing$getLightColor(world, diagonalState, samplePos);
q = sampleLight(world, scratch, samplePos, diagonalState, dx, dy, dz);
}

float r;
Expand All @@ -162,10 +165,12 @@ public static void computeTileLighting(
r = f;
s = i;
} else {
samplePos.set(c1x + corner2.getStepX(), c1y + corner2.getStepY(), c1z + corner2.getStepZ());
BlockState diagonalState = world.getBlockState(samplePos);
int dx = c1x + corner2.getStepX();
int dy = c1y + corner2.getStepY();
int dz = c1z + corner2.getStepZ();
BlockState diagonalState = sampleState(world, scratch, samplePos, dx, dy, dz);
r = diagonalState.getShadeBrightness(world, samplePos);
s = greedyMeshing$getLightColor(world, diagonalState, samplePos);
s = sampleLight(world, scratch, samplePos, diagonalState, dx, dy, dz);
}

float t;
Expand All @@ -174,15 +179,16 @@ public static void computeTileLighting(
t = f;
u = i;
} else {
samplePos.set(c1x + corner3.getStepX(), c1y + corner3.getStepY(), c1z + corner3.getStepZ());
BlockState diagonalState = world.getBlockState(samplePos);
int dx = c1x + corner3.getStepX();
int dy = c1y + corner3.getStepY();
int dz = c1z + corner3.getStepZ();
BlockState diagonalState = sampleState(world, scratch, samplePos, dx, dy, dz);
t = diagonalState.getShadeBrightness(world, samplePos);
u = greedyMeshing$getLightColor(world, diagonalState, samplePos);
u = sampleLight(world, scratch, samplePos, diagonalState, dx, dy, dz);
}

samplePos.set(faceX, faceY, faceZ);
BlockState faceState = world.getBlockState(samplePos);
int v = greedyMeshing$getLightColor(world, faceState, samplePos);
BlockState faceState = sampleState(world, scratch, samplePos, faceX, faceY, faceZ);
int v = sampleLight(world, scratch, samplePos, faceState, faceX, faceY, faceZ);
float w = faceState.getShadeBrightness(world, samplePos);

float ao0 = (m + f + p + w) * 0.25F;
Expand Down Expand Up @@ -352,6 +358,31 @@ private static boolean isOpen(BlockAndTintGetter world, BlockPos.MutableBlockPos
//?}
}

/** Positions the shared scratch pos at (x, y, z) and returns the state there, via the section cache when set. */
private static BlockState sampleState(BlockAndTintGetter world, Scratch scratch, BlockPos.MutableBlockPos pos,
int x, int y, int z) {
pos.set(x, y, z);
GreedySectionCache cache = scratch.cache;
return cache != null ? cache.state(x, y, z) : world.getBlockState(pos);
}

private static int sampleLight(BlockAndTintGetter world, Scratch scratch, BlockPos.MutableBlockPos pos,
BlockState state, int x, int y, int z) {
GreedySectionCache cache = scratch.cache;
return cache != null ? cache.light(x, y, z) : greedyMeshing$getLightColor(world, state, pos);
}

private static boolean isOpenAt(BlockAndTintGetter world, Scratch scratch, BlockPos.MutableBlockPos pos,
int x, int y, int z) {
GreedySectionCache cache = scratch.cache;
return cache != null ? !cache.occludes(x, y, z) : isOpen(world, pos, x, y, z);
}

/** Packed light for an already-resolved state at {@code pos}. */
public static int lightAt(BlockAndTintGetter world, BlockState state, BlockPos pos) {
return greedyMeshing$getLightColor(world, state, pos);
}

/** Packed block/sky light at a face sample, using the version-correct light API. */
public static int mergeLightKey(BlockAndTintGetter world, BlockPos pos) {
return greedyMeshing$getLightColor(world, world.getBlockState(pos), pos);
Expand All @@ -377,5 +408,7 @@ public static final class Scratch {
// Set once per section build. False inside a Sable sub-level, whose own shader applies
// directional shading dynamically — baking ours in on top would double it (issue #4).
public boolean applyDirectionalShade = true;
// Optional per-section neighbour cache (Sodium path). Null means read straight from the world.
public GreedySectionCache cache;
}
}
Loading