From 5c5435a07ac7f85f0f3ea7cdd1b242e488a7c110 Mon Sep 17 00:00:00 2001 From: boubou19 Date: Sun, 9 Aug 2026 01:09:30 +0200 Subject: [PATCH 1/8] cache light lamp states per dimension --- .../projectred/illumination/blocks.scala | 264 +++++++++++++++++- 1 file changed, 259 insertions(+), 5 deletions(-) diff --git a/src/main/scala/mrtjp/projectred/illumination/blocks.scala b/src/main/scala/mrtjp/projectred/illumination/blocks.scala index ac73e63c5..730979fdd 100644 --- a/src/main/scala/mrtjp/projectred/illumination/blocks.scala +++ b/src/main/scala/mrtjp/projectred/illumination/blocks.scala @@ -88,6 +88,13 @@ class BlockLamp override def getIcon(side: Int, meta: Int) = if (meta > 15) BlockLamp.on(meta % 16) else BlockLamp.off(meta) + override def getLightValue(w: IBlockAccess, x: Int, y: Int, z: Int) = + w match { + case world: World => + BlockLamp.getLightValue(world.provider.dimensionId, x, y, z) + case _ => super.getLightValue(w, x, y, z) + } + def getConnectionMask( world: IBlockAccess, x: Int, @@ -108,11 +115,199 @@ class BlockLamp object BlockLamp { var on: Seq[IIcon] = null var off: Seq[IIcon] = null + + private val cache = new LampLightCache + + def getLightValue(dim: Int, x: Int, y: Int, z: Int) = + cache.get(dim, x, y, z) >> 4 + def setLightValue(dim: Int, x: Int, y: Int, z: Int, light: Int, color: Int) = + cache.put(dim, x, y, z, (light << 4) | color) + def clearLightValue(dim: Int, x: Int, y: Int, z: Int) = + cache.remove(dim, x, y, z) + def foreachLitHalo(dim: Int)(f: Int4Consumer) = + cache.foreachLit(dim)(f) + def cacheVersion(dim: Int): Long = + cache.version(dim) +} + +trait Int4Consumer { + def apply(x: Int, y: Int, z: Int, color: Int): Unit +} + +private class LampLightCache { + private var dims = new Array[Int](2) + private var tables = new Array[LampLightTable](2) + private var count = 0 + + def get(dim: Int, x: Int, y: Int, z: Int): Int = { + val t = find(dim) + if (t == null) 0 else t.get(pack(x, y, z)) + } + + def put(dim: Int, x: Int, y: Int, z: Int, v: Int): Unit = + tableFor(dim).put(pack(x, y, z), v) + + def remove(dim: Int, x: Int, y: Int, z: Int): Unit = { + val t = find(dim) + if (t != null) t.remove(pack(x, y, z)) + } + + def foreachLit(dim: Int)(f: Int4Consumer): Unit = { + val t = find(dim) + if (t != null) t.foreachLit(f) + } + + def version(dim: Int): Long = { + val t = find(dim) + if (t == null) -1 else t.version + } + + private def find(dim: Int): LampLightTable = { + var i = 0 + while (i < count) { + if (dims(i) == dim) return tables(i) + i += 1 + } + null + } + + private def tableFor(dim: Int): LampLightTable = { + val t = find(dim) + if (t != null) return t + if (count == dims.length) { + val newDims = new Array[Int](dims.length * 2) + val newTables = new Array[LampLightTable](tables.length * 2) + System.arraycopy(dims, 0, newDims, 0, count) + System.arraycopy(tables, 0, newTables, 0, count) + dims = newDims + tables = newTables + } + val table = new LampLightTable + dims(count) = dim + tables(count) = table + count += 1 + table + } + + private def pack(x: Int, y: Int, z: Int) = + ((x.toLong & 0x3ffffffL) << 38) | ((z.toLong & 0x3ffffffL) << 12) | + (y.toLong & 0xfffL) +} + +private class LampLightTable { + private val EMPTY = Long.MinValue + private val TOMB = Long.MinValue + 1 + private var keys = Array.fill(8)(EMPTY) + private var vals = new Array[Int](8) + private var used = 0 + private var mask = 7 + private[illumination] var version: Long = 0 + + def get(key: Long): Int = { + var i = hash(key) & mask + var n = 0 + while (n < keys.length) { + val k = keys(i) + if (k == key) return vals(i) + if (k == EMPTY) return 0 + i = (i + 1) & mask + n += 1 + } + 0 + } + + def put(key: Long, v: Int): Unit = { + var i = hash(key) & mask + var n = 0 + while (n < keys.length) { + val k = keys(i) + if (k == key) { + if (vals(i) != v) { + vals(i) = v + version += 1 + } + return + } + if (k == EMPTY || k == TOMB) { + keys(i) = key + vals(i) = v + used += 1 + if (used >= keys.length - keys.length / 3) grow() + version += 1 + return + } + i = (i + 1) & mask + n += 1 + } + } + + def remove(key: Long): Unit = { + var i = hash(key) & mask + var n = 0 + while (n < keys.length) { + val k = keys(i) + if (k == key) { + keys(i) = TOMB + version += 1 + return + } + if (k == EMPTY) return + i = (i + 1) & mask + n += 1 + } + } + + private def grow(): Unit = { + val oldKeys = keys + val oldVals = vals + val size = oldKeys.length * 2 + keys = Array.fill(size)(EMPTY) + vals = new Array[Int](size) + mask = size - 1 + var i = 0 + while (i < oldKeys.length) { + val oldKey = oldKeys(i) + if (oldKey != EMPTY && oldKey != TOMB) { + var j = hash(oldKey) & mask + var n = 0 + while (n < size && keys(j) != EMPTY) { + j = (j + 1) & mask + n += 1 + } + if (keys(j) == EMPTY) { + keys(j) = oldKey + vals(j) = oldVals(i) + } + } + i += 1 + } + } + + private def hash(key: Long): Int = { + val h = key * 0x9e3779b97f4a7c15L + (h ^ (h >>> 32)).toInt + } + + def foreachLit(f: Int4Consumer): Unit = { + var i = 0 + while (i < keys.length) { + val k = keys(i) + if (k != EMPTY && k != TOMB && (vals(i) >>> 4) > 0) { + val x = ((k >>> 38).toInt << 6) >> 6 + val y = (k & 0xfffL).toInt + val z = (((k >>> 12).toInt & 0x3ffffff) << 6) >> 6 + f(x, y, z, vals(i) & 0xf) + } + i += 1 + } + } } class TileLamp extends InstancedBlockTile with ILight { var inverted = false var powered = false + private var lightCache = 0 + private var lightDirty = true override def getBlock = ProjectRedIllumination.blockLamp override def getMetaData = getColor + (if (inverted) 16 else 0) @@ -125,33 +320,86 @@ class TileLamp extends InstancedBlockTile with ILight { hit: Vector3 ) { inverted = meta > 15 + lightDirty = true scheduleTick(2) } - override def getLightValue = if (inverted != powered) - IlluminationProxy.getLightValue(getColor, 15) - else 0 + override def getLightValue = { + if (lightDirty) recomputeLight() + lightCache + } + + private def recomputeLight() { + lightCache = + if (inverted != powered) + IlluminationProxy.getLightValue(getColor, 15) + else 0 + lightDirty = false + BlockLamp.setLightValue( + world.provider.dimensionId, + x, + y, + z, + lightCache, + getColor + ) + } override def onNeighborChange(b: Block) { if (!world.isRemote) scheduleTick(2) } + override def onBlockRemoval() { + super.onBlockRemoval() + BlockLamp.clearLightValue(world.provider.dimensionId, x, y, z) + } + + override def onChunkUnload() { + super.onChunkUnload() + BlockLamp.clearLightValue(world.provider.dimensionId, x, y, z) + } + + override def invalidate() { + super.invalidate() + BlockLamp.clearLightValue(world.provider.dimensionId, x, y, z) + } + def checkPower = { world.isBlockIndirectlyGettingPowered(x, y, z) || world.getBlockPowerInput(x, y, z) != 0 } override def onScheduledTick() { - val old = powered + val oldInv = inverted + val oldPow = powered + inverted = getBlockMetadata > 15 powered = checkPower - if (old != powered) { + if (oldInv != inverted || oldPow != powered) { + recomputeLight() markDescUpdate() markLight() } } + override def update() { + super.update() + if (lightDirty) { + recomputeLight() + markLight() + } + } + + override def updateClient() { + super.updateClient() + if (lightDirty) { + recomputeLight() + markLight() + } + } + override def readDesc(in: MCDataInput) { inverted = in.readBoolean() powered = in.readBoolean() + recomputeLight() markRender() markLight() } @@ -163,6 +411,12 @@ class TileLamp extends InstancedBlockTile with ILight { override def load(tag: NBTTagCompound) { inverted = tag.getBoolean("inv") powered = tag.getBoolean("pow") + lightDirty = true + } + + override def validate() { + super.validate() + scheduleTick(2) } override def save(tag: NBTTagCompound) { From 70a56aacddd8a0192ba15a84406139fa22574996 Mon Sep 17 00:00:00 2001 From: boubou19 Date: Sun, 9 Aug 2026 01:09:32 +0200 Subject: [PATCH 2/8] render lamp halos from the light cache in one static VBO batch --- .../mrtjp/projectred/core/Configurator.scala | 6 - .../mrtjp/projectred/core/RenderHalo.scala | 366 +++++++++++++++--- .../projectred/illumination/renders.scala | 10 +- 3 files changed, 303 insertions(+), 79 deletions(-) diff --git a/src/main/scala/mrtjp/projectred/core/Configurator.scala b/src/main/scala/mrtjp/projectred/core/Configurator.scala index 0e31b18b5..7a13ff56a 100644 --- a/src/main/scala/mrtjp/projectred/core/Configurator.scala +++ b/src/main/scala/mrtjp/projectred/core/Configurator.scala @@ -37,7 +37,6 @@ object Configurator extends ModConfig("ProjRed|Core") { var logicwires3D = true var staticWires = true var staticGates = true - var lightHaloMax = -1 var pipeRoutingFX = true /** World Gen * */ @@ -169,11 +168,6 @@ object Configurator extends ModConfig("ProjRed|Core") { staticGates, "If set to false, gates will be rendered in the TESR rather than the WorldRenderer." ) - lightHaloMax = rendering.put( - "Light Halo Render Count", - lightHaloMax, - "Number of lights to render, -1 for unlimited" - ) pipeRoutingFX = rendering.put( "Routed Pipe FX", pipeRoutingFX, diff --git a/src/main/scala/mrtjp/projectred/core/RenderHalo.scala b/src/main/scala/mrtjp/projectred/core/RenderHalo.scala index 9a3e3d1d7..704307602 100644 --- a/src/main/scala/mrtjp/projectred/core/RenderHalo.scala +++ b/src/main/scala/mrtjp/projectred/core/RenderHalo.scala @@ -1,53 +1,108 @@ package mrtjp.projectred.core -import codechicken.lib.render.{BlockRenderer, CCRenderState} +import codechicken.lib.render.BlockRenderer.BlockFace import codechicken.lib.vec._ import cpw.mods.fml.common.eventhandler.SubscribeEvent import mrtjp.core.color.Colors +import mrtjp.projectred.illumination.{BlockLamp, Int4Consumer} import net.minecraft.client.Minecraft -import net.minecraft.world.World +import net.minecraft.client.renderer.culling.Frustrum +import net.minecraft.util.AxisAlignedBB import net.minecraftforge.client.event.RenderWorldLastEvent +import java.nio.{BufferOverflowException, ByteBuffer} +import org.lwjgl.BufferUtils import org.lwjgl.opengl.GL11._ +import org.lwjgl.opengl.GL15 object RenderHalo { - private var renderList = Vector[LightCache]() - private val renderEntityPos = new Vector3 - private val vec = new Vector3 - - private class LightCache( - val pos: BlockCoord, - val color: Int, - val cube: Cuboid6 - ) extends Ordered[LightCache] { - def this(x: Int, y: Int, z: Int, c: Int, cube: Cuboid6) = - this(new BlockCoord(x, y, z), c, cube) - - private def renderDist = - vec.set(pos.x, pos.y, pos.z).sub(renderEntityPos).magSquared - - override def compare(o: LightCache) = { - val ra = renderDist - val rb = o.renderDist - if (ra == rb) 0 else if (ra < rb) 1 else -1 + private var lightArray = new Array[LightCache](64) + private var lightCount = 0 + private val pool = new java.util.ArrayDeque[LightCache]() + private val translation = new Translation(0, 0, 0) + private val haloColours = Array.tabulate(16)(i => Colors(i).rgba) + private val haloAlpha = 128 / 255.0f + private val frustum = new Frustrum + private val cullBox = AxisAlignedBB.getBoundingBox(0, 0, 0, 0, 0, 0) + private val lampHaloBox = Cuboid6.full.copy.expand(0.05d) + + private val haloFaceVerts = { + val face = new BlockFace() + val buf = new Array[Float](72) + var s = 0 + var o = 0 + while (s < 6) { + face.loadCuboidFace(lampHaloBox, s) + var i = 0 + while (i < 4) { + val v = face.verts(i).vec + buf(o) = v.x.toFloat + buf(o + 1) = v.y.toFloat + buf(o + 2) = v.z.toFloat + o += 3 + i += 1 + } + s += 1 + } + buf + } + + private var batchVBO = 0 + private var batchDim = Int.MinValue + private var batchVersion = -1L + private var batchVerts = 0 + private var batchBuf: ByteBuffer = null + private var anchorX = 0.0d + private var anchorY = 0.0d + private var anchorZ = 0.0d + + private class VBOEntry( + val minX: Double, + val minY: Double, + val minZ: Double, + val maxX: Double, + val maxY: Double, + val maxZ: Double, + val vbo: Int + ) + + private val haloVBOs = new java.util.ArrayList[VBOEntry]() + + private class LightCache { + var x = 0 + var y = 0 + var z = 0 + var color = 0 + var cube: Cuboid6 = _ + + def set(x: Int, y: Int, z: Int, color: Int, cube: Cuboid6) { + this.x = x + this.y = y + this.z = z + this.color = color + this.cube = cube } } def addLight(x: Int, y: Int, z: Int, color: Int, box: Cuboid6) { - renderList :+= new LightCache(x, y, z, color, box) + if (lightCount == lightArray.length) + lightArray = java.util.Arrays.copyOf(lightArray, lightCount * 2) + var lc = pool.poll() + if (lc == null) lc = new LightCache() + lc.set(x, y, z, color, box) + lightArray(lightCount) = lc + lightCount += 1 } @SubscribeEvent def onRenderWorldLast(event: RenderWorldLastEvent) { - if (renderList.isEmpty) return - val w = Minecraft.getMinecraft.theWorld val entity = Minecraft.getMinecraft.renderViewEntity - renderEntityPos.set( - entity.posX, - entity.posY + entity.getEyeHeight, - entity.posZ + frustum.setPosition( + entity.posX - (entity.posX - entity.lastTickPosX) * event.partialTicks, + entity.posY - (entity.posY - entity.lastTickPosY) * event.partialTicks, + entity.posZ - (entity.posZ - entity.lastTickPosZ) * event.partialTicks ) - - renderList = renderList.sorted + val visible = compactVisible() + val world = Minecraft.getMinecraft.theWorld glPushMatrix() @@ -60,19 +115,42 @@ object RenderHalo { ) prepareRenderState() - val it = renderList.iterator - val max = - if (Configurator.lightHaloMax < 0) renderList.size - else Configurator.lightHaloMax var i = 0 - while (i < max && it.hasNext) { - val cc = it.next() - renderHalo(w, cc) + while (i < lightCount) { + if (i < visible) renderHalo(lightArray(i)) + pool.add(lightArray(i)) i += 1 } + lightCount = 0 + + if (world != null) { + val entity = Minecraft.getMinecraft.renderViewEntity + val dim = world.provider.dimensionId + val ver = BlockLamp.cacheVersion(dim) + val dx = entity.posX - anchorX + val dy = entity.posY - anchorY + val dz = entity.posZ - anchorZ + if ( + dim != batchDim || ver != batchVersion || + dx * dx + dy * dy + dz * dz > 1073741824.0d + ) { + rebuildBatch(dim) + batchDim = dim + batchVersion = ver + } + if (batchVerts > 0) { + glPushMatrix() + glTranslated( + anchorX - entity.posX, + anchorY - entity.posY, + anchorZ - entity.posZ + ) + drawBatch() + glPopMatrix() + } + } - renderList = Vector() restoreRenderState() glPopMatrix() } @@ -84,14 +162,9 @@ object RenderHalo { glDisable(GL_LIGHTING) glDisable(GL_CULL_FACE) glDepthMask(false) - val state = CCRenderState.instance - state.resetInstance() - state.setDynamicInstance() - state.startDrawingInstance() } def restoreRenderState() { - CCRenderState.instance.drawInstance() glDepthMask(true) glColor4d(1, 1, 1, 1) glEnable(GL_CULL_FACE) @@ -101,32 +174,197 @@ object RenderHalo { glDisable(GL_BLEND) } - private def renderHalo(world: World, cc: LightCache) { - CCRenderState.instance.setBrightnessInstance( - world, - cc.pos.x, - cc.pos.y, - cc.pos.z - ) + private def renderHalo(cc: LightCache) { + renderHaloAt(cc.x, cc.y, cc.z, cc.color, cc.cube) + } + + private def renderHaloAt(x: Int, y: Int, z: Int, color: Int, box: Cuboid6) { // Make sure to use camera coordinates for the halo transformation. val entity = Minecraft.getMinecraft.renderViewEntity - renderHalo( - cc.cube, - cc.color, - new Translation( - cc.pos.x - entity.posX, - cc.pos.y - entity.posY, - cc.pos.z - entity.posZ + translation.vec.set(x - entity.posX, y - entity.posY, z - entity.posZ) + glPushMatrix() + renderHalo(box, color, translation) + glPopMatrix() + } + + private def inFrustum(cc: LightCache): Boolean = + inFrustum(cc.x, cc.y, cc.z, cc.cube) + + private def inFrustum(x: Int, y: Int, z: Int, cube: Cuboid6): Boolean = { + cullBox.minX = x + cube.min.x + cullBox.minY = y + cube.min.y + cullBox.minZ = z + cube.min.z + cullBox.maxX = x + cube.max.x + cullBox.maxY = y + cube.max.y + cullBox.maxZ = z + cube.max.z + frustum.isBoundingBoxInFrustum(cullBox) + } + + private def compactVisible(): Int = { + var w = 0 + var i = 0 + while (i < lightCount) { + val cc = lightArray(i) + if (inFrustum(cc)) { + if (w != i) { + lightArray(i) = lightArray(w) + lightArray(w) = cc + } + w += 1 + } + i += 1 + } + w + } + + def renderHalo(cuboid: Cuboid6, colour: Int, t: Transformation) { + val rgba = haloColours(colour) + glColor4f( + (rgba >>> 24 & 255) / 255.0f, + (rgba >>> 16 & 255) / 255.0f, + (rgba >>> 8 & 255) / 255.0f, + haloAlpha + ) + t.glApply() + val vbo = getHaloVBO(cuboid) + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo) + glVertexPointer(3, GL_FLOAT, 0, 0L) + glEnableClientState(GL_VERTEX_ARRAY) + glDrawArrays(GL_QUADS, 0, 24) + glDisableClientState(GL_VERTEX_ARRAY) + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0) + } + + private def getHaloVBO(cuboid: Cuboid6): Int = { + var i = 0 + while (i < haloVBOs.size) { + val e = haloVBOs.get(i) + if ( + e.minX == cuboid.min.x && e.minY == cuboid.min.y && e.minZ == cuboid.min.z + && e.maxX == cuboid.max.x && e.maxY == cuboid.max.y && e.maxZ == cuboid.max.z + ) + return e.vbo + i += 1 + } + val vbo = buildHaloVBO(cuboid) + haloVBOs.add( + new VBOEntry( + cuboid.min.x, + cuboid.min.y, + cuboid.min.z, + cuboid.max.x, + cuboid.max.y, + cuboid.max.z, + vbo ) ) + vbo } - def renderHalo(cuboid: Cuboid6, colour: Int, t: Transformation) { - val state = CCRenderState.instance - state.resetInstance() - state.setPipelineInstance(t) - state.baseColour = Colors(colour).rgba - state.alphaOverride = 128 - BlockRenderer.renderCuboid(cuboid, 0) + private def buildHaloVBO(cuboid: Cuboid6): Int = { + val buf = BufferUtils.createFloatBuffer(72) + val face = new BlockFace() + var s = 0 + while (s < 6) { + face.loadCuboidFace(cuboid, s) + var i = 0 + while (i < 4) { + val v = face.verts(i).vec + buf.put(v.x.toFloat).put(v.y.toFloat).put(v.z.toFloat) + i += 1 + } + s += 1 + } + buf.flip() + val vbo = GL15.glGenBuffers() + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo) + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buf, GL15.GL_STATIC_DRAW) + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0) + vbo + } + + private def rebuildBatch(dim: Int) { + val entity = Minecraft.getMinecraft.renderViewEntity + anchorX = entity.posX + anchorY = entity.posY + anchorZ = entity.posZ + var count = 0 + val colorCounts = new Array[Int](16) + val counter = new Int4Consumer { + override def apply(x: Int, y: Int, z: Int, color: Int) { + count += 1 + colorCounts(color & 15) += 1 + } + } + BlockLamp.foreachLitHalo(dim)(counter) + if (count == 0) { + batchVerts = 0 + return + } + val ranges = new Array[Int](17) + var c = 0 + while (c < 16) { + ranges(c + 1) = ranges(c) + colorCounts(c) * 24 * 12 + c += 1 + } + val bytes = ranges(16) + 2048 + if (batchBuf == null || batchBuf.capacity < bytes) + batchBuf = BufferUtils.createByteBuffer(bytes) + batchBuf.clear() + val b = batchBuf + val cursors = ranges.clone() + val filler = new Int4Consumer { + override def apply(x: Int, y: Int, z: Int, color: Int) { + val cc = color & 15 + b.position(cursors(cc)) + var i = 0 + while (i < 72) { + b.putFloat(haloFaceVerts(i) + x - anchorX.toFloat) + b.putFloat(haloFaceVerts(i + 1) + y - anchorY.toFloat) + b.putFloat(haloFaceVerts(i + 2) + z - anchorZ.toFloat) + i += 3 + } + cursors(cc) += 24 * 12 + } + } + try { + BlockLamp.foreachLitHalo(dim)(filler) + } catch { + case _: BufferOverflowException => return + } + batchVerts = b.position() / 12 + batchRanges = ranges + b.flip() + if (batchVBO == 0) batchVBO = GL15.glGenBuffers() + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, batchVBO) + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, b, GL15.GL_STATIC_DRAW) + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0) + } + + private def drawBatch() { + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, batchVBO) + glEnableClientState(GL_VERTEX_ARRAY) + glVertexPointer(3, GL_FLOAT, 12, 0L) + var c = 0 + while (c < 16) { + val count = (batchVertsOf(c + 1) - batchVertsOf(c)) / 24 + if (count > 0) { + val rgba = haloColours(c) + glColor4f( + (rgba >>> 24 & 255) / 255.0f, + (rgba >>> 16 & 255) / 255.0f, + (rgba >>> 8 & 255) / 255.0f, + haloAlpha + ) + glDrawArrays(GL_QUADS, batchVertsOf(c), count * 24) + } + c += 1 + } + glDisableClientState(GL_VERTEX_ARRAY) + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0) } + + private def batchVertsOf(i: Int) = batchRanges(i) / 12 + + private var batchRanges = new Array[Int](17) } diff --git a/src/main/scala/mrtjp/projectred/illumination/renders.scala b/src/main/scala/mrtjp/projectred/illumination/renders.scala index 28860b2a3..f21ca02c3 100644 --- a/src/main/scala/mrtjp/projectred/illumination/renders.scala +++ b/src/main/scala/mrtjp/projectred/illumination/renders.scala @@ -76,15 +76,7 @@ object LampTESR extends TileEntitySpecialRenderer with IItemRenderer { y: Double, z: Double, partials: Float - ) { - te match { - case light: ILight if light.isOn => - val meta = - te.getWorldObj.getBlockMetadata(te.xCoord, te.yCoord, te.zCoord) % 16 - RenderHalo.addLight(te.xCoord, te.yCoord, te.zCoord, meta, lBounds) - case _ => - } - } + ) {} } trait ButtonRenderCommons extends IItemRenderer { From 9b45acf3848ef547900c9226a6a7a6540bd9ea48 Mon Sep 17 00:00:00 2001 From: Algent Date: Sun, 9 Aug 2026 02:09:40 +0200 Subject: [PATCH 3/8] fix lamp cache world ownership --- .../mrtjp/projectred/core/RenderHalo.scala | 18 ++-- .../projectred/illumination/blocks.scala | 85 ++++++++----------- 2 files changed, 43 insertions(+), 60 deletions(-) diff --git a/src/main/scala/mrtjp/projectred/core/RenderHalo.scala b/src/main/scala/mrtjp/projectred/core/RenderHalo.scala index 704307602..218b308a4 100644 --- a/src/main/scala/mrtjp/projectred/core/RenderHalo.scala +++ b/src/main/scala/mrtjp/projectred/core/RenderHalo.scala @@ -8,6 +8,7 @@ import mrtjp.projectred.illumination.{BlockLamp, Int4Consumer} import net.minecraft.client.Minecraft import net.minecraft.client.renderer.culling.Frustrum import net.minecraft.util.AxisAlignedBB +import net.minecraft.world.World import net.minecraftforge.client.event.RenderWorldLastEvent import java.nio.{BufferOverflowException, ByteBuffer} import org.lwjgl.BufferUtils @@ -47,7 +48,7 @@ object RenderHalo { } private var batchVBO = 0 - private var batchDim = Int.MinValue + private var batchWorld: World = null private var batchVersion = -1L private var batchVerts = 0 private var batchBuf: ByteBuffer = null @@ -126,17 +127,16 @@ object RenderHalo { if (world != null) { val entity = Minecraft.getMinecraft.renderViewEntity - val dim = world.provider.dimensionId - val ver = BlockLamp.cacheVersion(dim) + val ver = BlockLamp.cacheVersion(world) val dx = entity.posX - anchorX val dy = entity.posY - anchorY val dz = entity.posZ - anchorZ if ( - dim != batchDim || ver != batchVersion || + (world ne batchWorld) || ver != batchVersion || dx * dx + dy * dy + dz * dz > 1073741824.0d ) { - rebuildBatch(dim) - batchDim = dim + rebuildBatch(world) + batchWorld = world batchVersion = ver } if (batchVerts > 0) { @@ -283,7 +283,7 @@ object RenderHalo { vbo } - private def rebuildBatch(dim: Int) { + private def rebuildBatch(world: World) { val entity = Minecraft.getMinecraft.renderViewEntity anchorX = entity.posX anchorY = entity.posY @@ -296,7 +296,7 @@ object RenderHalo { colorCounts(color & 15) += 1 } } - BlockLamp.foreachLitHalo(dim)(counter) + BlockLamp.foreachLitHalo(world)(counter) if (count == 0) { batchVerts = 0 return @@ -328,7 +328,7 @@ object RenderHalo { } } try { - BlockLamp.foreachLitHalo(dim)(filler) + BlockLamp.foreachLitHalo(world)(filler) } catch { case _: BufferOverflowException => return } diff --git a/src/main/scala/mrtjp/projectred/illumination/blocks.scala b/src/main/scala/mrtjp/projectred/illumination/blocks.scala index 730979fdd..8bbcc83da 100644 --- a/src/main/scala/mrtjp/projectred/illumination/blocks.scala +++ b/src/main/scala/mrtjp/projectred/illumination/blocks.scala @@ -91,7 +91,7 @@ class BlockLamp override def getLightValue(w: IBlockAccess, x: Int, y: Int, z: Int) = w match { case world: World => - BlockLamp.getLightValue(world.provider.dimensionId, x, y, z) + BlockLamp.getLightValue(world, x, y, z) case _ => super.getLightValue(w, x, y, z) } @@ -118,16 +118,16 @@ object BlockLamp { private val cache = new LampLightCache - def getLightValue(dim: Int, x: Int, y: Int, z: Int) = - cache.get(dim, x, y, z) >> 4 - def setLightValue(dim: Int, x: Int, y: Int, z: Int, light: Int, color: Int) = - cache.put(dim, x, y, z, (light << 4) | color) - def clearLightValue(dim: Int, x: Int, y: Int, z: Int) = - cache.remove(dim, x, y, z) - def foreachLitHalo(dim: Int)(f: Int4Consumer) = - cache.foreachLit(dim)(f) - def cacheVersion(dim: Int): Long = - cache.version(dim) + def getLightValue(world: World, x: Int, y: Int, z: Int) = + cache.get(world, x, y, z) >> 4 + def setLightValue(world: World, x: Int, y: Int, z: Int, light: Int, color: Int) = + cache.put(world, x, y, z, (light << 4) | color) + def clearLightValue(world: World, x: Int, y: Int, z: Int) = + cache.remove(world, x, y, z) + def foreachLitHalo(world: World)(f: Int4Consumer) = + cache.foreachLit(world)(f) + def cacheVersion(world: World): Long = + cache.version(world) } trait Int4Consumer { @@ -135,57 +135,40 @@ trait Int4Consumer { } private class LampLightCache { - private var dims = new Array[Int](2) - private var tables = new Array[LampLightTable](2) - private var count = 0 + private val tables = new java.util.WeakHashMap[World, LampLightTable]() - def get(dim: Int, x: Int, y: Int, z: Int): Int = { - val t = find(dim) + def get(world: World, x: Int, y: Int, z: Int): Int = { + val t = find(world) if (t == null) 0 else t.get(pack(x, y, z)) } - def put(dim: Int, x: Int, y: Int, z: Int, v: Int): Unit = - tableFor(dim).put(pack(x, y, z), v) + def put(world: World, x: Int, y: Int, z: Int, v: Int): Unit = + tableFor(world).put(pack(x, y, z), v) - def remove(dim: Int, x: Int, y: Int, z: Int): Unit = { - val t = find(dim) + def remove(world: World, x: Int, y: Int, z: Int): Unit = { + val t = find(world) if (t != null) t.remove(pack(x, y, z)) } - def foreachLit(dim: Int)(f: Int4Consumer): Unit = { - val t = find(dim) + def foreachLit(world: World)(f: Int4Consumer): Unit = { + val t = find(world) if (t != null) t.foreachLit(f) } - def version(dim: Int): Long = { - val t = find(dim) + def version(world: World): Long = { + val t = find(world) if (t == null) -1 else t.version } - private def find(dim: Int): LampLightTable = { - var i = 0 - while (i < count) { - if (dims(i) == dim) return tables(i) - i += 1 - } - null - } - - private def tableFor(dim: Int): LampLightTable = { - val t = find(dim) - if (t != null) return t - if (count == dims.length) { - val newDims = new Array[Int](dims.length * 2) - val newTables = new Array[LampLightTable](tables.length * 2) - System.arraycopy(dims, 0, newDims, 0, count) - System.arraycopy(tables, 0, newTables, 0, count) - dims = newDims - tables = newTables + private def find(world: World): LampLightTable = + tables.synchronized(tables.get(world)) + + private def tableFor(world: World): LampLightTable = tables.synchronized { + var table = tables.get(world) + if (table == null) { + table = new LampLightTable + tables.put(world, table) } - val table = new LampLightTable - dims(count) = dim - tables(count) = table - count += 1 table } @@ -335,7 +318,7 @@ class TileLamp extends InstancedBlockTile with ILight { else 0 lightDirty = false BlockLamp.setLightValue( - world.provider.dimensionId, + world, x, y, z, @@ -350,17 +333,17 @@ class TileLamp extends InstancedBlockTile with ILight { override def onBlockRemoval() { super.onBlockRemoval() - BlockLamp.clearLightValue(world.provider.dimensionId, x, y, z) + BlockLamp.clearLightValue(world, x, y, z) } override def onChunkUnload() { super.onChunkUnload() - BlockLamp.clearLightValue(world.provider.dimensionId, x, y, z) + BlockLamp.clearLightValue(world, x, y, z) } override def invalidate() { super.invalidate() - BlockLamp.clearLightValue(world.provider.dimensionId, x, y, z) + BlockLamp.clearLightValue(world, x, y, z) } def checkPower = { From 2e622b7c1d8a76a1fcb0fa9ff671c4b84e92cd59 Mon Sep 17 00:00:00 2001 From: Algent Date: Sun, 9 Aug 2026 02:10:42 +0200 Subject: [PATCH 4/8] fix lamp cache tombstone probing --- .../projectred/illumination/blocks.scala | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/scala/mrtjp/projectred/illumination/blocks.scala b/src/main/scala/mrtjp/projectred/illumination/blocks.scala index 8bbcc83da..aa061cb67 100644 --- a/src/main/scala/mrtjp/projectred/illumination/blocks.scala +++ b/src/main/scala/mrtjp/projectred/illumination/blocks.scala @@ -202,6 +202,7 @@ private class LampLightTable { def put(key: Long, v: Int): Unit = { var i = hash(key) & mask var n = 0 + var tomb = -1 while (n < keys.length) { val k = keys(i) if (k == key) { @@ -211,17 +212,23 @@ private class LampLightTable { } return } - if (k == EMPTY || k == TOMB) { - keys(i) = key - vals(i) = v - used += 1 - if (used >= keys.length - keys.length / 3) grow() - version += 1 + if (k == TOMB && tomb < 0) tomb = i + else if (k == EMPTY) { + insert(if (tomb < 0) i else tomb, key, v) return } i = (i + 1) & mask n += 1 } + if (tomb >= 0) insert(tomb, key, v) + } + + private def insert(i: Int, key: Long, v: Int): Unit = { + keys(i) = key + vals(i) = v + used += 1 + if (used >= keys.length - keys.length / 3) grow() + version += 1 } def remove(key: Long): Unit = { From 945a9eae5311490f4048c754b91067b44638175d Mon Sep 17 00:00:00 2001 From: Algent Date: Sun, 9 Aug 2026 02:11:17 +0200 Subject: [PATCH 5/8] fix multicolour halo batch upload --- src/main/scala/mrtjp/projectred/core/RenderHalo.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/scala/mrtjp/projectred/core/RenderHalo.scala b/src/main/scala/mrtjp/projectred/core/RenderHalo.scala index 218b308a4..b8739c3e2 100644 --- a/src/main/scala/mrtjp/projectred/core/RenderHalo.scala +++ b/src/main/scala/mrtjp/projectred/core/RenderHalo.scala @@ -332,8 +332,10 @@ object RenderHalo { } catch { case _: BufferOverflowException => return } - batchVerts = b.position() / 12 + val batchBytes = ranges(16) + batchVerts = batchBytes / 12 batchRanges = ranges + b.position(batchBytes) b.flip() if (batchVBO == 0) batchVBO = GL15.glGenBuffers() GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, batchVBO) From 18e9bf4b10a5e285c426f1b4adf5325e1d8dde15 Mon Sep 17 00:00:00 2001 From: Algent Date: Sun, 9 Aug 2026 02:11:51 +0200 Subject: [PATCH 6/8] fix lamp cache tombstone accounting --- src/main/scala/mrtjp/projectred/illumination/blocks.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/scala/mrtjp/projectred/illumination/blocks.scala b/src/main/scala/mrtjp/projectred/illumination/blocks.scala index aa061cb67..bd7ea0c0c 100644 --- a/src/main/scala/mrtjp/projectred/illumination/blocks.scala +++ b/src/main/scala/mrtjp/projectred/illumination/blocks.scala @@ -224,9 +224,9 @@ private class LampLightTable { } private def insert(i: Int, key: Long, v: Int): Unit = { + if (keys(i) == EMPTY) used += 1 keys(i) = key vals(i) = v - used += 1 if (used >= keys.length - keys.length / 3) grow() version += 1 } @@ -254,6 +254,7 @@ private class LampLightTable { keys = Array.fill(size)(EMPTY) vals = new Array[Int](size) mask = size - 1 + used = 0 var i = 0 while (i < oldKeys.length) { val oldKey = oldKeys(i) @@ -267,6 +268,7 @@ private class LampLightTable { if (keys(j) == EMPTY) { keys(j) = oldKey vals(j) = oldVals(i) + used += 1 } } i += 1 From e52e35bbce77fdae47feaeb17fc6b7ae9bcb3e18 Mon Sep 17 00:00:00 2001 From: Algent Date: Sun, 9 Aug 2026 02:12:28 +0200 Subject: [PATCH 7/8] fix halo batch coordinate precision --- src/main/scala/mrtjp/projectred/core/RenderHalo.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/mrtjp/projectred/core/RenderHalo.scala b/src/main/scala/mrtjp/projectred/core/RenderHalo.scala index b8739c3e2..95734df1e 100644 --- a/src/main/scala/mrtjp/projectred/core/RenderHalo.scala +++ b/src/main/scala/mrtjp/projectred/core/RenderHalo.scala @@ -319,9 +319,9 @@ object RenderHalo { b.position(cursors(cc)) var i = 0 while (i < 72) { - b.putFloat(haloFaceVerts(i) + x - anchorX.toFloat) - b.putFloat(haloFaceVerts(i + 1) + y - anchorY.toFloat) - b.putFloat(haloFaceVerts(i + 2) + z - anchorZ.toFloat) + b.putFloat((x.toDouble - anchorX + haloFaceVerts(i)).toFloat) + b.putFloat((y.toDouble - anchorY + haloFaceVerts(i + 1)).toFloat) + b.putFloat((z.toDouble - anchorZ + haloFaceVerts(i + 2)).toFloat) i += 3 } cursors(cc) += 24 * 12 From 0c3beaaa0b92a07d46c70aae1108d51896ef18fa Mon Sep 17 00:00:00 2001 From: boubou19 Date: Sun, 9 Aug 2026 04:29:03 +0200 Subject: [PATCH 8/8] fix world leak + spotless --- src/main/scala/mrtjp/projectred/core/RenderHalo.scala | 6 ++++++ .../scala/mrtjp/projectred/illumination/blocks.scala | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/scala/mrtjp/projectred/core/RenderHalo.scala b/src/main/scala/mrtjp/projectred/core/RenderHalo.scala index 95734df1e..e40cf55c8 100644 --- a/src/main/scala/mrtjp/projectred/core/RenderHalo.scala +++ b/src/main/scala/mrtjp/projectred/core/RenderHalo.scala @@ -10,6 +10,7 @@ import net.minecraft.client.renderer.culling.Frustrum import net.minecraft.util.AxisAlignedBB import net.minecraft.world.World import net.minecraftforge.client.event.RenderWorldLastEvent +import net.minecraftforge.event.world.WorldEvent import java.nio.{BufferOverflowException, ByteBuffer} import org.lwjgl.BufferUtils import org.lwjgl.opengl.GL11._ @@ -94,6 +95,11 @@ object RenderHalo { lightCount += 1 } + @SubscribeEvent + def onWorldUnload(event: WorldEvent.Unload) { + batchWorld = null + } + @SubscribeEvent def onRenderWorldLast(event: RenderWorldLastEvent) { val entity = Minecraft.getMinecraft.renderViewEntity diff --git a/src/main/scala/mrtjp/projectred/illumination/blocks.scala b/src/main/scala/mrtjp/projectred/illumination/blocks.scala index bd7ea0c0c..08a3571a0 100644 --- a/src/main/scala/mrtjp/projectred/illumination/blocks.scala +++ b/src/main/scala/mrtjp/projectred/illumination/blocks.scala @@ -120,7 +120,14 @@ object BlockLamp { def getLightValue(world: World, x: Int, y: Int, z: Int) = cache.get(world, x, y, z) >> 4 - def setLightValue(world: World, x: Int, y: Int, z: Int, light: Int, color: Int) = + def setLightValue( + world: World, + x: Int, + y: Int, + z: Int, + light: Int, + color: Int + ) = cache.put(world, x, y, z, (light << 4) | color) def clearLightValue(world: World, x: Int, y: Int, z: Int) = cache.remove(world, x, y, z)