Class LightSampleCache

java.lang.Object
com.hbm_m.client.render.LightSampleCache

public final class LightSampleCache extends Object
Per-frame cache for the smoothed lightmap UV pair sampled at the 6 face centres of a BlockEntity's render bounding box.

Why this exists. InstancedStaticPartRenderer.sampleSmoothLightUV is the most expensive call on the per-machine path under shaders - the profiler attributed roughly 17% of frame time to it on a small Advanced Assembler farm. The work itself (6 × Level.getBlockState + 6 × LevelRenderer.getLightColor) is fundamentally chunky, but it was being repeated per part renderer for the same BlockEntity:

  • Advanced Assembler has 11 parts (Base, Frame, 3× Ring, Arm[Bone1..6]). For each visible machine the BlockEntityRenderer dispatches to 11 different InstancedStaticPartRenderer.addInstance(...) calls, each of which used to sample the lightmap independently for the SAME world-space bbox.
  • Both the main pass and the shadow pass repeat the dispatch - so the 11× redundancy doubles to 22× per machine per frame.
  • For 6 visible machines that adds up to ~792 lightmap lookups per frame for a single multiblock type, plus the bbox call (which itself can be non-trivial for dynamic-bbox BEs).
Sharing a single (blockU, skyV) pair across all parts of one BE for the lifetime of one frame collapses that to ~6 lookups per machine per frame - an 11–22× reduction.

Invalidation strategy. The cache uses a render-frame counter that is bumped from ClientModEvents.onRenderLevelStage(AFTER_BLOCK_ENTITIES) - i.e. ONCE per fully-rendered frame, after both the shadow pass and the main pass have drained their block-entity dispatches. So a sample taken during the shadow pass is reused during the main pass of the same frame (correct - the world hasn't changed between them) and a fresh sample is taken on the next frame. There is no need for a stronger TTL because the lightmap only changes over multiple ticks anyway.

Periodic pruning every 600 frames (~10 seconds) drops entries belonging to BlockEntities that haven't been rendered recently - typical "I walked past a chunk and now it's behind me" case - keeping the cache bounded in long sessions.

Single-threaded: lives entirely on the render thread.

  • Field Details

    • BASE_POSE

      public static final ThreadLocal<org.joml.Matrix4f> BASE_POSE
    • BASE_POSE_SET

      public static final ThreadLocal<Boolean> BASE_POSE_SET
  • Method Details

    • onFrameStart

      public static void onFrameStart()
      Bumps the frame counter; subsequent getOrSample(net.minecraft.world.level.block.entity.BlockEntity, int, float[], int) calls will resample once per BlockEntity rather than reusing the previous frame's value. Periodically prunes stale entries.
    • getOrSample

      public static void getOrSample(@Nullable net.minecraft.world.level.block.entity.BlockEntity be, int packedLightFallback, float[] outUV, int outBase)
      Returns the smoothed lightmap UV pair for the supplied BlockEntity, sampling lazily on the first call within a frame and reusing the cached value for all subsequent calls within the same frame.

      Falls back to packedLightFallback when:

      • be is null or detached from a level;
      • the bounding box is non-finite (some BEs return AABB.INFINITE);
      • all 6 face positions are themselves opaque (machine fully buried).
      Parameters:
      be - the BlockEntity providing world + bbox
      packedLightFallback - vanilla packed light to use if sampling fails
      outUV - destination array, written as outUV[outBase] = blockU, outUV[outBase+1] = skyV on the same 0..240 scale that BufferBuilder.uv2 writes
      outBase - offset into outUV
    • invalidateAll

      public static void invalidateAll()
      Drops every cached entry. Call when the level changes (dimension switch, world unload) so we don't return a stale UV taken from a now-invalid Level reference. Cheap - the map is typically a few dozen entries.
    • getOrSample8

      public static void getOrSample8(@Nullable net.minecraft.world.level.block.entity.BlockEntity be, long partIdentityHash, float[] objBbox, net.minecraft.core.BlockPos blockPos, org.joml.Matrix4f localPose, int packedLightFallback, float[] out16)
      Samples lightmap UV pairs at the 8 world-space corners of the given object-space bbox. Writes into out16 as [c0.blockU, c0.skyV, c1.blockU, c1.skyV, ...].

      Corner index encoding: bit 0 = x, bit 1 = y, bit 2 = z; bit set means "take max side on that axis, else min side". This order must match the shader's w.x / w.y / w.z weight indexing in trilinearBright.

      Coordinate system. The 8 world block positions are resolved as blockPos + floor(localPose * objCorner), where:

      • blockPos is the BE's integer world position (exact);
      • localPose is the per-BE local transform (rotation, scale, and any small sub-block translation applied inside the BER) without the camera's view rotation and without the blockPos - cameraPos offset baked in by LevelRenderer.
      This avoids composing a full absolute world pose in Matrix4f, which would lose precision at large camera offsets (≥ ~10k blocks from origin) and cause the floored block index to flicker between adjacent blocks as the camera moves sub-block distances - producing the "models shimmer between bright and dark when you move near a torch" symptom. Float32 has ~7 decimal digits of precision, so (float)cameraPos + (float)(blockPos - cameraPos) at cameraPos ~ 10^6 rounds inconsistently across frames.

      Cache key combines BE.getBlockPos().asLong() with a partIdentityHash so separate parts of one BE don't collide.

      Parameters:
      be - owning BlockEntity (for world lookup + cache key)
      partIdentityHash - stable hash identifying which part of the BE this sample set belongs to (e.g. renderer identity)
      objBbox - {minX, minY, minZ, maxX, maxY, maxZ}
      blockPos - BE world position (int, exact)
      localPose - object-space -> block-relative transform (per-BE rot/scale/local trans; no view rot; no camera-relative offset)
      packedLightFallback - vanilla packed light when sampling isn't possible
      out16 - destination, 16 floats (must be non-null)
    • getOrSample8Lod

      public static void getOrSample8Lod(@Nullable net.minecraft.world.level.block.entity.BlockEntity be, long partIdentityHash, float[] objBbox, net.minecraft.core.BlockPos blockPos, org.joml.Matrix4f localPose, int packedLightFallback, float[] out16, double distSqToCamera)
    • getOrSample16

      public static void getOrSample16(@Nullable net.minecraft.world.level.block.entity.BlockEntity be, long partIdentityHash, float[] objBbox, net.minecraft.core.BlockPos blockPos, org.joml.Matrix4f localPose, int packedLightFallback, float[] out32)
      Samples lightmap UV pairs for a 2x4x2 lattice (X/Z corners across 4 Y slices). Output layout: 16 probes * 2 floats = 32 floats: slice0: (x0z0, x1z0, x0z1, x1z1), then slice1, slice2, slice3; each probe contributes (blockU, skyV).

      Intended for very tall machines where a single 2x2x2 corner set fails to capture mid-height localized block light (e.g. a torch attached halfway up the side of a tower).