diff --git a/li/cil/oc/client/gui/MonospaceFontRenderer.scala b/li/cil/oc/client/gui/MonospaceFontRenderer.scala deleted file mode 100644 index f8c409c0d..000000000 --- a/li/cil/oc/client/gui/MonospaceFontRenderer.scala +++ /dev/null @@ -1,91 +0,0 @@ -package li.cil.oc.client.gui - -import li.cil.oc.Config -import net.minecraft.client.renderer.GLAllocation -import net.minecraft.client.renderer.Tessellator -import net.minecraft.client.renderer.texture.TextureManager -import net.minecraft.util.ResourceLocation -import org.lwjgl.opengl.GL11 -import scala.io.Source - -object MonospaceFontRenderer { - private val font = new ResourceLocation(Config.resourceDomain, "textures/font/chars.png") - - private val chars = Source.fromInputStream(MonospaceFontRenderer.getClass.getResourceAsStream("/assets/" + Config.resourceDomain + "/textures/font/chars.txt")).mkString - - private var instance: Option[Renderer] = None - - def init(textureManager: TextureManager) = - instance = instance.orElse(Some(new Renderer(textureManager))) - - val (fontWidth, fontHeight) = (5, 9) - - def drawString(value: Array[Char], x: Int, y: Int) = instance match { - case None => // Do nothing, not initialized. - case Some(renderer) => renderer.drawString(value, x, y) - } - - private class Renderer(private val textureManager: TextureManager) { - /** Display lists, one per char (renders quad with char's uv coords). */ - private val charLists = GLAllocation.generateDisplayLists(256) - - /** Buffer filled with char display lists to efficiently draw strings. */ - private val listBuffer = GLAllocation.createDirectIntBuffer(512) - - // Set up the display lists. - { - val (charWidth, charHeight) = (MonospaceFontRenderer.fontWidth * 2, MonospaceFontRenderer.fontHeight * 2) - val cols = 256 / charWidth - val uStep = charWidth / 256.0 - val vStep = charHeight / 256.0 - val t = Tessellator.instance - // Now create lists for all printable chars. - for (index <- 1 until 0xFF) { - val x = (index - 1) % cols - val y = (index - 1) / cols - val u = x * uStep - val v = y * vStep - GL11.glNewList(charLists + index, GL11.GL_COMPILE) - t.startDrawingQuads() - t.addVertexWithUV(0, charHeight, 0, u, v + vStep) - t.addVertexWithUV(charWidth, charHeight, 0, u + uStep, v + vStep) - t.addVertexWithUV(charWidth, 0, 0, u + uStep, v) - t.addVertexWithUV(0, 0, 0, u, v) - t.draw() - GL11.glTranslatef(charWidth, 0, 0) - GL11.glEndList() - } - // Special case for whitespace: just translate, don't render. - GL11.glNewList(charLists + ' ', GL11.GL_COMPILE) - GL11.glTranslatef(charWidth, 0, 0) - GL11.glEndList() - } - - def drawString(value: Array[Char], x: Int, y: Int) = { - setTexture(textureManager, MonospaceFontRenderer.font) - GL11.glPushMatrix() - GL11.glTranslatef(x, y, 0) - GL11.glScalef(0.5f, 0.5f, 1) - GL11.glColor4f(1, 1, 1, 1) - for (c <- value) { - val index = 1 + chars.indexOf(c) match { - case -1 => chars.indexOf('?') - case i => i - } - listBuffer.put(charLists + index) - if (listBuffer.remaining == 0) - flush() - } - flush() - GL11.glPopMatrix() - } - - private def flush() = { - listBuffer.flip() - GL11.glCallLists(listBuffer) - listBuffer.clear() - } - } - - private def setTexture(tm: TextureManager, resource: ResourceLocation) = tm.bindTexture(resource) -} \ No newline at end of file diff --git a/li/cil/oc/client/gui/Screen.scala b/li/cil/oc/client/gui/Screen.scala index 4c897f174..e2f28cb2c 100644 --- a/li/cil/oc/client/gui/Screen.scala +++ b/li/cil/oc/client/gui/Screen.scala @@ -2,6 +2,7 @@ package li.cil.oc.client.gui import li.cil.oc.Config import li.cil.oc.client.PacketSender +import li.cil.oc.client.renderer.MonospaceFontRenderer import li.cil.oc.common.tileentity import net.minecraft.client.gui.{GuiScreen => MCGuiScreen} import net.minecraft.client.renderer.GLAllocation @@ -11,6 +12,7 @@ import net.minecraft.util.ResourceLocation import org.lwjgl.input.Keyboard import org.lwjgl.opengl.GL11 import scala.collection.mutable +import li.cil.oc.util.PackedColor /** * This GUI shows the buffer of a single screen. @@ -47,11 +49,11 @@ class Screen(tileEntity: tileentity.Screen) extends MCGuiScreen { // Re-build display lists. Screen.compileBackground(innerWidth, innerHeight) - Screen.compileText(scale, screen.instance.lines) + Screen.compileText(scale, screen.instance.lines, screen.instance.colors, screen.instance.depth) } /** Must be called whenever the buffer of the underlying screen changes. */ - def updateText() = Screen.compileText(scale, screen.instance.lines) + def updateText() = Screen.compileText(scale, screen.instance.lines, screen.instance.colors, screen.instance.depth) override def handleKeyboardInput() { super.handleKeyboardInput() @@ -182,14 +184,14 @@ object Screen { GL11.glEndList() } - private[gui] def compileText(scale: Double, lines: Array[Array[Char]]) = + private[gui] def compileText(scale: Double, lines: Array[Array[Char]], colors:Array[Array[Int]], depth: PackedColor.Depth.Value) = if (textureManager.isDefined) { GL11.glNewList(displayLists.get + 1, GL11.GL_COMPILE) GL11.glTranslatef(margin + innerMargin, margin + innerMargin, 0) GL11.glScaled(scale, scale, 1) - lines.zipWithIndex.foreach { - case (line, i) => MonospaceFontRenderer.drawString(line, 0, i * MonospaceFontRenderer.fontHeight) + lines.zip(colors).zipWithIndex.foreach { + case ((line, color), i) => MonospaceFontRenderer.drawString(0, i * MonospaceFontRenderer.fontHeight, line, color, depth) } GL11.glEndList() diff --git a/li/cil/oc/client/renderer/MonospaceFontRenderer.scala b/li/cil/oc/client/renderer/MonospaceFontRenderer.scala new file mode 100644 index 000000000..b2313c5a8 --- /dev/null +++ b/li/cil/oc/client/renderer/MonospaceFontRenderer.scala @@ -0,0 +1,137 @@ +package li.cil.oc.client.renderer + +import li.cil.oc.util.{RenderState, PackedColor} +import li.cil.oc.{OpenComputers, Config} +import net.minecraft.client.renderer.GLAllocation +import net.minecraft.client.renderer.Tessellator +import net.minecraft.client.renderer.texture.TextureManager +import net.minecraft.util.ResourceLocation +import org.lwjgl.opengl.GL11 +import scala.io.Source + +object MonospaceFontRenderer { + private val font = new ResourceLocation(Config.resourceDomain, "textures/font/chars.png") + + private val chars = Source.fromInputStream(MonospaceFontRenderer.getClass.getResourceAsStream("/assets/" + Config.resourceDomain + "/textures/font/chars.txt")).mkString + + private var instance: Option[Renderer] = None + + def init(textureManager: TextureManager) = this.synchronized( + instance = instance.orElse(Some(new Renderer(textureManager)))) + + val (fontWidth, fontHeight) = (5, 9) + + def drawString(x: Int, y: Int, value: Array[Char], color: Array[Int], depth: PackedColor.Depth.Value) = instance match { + case None => OpenComputers.log.warning("Trying to render string with uninitialized MonospaceFontRenderer.") + case Some(renderer) => renderer.drawString(x, y, value, color, depth) + } + + private class Renderer(private val textureManager: TextureManager) { + /** Display lists, one per char (renders quad with char's uv coords). */ + private val charLists = GLAllocation.generateDisplayLists(256) + + /** Buffer filled with char display lists to efficiently draw strings. */ + private val listBuffer = GLAllocation.createDirectIntBuffer(512) + + private val (charWidth, charHeight) = (MonospaceFontRenderer.fontWidth * 2, MonospaceFontRenderer.fontHeight * 2) + private val cols = 256 / charWidth + private val uStep = charWidth / 256.0 + private val vStep = charHeight / 256.0 + + // Set up the display lists. + { + val t = Tessellator.instance + // Now create lists for all printable chars. + for (index <- 1 until 0xFF) { + val x = (index - 1) % cols + val y = (index - 1) / cols + val u = x * uStep + val v = y * vStep + GL11.glNewList(charLists + index, GL11.GL_COMPILE) + t.startDrawingQuads() + t.addVertexWithUV(0, charHeight, 0, u, v + vStep) + t.addVertexWithUV(charWidth, charHeight, 0, u + uStep, v + vStep) + t.addVertexWithUV(charWidth, 0, 0, u + uStep, v) + t.addVertexWithUV(0, 0, 0, u, v) + t.draw() + GL11.glTranslatef(charWidth, 0, 0) + GL11.glEndList() + } + // Special case for whitespace: just translate, don't render. + GL11.glNewList(charLists + ' ', GL11.GL_COMPILE) + GL11.glTranslatef(charWidth, 0, 0) + GL11.glEndList() + } + + def drawString(x: Int, y: Int, value: Array[Char], color: Array[Int], depth: PackedColor.Depth.Value) = { + if (color.length != value.length) throw new IllegalArgumentException("Color count must match char count.") + + textureManager.bindTexture(MonospaceFontRenderer.font) + GL11.glPushMatrix() + GL11.glPushAttrib(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT) + GL11.glTranslatef(x, y, 0) + GL11.glScalef(0.5f, 0.5f, 1) + GL11.glDepthMask(false) + RenderState.makeItBlend() + + // Background first. We try to merge adjacent backgrounds of the same + // color to reduce the number of quads we have to draw. + var cbg = 0x000000 + var width = 0 + for (col <- color.map(PackedColor.unpackBackground(_, depth))) { + if (col != cbg) { + draw(cbg, width) + cbg = col + width = 0 + } + width = width + 1 + } + draw(cbg, width) + + // Foreground second. We only have to flush when the color changes, so + // unless every char has a different color this should be quite efficient. + var cfg = 0x000000 + GL11.glColor3f(0, 0, 0) + for ((ch, col) <- value.zip(color.map(PackedColor.unpackForeground(_, depth)))) { + val index = 1 + chars.indexOf(ch) match { + case -1 => chars.indexOf('?') + case i => i + } + if (col != cfg) { + // Color changed, force flush and adjust colors. + flush() + cfg = col + GL11.glColor3ub( + (cfg & 0xFF0000 >> 16).toByte, + (cfg & 0x00FF00 >> 8).toByte, + (cfg & 0x0000FF).toByte) + } + listBuffer.put(charLists + index) + if (listBuffer.remaining == 0) + flush() + } + flush() + + GL11.glPopAttrib() + GL11.glPopMatrix() + } + + private def draw(color: Int, width: Int) = if (color != 0 && width > 0) { + val t = Tessellator.instance + t.startDrawingQuads() + t.setColorOpaque_I(color) + t.addVertexWithUV(0, charHeight, 0, 0, vStep) + t.addVertexWithUV(charWidth, charHeight, 0, width * uStep, vStep) + t.addVertexWithUV(charWidth, 0, 0, width * uStep, 0) + t.addVertexWithUV(0, 0, 0, 0, 0) + t.draw() + } + + private def flush() = { + listBuffer.flip() + GL11.glCallLists(listBuffer) + listBuffer.clear() + } + } + +} \ No newline at end of file diff --git a/li/cil/oc/client/renderer/tileentity/ScreenRenderer.scala b/li/cil/oc/client/renderer/tileentity/ScreenRenderer.scala index aa5627068..e929fdb07 100644 --- a/li/cil/oc/client/renderer/tileentity/ScreenRenderer.scala +++ b/li/cil/oc/client/renderer/tileentity/ScreenRenderer.scala @@ -5,7 +5,7 @@ import cpw.mods.fml.common.{TickType, ITickHandler} import java.util import java.util.concurrent.{TimeUnit, Callable} import li.cil.oc.Config -import li.cil.oc.client.gui.MonospaceFontRenderer +import li.cil.oc.client.renderer.MonospaceFontRenderer import li.cil.oc.common.tileentity.Screen import li.cil.oc.util.RenderState import net.minecraft.client.Minecraft @@ -130,8 +130,8 @@ object ScreenRenderer extends TileEntitySpecialRenderer with Callable[Int] with // Slightly offset the text so it doesn't clip into the screen. GL11.glTranslatef(0, 0, 0.01f) - for ((line, i) <- screen.instance.lines.zipWithIndex) { - MonospaceFontRenderer.drawString(line, 0, i * MonospaceFontRenderer.fontHeight) + for (((line, color), i) <- screen.instance.lines.zip(screen.instance.colors).zipWithIndex) { + MonospaceFontRenderer.drawString(0, i * MonospaceFontRenderer.fontHeight, line, color, screen.instance.depth) } GL11.glEndList() diff --git a/li/cil/oc/common/component/Screen.scala b/li/cil/oc/common/component/Screen.scala index bc84864ce..661bfabb8 100644 --- a/li/cil/oc/common/component/Screen.scala +++ b/li/cil/oc/common/component/Screen.scala @@ -3,12 +3,12 @@ package li.cil.oc.common.component import li.cil.oc.api.Persistable import li.cil.oc.api.network.Visibility import li.cil.oc.common.{tileentity, component} -import li.cil.oc.util.TextBuffer +import li.cil.oc.util.{PackedColor, TextBuffer} import li.cil.oc.{Config, util, api} import net.minecraft.nbt.NBTTagCompound class Screen(val owner: Screen.Environment, val maxResolution: (Int, Int)) extends Persistable { - private val buffer = new TextBuffer(maxResolution) + private val buffer = new TextBuffer(maxResolution, PackedColor.Depth.OneBit) // ----------------------------------------------------------------------- // @@ -16,6 +16,10 @@ class Screen(val owner: Screen.Environment, val maxResolution: (Int, Int)) exten def lines = buffer.buffer + def colors = buffer.color + + def depth = buffer.depth + def resolution = buffer.size def resolution_=(value: (Int, Int)) = { diff --git a/li/cil/oc/util/PackedColor.scala b/li/cil/oc/util/PackedColor.scala new file mode 100644 index 000000000..93d92c418 --- /dev/null +++ b/li/cil/oc/util/PackedColor.scala @@ -0,0 +1,100 @@ +package li.cil.oc.util + +object PackedColor { + + object Depth extends Enumeration { + val OneBit, EightBit, SixteenBit = Value + } + + private val rMask32 = 0xFF0000 + private val gMask32 = 0x00FF00 + private val bMask32 = 0x0000FF + private val rShift32 = 16 + private val gShift32 = 8 + private val bShift32 = 0 + + // 7 6 5 4 3 2 1 0 + // r r r g g g b b : 3.3.2 + private val rMask8 = Integer.parseInt("11100000", 2) + private val gMask8 = Integer.parseInt("00011100", 2) + private val bMask8 = Integer.parseInt("00000011", 2) + private val rShift8 = 5 + private val gShift8 = 2 + private val bShift8 = 0 + private val rScale8 = 255.0 / 0x7 + private val gScale8 = 255.0 / 0x7 + private val bScale8 = 255.0 / 0x3 + + // 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 + // r r r r r g g g g g g b b b b b : 5.6.5 + private val rMask16 = Integer.parseInt("1111100000000000", 2) + private val gMask16 = Integer.parseInt("0000011111100000", 2) + private val bMask16 = Integer.parseInt("0000000000011111", 2) + private val rShift16 = 11 + private val gShift16 = 5 + private val bShift16 = 0 + private val rScale16 = 255.0 / 0x1F + private val gScale16 = 255.0 / 0x3F + private val bScale16 = 255.0 / 0x1F + + private def extractFrom1Bit(c: Short) = if (c == 0) 0x000000 else 0xFFFFFF + + private def compressTo1Bit(c: Int) = (if (c == 0) 0 else 1).toShort + + private def extractFrom8Bit(c: Short) = { + val r = ((((c & rMask8) >>> rShift8) * rScale8).toInt << rShift32) & rMask32 + val g = ((((c & gMask8) >>> gShift8) * gScale8).toInt << gShift32) & gMask32 + val b = ((((c & bMask8) >>> bShift8) * bScale8).toInt << bShift32) & bMask32 + r | g | b + } + + private def compressTo8Bit(c: Int) = { + val r = ((((c & rMask32) >>> rShift32) / rScale8).toInt << rShift8) & rMask8 + val g = ((((c & gMask32) >>> gShift32) / gScale8).toInt << gShift8) & gMask8 + val b = ((((c & bMask32) >>> bShift32) / bScale8).toInt << bShift8) & bMask8 + (r | g | b).toShort + } + + private def extractFrom16Bit(c: Short) = { + val r = ((((c & rMask16) >>> rShift16) * rScale16).toInt << rShift32) & rMask32 + val g = ((((c & gMask16) >>> gShift16) * gScale16).toInt << gShift32) & gMask32 + val b = ((((c & bMask16) >>> bShift16) * bScale16).toInt << bShift32) & bMask32 + r | g | b + } + + private def compressTo16Bit(c: Int) = { + val r = ((((c & rMask32) >>> rShift32) / rScale16).toInt << rShift16) & rMask16 + val g = ((((c & gMask32) >>> gShift32) / gScale16).toInt << gShift16) & gMask16 + val b = ((((c & bMask32) >>> bShift32) / bScale16).toInt << bShift16) & bMask16 + (r | g | b).toShort + } + + // Colors are packed: 0xFFFFBBBB (F = foreground, B = background) + private val fgShift = 16 + private val bgShift = 0 + + def pack(foreground: Int, background: Int, depth: Depth.Value) = + depth match { + case Depth.OneBit => (compressTo1Bit(foreground) << fgShift) | (compressTo1Bit(background) << bgShift) + case Depth.EightBit => (compressTo8Bit(foreground) << fgShift) | (compressTo8Bit(background) << bgShift) + case Depth.SixteenBit => (compressTo16Bit(foreground) << fgShift) | (compressTo16Bit(background) << bgShift) + } + + def unpackForeground(color: Int, depth: Depth.Value) = { + val c = (color >>> fgShift).toShort + depth match { + case Depth.OneBit => extractFrom1Bit(c) + case Depth.EightBit => extractFrom8Bit(c) + case Depth.SixteenBit => extractFrom16Bit(c) + } + } + + def unpackBackground(color: Int, depth: Depth.Value) = { + val c = (color >>> bgShift).toShort + depth match { + case Depth.OneBit => extractFrom1Bit(c) + case Depth.EightBit => extractFrom8Bit(c) + case Depth.SixteenBit => extractFrom16Bit(c) + } + } +} diff --git a/li/cil/oc/util/TextBuffer.scala b/li/cil/oc/util/TextBuffer.scala index 4b8019a67..d09a6b824 100644 --- a/li/cil/oc/util/TextBuffer.scala +++ b/li/cil/oc/util/TextBuffer.scala @@ -1,8 +1,6 @@ package li.cil.oc.util -import net.minecraft.nbt.NBTTagCompound -import net.minecraft.nbt.NBTTagList -import net.minecraft.nbt.NBTTagString +import net.minecraft.nbt.{NBTTagIntArray, NBTTagCompound, NBTTagList, NBTTagString} /** * This stores chars in a 2D-Array and provides some manipulation functions. @@ -12,8 +10,30 @@ import net.minecraft.nbt.NBTTagString * relatively fast updates, given a smart algorithm (using copy()/fill() * instead of set()ing everything). */ -class TextBuffer(var width: Int, var height: Int) { - def this(size: (Int, Int)) = this(size._1, size._2) +class TextBuffer(var width: Int, var height: Int, val depth: PackedColor.Depth.Value) { + def this(size: (Int, Int), depth: PackedColor.Depth.Value) = this(size._1, size._2, depth) + + private var foreground_ = 0xFFFFFF + + private var background_ = 0x000000 + + private var packed = PackedColor.pack(foreground_, background_, depth) + + def foreground = foreground_ + + def foreground_=(value: Int) = { + foreground_ = value + packed = PackedColor.pack(foreground_, background_, depth) + } + + def background = background_ + + def background_=(value: Int) = { + background_ = value + packed = PackedColor.pack(foreground_, background_, depth) + } + + var color = Array.fill(height, width)(packed) var buffer = Array.fill(height, width)(' ') @@ -32,10 +52,13 @@ class TextBuffer(var width: Int, var height: Int) { val (w, h) = (iw max 1, ih max 1) if (width != w || height != h) { val newBuffer = Array.fill(h, w)(' ') - (0 until (h min height)) foreach { - y => Array.copy(buffer(y), 0, newBuffer(y), 0, w min width) - } + val newColor = Array.fill(h, w)(packed) + (0 until (h min height)).foreach(y => { + Array.copy(buffer(y), 0, newBuffer(y), 0, w min width) + Array.copy(color(y), 0, newColor(y), 0, w min width) + }) buffer = newBuffer + color = newColor width = w height = h true @@ -52,10 +75,12 @@ class TextBuffer(var width: Int, var height: Int) { else { var changed = false val line = buffer(row) + val lineColor = color(row) for (x <- col until ((col + s.length) min width)) if (x >= 0) { val c = s(x - col) - changed = changed || (line(x) != c) + changed = changed || (line(x) != c) || (lineColor(x) != packed) line(x) = c + lineColor(x) = packed } changed } @@ -68,9 +93,11 @@ class TextBuffer(var width: Int, var height: Int) { var changed = false for (y <- (row max 0) until ((row + h) min height)) { val line = buffer(y) + val lineColor = color(y) for (x <- (col max 0) until ((col + w) min width)) { - changed = changed || (line(x) != c) + changed = changed || (line(x) != c) || (lineColor(x) != packed) line(x) = c + lineColor(x) = packed } } changed @@ -97,13 +124,16 @@ class TextBuffer(var width: Int, var height: Int) { var changed = false for (ny <- dy0 to dy1 by sy) { val nl = buffer(ny) + val nc = color(ny) ny - ty match { case oy if oy >= 0 && oy < height => val ol = buffer(oy) + val oc = color(oy) for (nx <- dx0 to dx1 by sx) nx - tx match { case ox if ox >= 0 && ox < width => { - changed = changed || (nl(nx) != ol(ox)) + changed = changed || (nl(nx) != ol(ox)) || (nc(nx) != oc(ox)) nl(nx) = ol(ox) + nc(nx) = oc(ox) } case _ => /* Got no source column. */ } @@ -118,19 +148,30 @@ class TextBuffer(var width: Int, var height: Int) { val h = nbt.getInteger("height") size = (w, h) val b = nbt.getTagList("buffer") - for (i <- 0 until (h min b.tagCount())) { - set(0, i, b.tagAt(i).asInstanceOf[NBTTagString].data) + for (i <- 0 until (h min b.tagCount)) { + val line = b.tagAt(i).asInstanceOf[NBTTagString].data + set(0, i, line) + } + val c = nbt.getTagList("color") + for (i <- 0 until (h min c.tagCount)) { + val line = c.tagAt(i).asInstanceOf[NBTTagIntArray].intArray + Array.copy(line, 0, color(i), 0, line.length min width) } } def writeToNBT(nbt: NBTTagCompound): Unit = { nbt.setInteger("width", width) nbt.setInteger("height", height) - val b = new NBTTagList + val b = new NBTTagList() for (i <- 0 until height) { - b.appendTag(new NBTTagString("", String.valueOf(buffer(i)))) + b.appendTag(new NBTTagString(null, String.valueOf(buffer(i)))) } nbt.setTag("buffer", b) + val c = new NBTTagList() + for (i <- 0 until height) { + c.appendTag(new NBTTagIntArray(null, color(i))) + } + nbt.setTag("color", c) } override def toString = {