diff --git a/src/main/scala/mrtjp/projectred/core/Configurator.scala b/src/main/scala/mrtjp/projectred/core/Configurator.scala index 11628240a..af7cb0fd7 100644 --- a/src/main/scala/mrtjp/projectred/core/Configurator.scala +++ b/src/main/scala/mrtjp/projectred/core/Configurator.scala @@ -39,7 +39,6 @@ object Configurator extends ModConfig("ProjRed|Core") { var logicwires3D = true var staticWires = true var staticGates = true - var lightHaloMax = -1 var pipeRoutingFX = true /** World Gen * */ @@ -176,11 +175,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..e40cf55c8 100644 --- a/src/main/scala/mrtjp/projectred/core/RenderHalo.scala +++ b/src/main/scala/mrtjp/projectred/core/RenderHalo.scala @@ -1,53 +1,115 @@ 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.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._ +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 batchWorld: World = null + 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 onWorldUnload(event: WorldEvent.Unload) { + batchWorld = null } @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 +122,41 @@ 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 ver = BlockLamp.cacheVersion(world) + val dx = entity.posX - anchorX + val dy = entity.posY - anchorY + val dz = entity.posZ - anchorZ + if ( + (world ne batchWorld) || ver != batchVersion || + dx * dx + dy * dy + dz * dz > 1073741824.0d + ) { + rebuildBatch(world) + batchWorld = world + batchVersion = ver + } + if (batchVerts > 0) { + glPushMatrix() + glTranslated( + anchorX - entity.posX, + anchorY - entity.posY, + anchorZ - entity.posZ + ) + drawBatch() + glPopMatrix() + } + } - renderList = Vector() restoreRenderState() glPopMatrix() } @@ -84,14 +168,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 +180,199 @@ 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(world: World) { + 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(world)(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((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 + } + } + try { + BlockLamp.foreachLitHalo(world)(filler) + } catch { + case _: BufferOverflowException => return + } + 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) + 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/blocks.scala b/src/main/scala/mrtjp/projectred/illumination/blocks.scala index ac73e63c5..08a3571a0 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, x, y, z) + case _ => super.getLightValue(w, x, y, z) + } + def getConnectionMask( world: IBlockAccess, x: Int, @@ -108,11 +115,198 @@ class BlockLamp object BlockLamp { var on: Seq[IIcon] = null var off: Seq[IIcon] = null + + private val cache = new LampLightCache + + 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 { + def apply(x: Int, y: Int, z: Int, color: Int): Unit +} + +private class LampLightCache { + private val tables = new java.util.WeakHashMap[World, LampLightTable]() + + 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(world: World, x: Int, y: Int, z: Int, v: Int): Unit = + tableFor(world).put(pack(x, y, z), v) + + 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(world: World)(f: Int4Consumer): Unit = { + val t = find(world) + if (t != null) t.foreachLit(f) + } + + def version(world: World): Long = { + val t = find(world) + if (t == null) -1 else t.version + } + + 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) + } + 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 + var tomb = -1 + while (n < keys.length) { + val k = keys(i) + if (k == key) { + if (vals(i) != v) { + vals(i) = v + version += 1 + } + return + } + 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 = { + if (keys(i) == EMPTY) used += 1 + keys(i) = key + vals(i) = v + if (used >= keys.length - keys.length / 3) grow() + version += 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 + used = 0 + 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) + used += 1 + } + } + 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 +319,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, + x, + y, + z, + lightCache, + getColor + ) + } override def onNeighborChange(b: Block) { if (!world.isRemote) scheduleTick(2) } + override def onBlockRemoval() { + super.onBlockRemoval() + BlockLamp.clearLightValue(world, x, y, z) + } + + override def onChunkUnload() { + super.onChunkUnload() + BlockLamp.clearLightValue(world, x, y, z) + } + + override def invalidate() { + super.invalidate() + BlockLamp.clearLightValue(world, 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 +410,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) { 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 {