gpu video ram with allocate, free, bitblts

writing text and color data to a gpu page is free and server side only
bitblt to screen cause an update and has more budget and power cost
This commit is contained in:
payonel 2020-05-17 17:29:19 -07:00
parent 9d38ecb51d
commit e70856bf9f
11 changed files with 611 additions and 95 deletions

View File

@ -244,6 +244,16 @@ opencomputers {
1024 1024
] ]
# Video ram can be allocated on a gpu. The amount of vram you can allocate
# is equal to the width*height of the max resolution of the gpu multiplied
# by the "vramSize" for that tier. For example, a T2 gpu can have 80*25*2 of
# text buffer space allocated
vramSizes: [
1,
2,
3
]
# This setting allows you to fine-tune how RAM sizes are scaled internally # This setting allows you to fine-tune how RAM sizes are scaled internally
# on 64 Bit machines (i.e. when the Minecraft server runs in a 64 Bit VM). # on 64 Bit machines (i.e. when the Minecraft server runs in a 64 Bit VM).
# Why is this even necessary? Because objects consume more memory in a 64 # Why is this even necessary? Because objects consume more memory in a 64

View File

@ -93,6 +93,12 @@ class Settings(val config: Config) {
OpenComputers.log.warn("Bad number of RAM sizes, ignoring.") OpenComputers.log.warn("Bad number of RAM sizes, ignoring.")
Array(192, 256, 384, 512, 768, 1024) Array(192, 256, 384, 512, 768, 1024)
} }
val vramSizes = Array(config.getIntList("computer.lua.vramSizes"): _*) match {
case Array(tier1, tier2, tier3) => Array(tier1: Int, tier2: Int, tier3: Int)
case _ =>
OpenComputers.log.warn("Bad number of VRAM sizes, ignoring.")
Array(1, 2, 3)
}
val ramScaleFor64Bit = config.getDouble("computer.lua.ramScaleFor64Bit") max 1 val ramScaleFor64Bit = config.getDouble("computer.lua.ramScaleFor64Bit") max 1
val maxTotalRam = config.getInt("computer.lua.maxTotalRam") max 0 val maxTotalRam = config.getInt("computer.lua.maxTotalRam") max 0

View File

@ -13,6 +13,7 @@ import li.cil.oc.api.event.NetworkActivityEvent
import li.cil.oc.client.renderer.PetRenderer import li.cil.oc.client.renderer.PetRenderer
import li.cil.oc.common.Loot import li.cil.oc.common.Loot
import li.cil.oc.common.PacketType import li.cil.oc.common.PacketType
import li.cil.oc.common.component
import li.cil.oc.common.container import li.cil.oc.common.container
import li.cil.oc.common.nanomachines.ControllerImpl import li.cil.oc.common.nanomachines.ControllerImpl
import li.cil.oc.common.tileentity._ import li.cil.oc.common.tileentity._
@ -620,6 +621,9 @@ object PacketHandler extends CommonPacketHandler {
case PacketType.TextBufferMultiViewportResolutionChange => onTextBufferMultiViewportResolutionChange(p, buffer) case PacketType.TextBufferMultiViewportResolutionChange => onTextBufferMultiViewportResolutionChange(p, buffer)
case PacketType.TextBufferMultiMaxResolutionChange => onTextBufferMultiMaxResolutionChange(p, buffer) case PacketType.TextBufferMultiMaxResolutionChange => onTextBufferMultiMaxResolutionChange(p, buffer)
case PacketType.TextBufferMultiSet => onTextBufferMultiSet(p, buffer) case PacketType.TextBufferMultiSet => onTextBufferMultiSet(p, buffer)
case PacketType.TextBufferRamInit => onTextBufferRamInit(p, buffer)
case PacketType.TextBufferBitBlt => onTextBufferBitBlt(p, buffer)
case PacketType.TextBufferRamDestroy => onTextBufferRamDestroy(p, buffer)
case PacketType.TextBufferMultiRawSetText => onTextBufferMultiRawSetText(p, buffer) case PacketType.TextBufferMultiRawSetText => onTextBufferMultiRawSetText(p, buffer)
case PacketType.TextBufferMultiRawSetBackground => onTextBufferMultiRawSetBackground(p, buffer) case PacketType.TextBufferMultiRawSetBackground => onTextBufferMultiRawSetBackground(p, buffer)
case PacketType.TextBufferMultiRawSetForeground => onTextBufferMultiRawSetForeground(p, buffer) case PacketType.TextBufferMultiRawSetForeground => onTextBufferMultiRawSetForeground(p, buffer)
@ -700,6 +704,41 @@ object PacketHandler extends CommonPacketHandler {
buffer.set(col, row, s, vertical) buffer.set(col, row, s, vertical)
} }
def onTextBufferRamInit(p: PacketParser, buffer: api.internal.TextBuffer): Unit = {
val id = p.readInt()
val nbt = p.readNBT()
buffer match {
case screen: component.traits.VideoRamAware => screen.loadBuffer(id, nbt)
case _ => // ignore
}
}
def onTextBufferBitBlt(p: PacketParser, buffer: api.internal.TextBuffer): Unit = {
val col = p.readInt()
val row = p.readInt()
val w = p.readInt()
val h = p.readInt()
val id = p.readInt()
val fromCol = p.readInt()
val fromRow = p.readInt()
component.GpuTextBuffer.bitblt(buffer, col, row, w, h, id, fromCol, fromRow)
}
def onTextBufferRamDestroy(p: PacketParser, buffer: api.internal.TextBuffer): Unit = {
val length = p.readInt()
val ids = new Array[Int](length)
for (i <- 0 until length) {
ids(i) = p.readInt()
}
buffer match {
case screen: component.traits.VideoRamAware => screen.removeBuffers(ids)
case _ => // ignore, not compatible with bitblts
}
}
def onTextBufferMultiRawSetText(p: PacketParser, buffer: api.internal.TextBuffer) { def onTextBufferMultiRawSetText(p: PacketParser, buffer: api.internal.TextBuffer) {
val col = p.readInt() val col = p.readInt()
val row = p.readInt() val row = p.readInt()

View File

@ -51,6 +51,9 @@ object PacketType extends Enumeration {
SwitchActivity, SwitchActivity,
TextBufferInit, // Goes both ways. TextBufferInit, // Goes both ways.
TextBufferMulti, TextBufferMulti,
TextBufferRamInit,
TextBufferBitBlt,
TextBufferRamDestroy,
TextBufferMultiColorChange, TextBufferMultiColorChange,
TextBufferMultiCopy, TextBufferMultiCopy,
TextBufferMultiDepthChange, TextBufferMultiDepthChange,

View File

@ -0,0 +1,154 @@
package li.cil.oc.common.component
import li.cil.oc.api.network.{ManagedEnvironment, Message, Node}
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.nbt.NBTTagCompound
import li.cil.oc.api.internal.TextBuffer.ColorDepth
import li.cil.oc.api
import li.cil.oc.common.component.traits.TextBufferProxy
import li.cil.oc.util.PackedColor
class GpuTextBuffer(val id: Int, val data: li.cil.oc.util.TextBuffer) extends ManagedEnvironment with traits.TextBufferProxy {
override def getMaximumWidth: Int = data.width
override def getMaximumHeight: Int = data.height
override def getViewportWidth: Int = data.height
override def getViewportHeight: Int = data.width
var dirty: Boolean = false
override def onBufferSet(col: Int, row: Int, s: String, vertical: Boolean): Unit = dirty = true
override def onBufferColorChange(): Unit = dirty = true
override def onBufferBitBlt(col: Int, row: Int, w: Int, h: Int, id: Int, fromCol: Int, fromRow: Int): Unit = dirty = true
override def onBufferCopy(col: Int, row: Int, w: Int, h: Int, tx: Int, ty: Int): Unit = dirty = true
override def onBufferFill(col: Int, row: Int, w: Int, h: Int, c: Char): Unit = dirty = true
override def onBufferRamInit(id: Int, ram: TextBufferProxy): Unit = dirty = false
override def setEnergyCostPerTick(value: Double): Unit = {}
override def getEnergyCostPerTick: Double = 0
override def setPowerState(value: Boolean): Unit = {}
override def getPowerState: Boolean = false
override def setMaximumResolution(width: Int, height: Int): Unit = {}
override def setAspectRatio(width: Double, height: Double): Unit = {}
override def getAspectRatio: Double = 1
override def setResolution(width: Int, height: Int): Boolean = false
override def setViewport(width: Int, height: Int): Boolean = false
override def setMaximumColorDepth(depth: ColorDepth): Unit = {}
override def getMaximumColorDepth: ColorDepth = data.format.depth
override def renderText: Boolean = false
override def renderWidth: Int = 0
override def renderHeight: Int = 0
override def setRenderingEnabled(enabled: Boolean): Unit = {}
override def isRenderingEnabled: Boolean = false
override def keyDown(character: Char, code: Int, player: EntityPlayer): Unit = {}
override def keyUp(character: Char, code: Int, player: EntityPlayer): Unit = {}
override def clipboard(value: String, player: EntityPlayer): Unit = {}
override def mouseDown(x: Double, y: Double, button: Int, player: EntityPlayer): Unit = {}
override def mouseDrag(x: Double, y: Double, button: Int, player: EntityPlayer): Unit = {}
override def mouseUp(x: Double, y: Double, button: Int, player: EntityPlayer): Unit = {}
override def mouseScroll(x: Double, y: Double, delta: Int, player: EntityPlayer): Unit = {}
override def canUpdate: Boolean = false
override def update(): Unit = {}
override def node: li.cil.oc.api.network.Node = null
override def onConnect(node: Node): Unit = {}
override def onDisconnect(node: Node): Unit = {}
override def onMessage(message: Message): Unit = {}
override def load(nbt: NBTTagCompound): Unit = {}
override def save(nbt: NBTTagCompound): Unit = {}
}
object GpuTextBuffer {
def wrap(id: Int, data: li.cil.oc.util.TextBuffer): GpuTextBuffer = new GpuTextBuffer(id, data)
def bitblt(dst: api.internal.TextBuffer, col: Int, row: Int, w: Int, h: Int, srcId: Int, fromCol: Int, fromRow: Int): Unit = {
dst match {
case screen: traits.TextBufferProxy => screen.getBuffer(srcId) match {
case Some(buffer: GpuTextBuffer) => {
bitblt(dst, col, row, w, h, buffer, fromCol, fromRow)
}
case _ => // ignore - got a bitblt for a missing buffer
}
case _ => // ignore - weird packet handler called this, should only happen for screens that know about thsi
}
}
def bitblt(dst: api.internal.TextBuffer, col: Int, row: Int, w: Int, h: Int, src: api.internal.TextBuffer, fromCol: Int, fromRow: Int): Unit = {
val x = col - 1
val y = row - 1
val fx = fromCol - 1
val fy = fromRow - 1
var adjustedDstX = x
var adjustedDstY = y
var adjustedWidth = w
var adjustedHeight = h
var adjustedSourceX = fx
var adjustedSourceY = fy
if (x < 0) {
adjustedWidth += x
adjustedSourceX -= x
adjustedDstX = 0
}
if (y < 0) {
adjustedHeight += y
adjustedSourceY -= y
adjustedDstY = 0
}
if (adjustedSourceX < 0) {
adjustedWidth += adjustedSourceX
adjustedDstX -= adjustedSourceX
adjustedSourceX = 0
}
if (adjustedSourceY < 0) {
adjustedHeight += adjustedSourceY
adjustedDstY -= adjustedSourceY
adjustedSourceY = 0
}
adjustedWidth -= ((adjustedDstX + adjustedWidth) - dst.getWidth) max 0
adjustedWidth -= ((adjustedSourceX + adjustedWidth) - src.getWidth) max 0
adjustedHeight -= ((adjustedDstY + adjustedHeight) - dst.getHeight) max 0
adjustedHeight -= ((adjustedSourceY + adjustedHeight) - src.getHeight) max 0
// anything left?
if (adjustedWidth <= 0 || adjustedHeight <= 0) {
return
}
dst match {
case dstRam: GpuTextBuffer => src match {
case srcRam: GpuTextBuffer => write_vram_to_vram(dstRam, adjustedDstX, adjustedDstY, adjustedWidth, adjustedHeight, srcRam, adjustedSourceX, adjustedSourceY)
case srcScreen: traits.TextBufferProxy => write_screen_to_vram(dstRam, adjustedDstX, adjustedDstY, adjustedWidth, adjustedHeight, srcScreen, adjustedSourceX, adjustedSourceY)
case _ => throw new UnsupportedOperationException("Source buffer does not support bitblt operations")
}
case dstScreen: traits.TextBufferProxy => src match {
case srcRam: GpuTextBuffer => write_vram_to_screen(dstScreen, adjustedDstX, adjustedDstY, adjustedWidth, adjustedHeight, srcRam, adjustedSourceX, adjustedSourceY)
case _: traits.TextBufferProxy => throw new UnsupportedOperationException("Screen to screen bitblt not supported")
case _ => throw new UnsupportedOperationException("Source buffer does not support bitblt operations")
}
case _ => throw new UnsupportedOperationException("Destination buffer does not support bitblt operations")
}
}
def write_vram_to_vram(dstRam: GpuTextBuffer, x: Int, y: Int, w: Int, h: Int, srcRam: GpuTextBuffer, fx: Int, fy: Int): Boolean = {
dstRam.data.rawcopy(x + 1, y + 1, w, h, srcRam.data, fx + 1, fx + 1)
}
def write_vram_to_screen(dstScreen: traits.TextBufferProxy, x: Int, y: Int, w: Int, h: Int, srcRam: GpuTextBuffer, fx: Int, fy: Int): Boolean = {
if (dstScreen.data.rawcopy(x + 1, y + 1, w, h, srcRam.data, fx + 1, fy + 1)) {
// rawcopy returns true only if data was modified
dstScreen.addBuffer(srcRam)
dstScreen.onBufferBitBlt(x + 1, y + 1, w, h, srcRam.id, fx + 1, fy + 1)
true
} else false
}
def write_screen_to_vram(dstRam: GpuTextBuffer, x: Int, y: Int, w: Int, h: Int, srcScreen: traits.TextBufferProxy, fx: Int, fy: Int): Boolean = {
val format: PackedColor.ColorFormat = PackedColor.Depth.format(srcScreen.getColorDepth)
val tempGpu = GpuTextBuffer.wrap(id = -1, new li.cil.oc.util.TextBuffer(w, h, format))
tempGpu.data.rawcopy(col = 1, row = 1, w, h, srcScreen.data, fx + 1, fy + 1)
write_vram_to_vram(dstRam, x, y, w, h, tempGpu, fx = 0, fy = 0)
}
}

View File

@ -22,6 +22,7 @@ import li.cil.oc.client.renderer.font.TextBufferRenderData
import li.cil.oc.client.{ComponentTracker => ClientComponentTracker} import li.cil.oc.client.{ComponentTracker => ClientComponentTracker}
import li.cil.oc.client.{PacketSender => ClientPacketSender} import li.cil.oc.client.{PacketSender => ClientPacketSender}
import li.cil.oc.common._ import li.cil.oc.common._
import li.cil.oc.common.component.traits.TextBufferProxy
import li.cil.oc.server.component.Keyboard import li.cil.oc.server.component.Keyboard
import li.cil.oc.server.{ComponentTracker => ServerComponentTracker} import li.cil.oc.server.{ComponentTracker => ServerComponentTracker}
import li.cil.oc.server.{PacketSender => ServerPacketSender} import li.cil.oc.server.{PacketSender => ServerPacketSender}
@ -34,7 +35,6 @@ import net.minecraft.entity.player.EntityPlayer
import net.minecraft.nbt.NBTTagCompound import net.minecraft.nbt.NBTTagCompound
import net.minecraftforge.event.world.ChunkEvent import net.minecraftforge.event.world.ChunkEvent
import net.minecraftforge.event.world.WorldEvent import net.minecraftforge.event.world.WorldEvent
import tconstruct.client.tabs.InventoryTabVanilla
import scala.collection.convert.WrapAsJava._ import scala.collection.convert.WrapAsJava._
import scala.collection.convert.WrapAsScala._ import scala.collection.convert.WrapAsScala._
@ -311,67 +311,28 @@ class TextBuffer(val host: EnvironmentHost) extends prefab.ManagedEnvironment wi
override def onBufferColorChange(): Unit = override def onBufferColorChange(): Unit =
proxy.onBufferColorChange() proxy.onBufferColorChange()
class ViewportBox(val x1: Int, val y1: Int, val x2: Int, val y2: Int) {
def width: Int = (x2 - x1 + 1) max 0
def height: Int = (y2 - y1 + 1) max 0
def isVisible: Boolean = width > 0 && height > 0
def isEmpty: Boolean = !isVisible
}
// return box within viewport
private def truncateToViewport(x1: Int, y1: Int, x2: Int, y2: Int): ViewportBox = {
val x: Int = (x1 min x2) max 0
val y: Int = (y1 min y2) max 0
val lastX: Int = ((x1 max x2) max 0) min (viewport._1 - 1)
val lastY: Int = ((y1 max y2) max 0) min (viewport._2 - 1)
new ViewportBox(x, y, lastX, lastY)
}
override def onBufferCopy(col: Int, row: Int, w: Int, h: Int, tx: Int, ty: Int): Unit = { override def onBufferCopy(col: Int, row: Int, w: Int, h: Int, tx: Int, ty: Int): Unit = {
// only notify about viewport changes proxy.onBufferCopy(col, row, w, h, tx, ty)
val box = truncateToViewport(col + tx, row + ty, col + tx + w - 1, row + ty + h - 1)
if (box.isVisible) {
proxy.onBufferCopy(col, row, box.width, box.height, tx, ty)
}
} }
override def onBufferFill(col: Int, row: Int, w: Int, h: Int, c: Char): Unit = { override def onBufferFill(col: Int, row: Int, w: Int, h: Int, c: Char): Unit = {
val box = truncateToViewport(col, row, col + w - 1, row + h - 1) proxy.onBufferFill(col, row, w, h, c)
if (box.isVisible) {
proxy.onBufferFill(col, row, box.width, box.height, c)
}
}
private def truncateToViewport(col: Int, row: Int, s: String, vertical: Boolean): String = {
var start: Int = 0
var last: Int = -1
if (vertical) {
val box = truncateToViewport(col, row, col, row + s.length - 1)
if (box.isVisible) {
start = box.y1 - row
last = box.y2 - row
}
} else {
val box = truncateToViewport(col, row, col + s.length - 1, row)
if (box.isVisible) {
start = box.x1 - col
last = box.x2 - col
}
}
if (last >= start && start >= 0 && last < s.length) {
s.substring(start, last + 1)
} else {
""
}
} }
override def onBufferSet(col: Int, row: Int, s: String, vertical: Boolean): Unit = { override def onBufferSet(col: Int, row: Int, s: String, vertical: Boolean): Unit = {
// only notify about viewport changes proxy.onBufferSet(col, row, s, vertical)
val truncatedString = truncateToViewport(col, row, s, vertical) }
if (!truncatedString.isEmpty) {
proxy.onBufferSet(col, row, truncatedString, vertical) override def onBufferBitBlt(col: Int, row: Int, w: Int, h: Int, id: Int, fromCol: Int, fromRow: Int): Unit = {
} proxy.onBufferBitBlt(col, row, w, h, id, fromCol, fromRow)
}
override def onBufferRamInit(id: Int, ram: TextBufferProxy): Unit = {
proxy.onBufferRamInit(id, ram)
}
override def onBufferRamDestroy(ids: Array[Int]): Unit = {
proxy.onBufferRamDestroy(ids)
} }
override def rawSetText(col: Int, row: Int, text: Array[Array[Char]]): Unit = { override def rawSetText(col: Int, row: Int, text: Array[Array[Char]]): Unit = {
@ -595,6 +556,18 @@ object TextBuffer {
owner.relativeLitArea = -1 owner.relativeLitArea = -1
} }
def onBufferBitBlt(col: Int, row: Int, w: Int, h: Int, id: Int, fromCol: Int, fromRow: Int): Unit = {
owner.relativeLitArea = -1
}
def onBufferRamInit(id: Int, ram: TextBufferProxy): Unit = {
owner.relativeLitArea = -1
}
def onBufferRamDestroy(ids: Array[Int]): Unit = {
owner.relativeLitArea = -1
}
def onBufferRawSetText(col: Int, row: Int, text: Array[Array[Char]]) { def onBufferRawSetText(col: Int, row: Int, text: Array[Array[Char]]) {
owner.relativeLitArea = -1 owner.relativeLitArea = -1
} }
@ -678,6 +651,19 @@ object TextBuffer {
dirty = true dirty = true
} }
override def onBufferBitBlt(col: Int, row: Int, w: Int, h: Int, id: Int, fromCol: Int, fromRow: Int): Unit = {
super.onBufferBitBlt(col, row, w, h, id, fromCol, fromRow)
dirty = true
}
override def onBufferRamInit(id: Int, buffer: TextBufferProxy): Unit = {
super.onBufferRamInit(id, buffer)
}
override def onBufferRamDestroy(ids: Array[Int]): Unit = {
super.onBufferRamDestroy(ids)
}
override def keyDown(character: Char, code: Int, player: EntityPlayer) { override def keyDown(character: Char, code: Int, player: EntityPlayer) {
debug(s"{type = keyDown, char = $character, code = $code}") debug(s"{type = keyDown, char = $character, code = $code}")
ClientPacketSender.sendKeyDown(nodeAddress, character, code) ClientPacketSender.sendKeyDown(nodeAddress, character, code)
@ -780,6 +766,26 @@ object TextBuffer {
owner.synchronized(ServerPacketSender.appendTextBufferSet(owner.pendingCommands, col, row, s, vertical)) owner.synchronized(ServerPacketSender.appendTextBufferSet(owner.pendingCommands, col, row, s, vertical))
} }
override def onBufferBitBlt(col: Int, row: Int, w: Int, h: Int, id: Int, fromCol: Int, fromRow: Int): Unit = {
super.onBufferBitBlt(col, row, w, h, id, fromCol, fromRow)
owner.host.markChanged()
owner.synchronized(ServerPacketSender.appendTextBufferBitBlt(owner.pendingCommands, col, row, w, h, id, fromCol, fromRow))
}
override def onBufferRamInit(id: Int, buffer: TextBufferProxy): Unit = {
super.onBufferRamInit(id, buffer)
owner.host.markChanged()
val nbt = new NBTTagCompound()
buffer.data.save(nbt)
owner.synchronized(ServerPacketSender.appendTextBufferRamInit(owner.pendingCommands, id, nbt))
}
override def onBufferRamDestroy(ids: Array[Int]): Unit = {
super.onBufferRamDestroy(ids)
owner.host.markChanged()
owner.synchronized(ServerPacketSender.appendTextBufferRamDestroy(owner.pendingCommands, ids))
}
override def onBufferRawSetText(col: Int, row: Int, text: Array[Array[Char]]) { override def onBufferRawSetText(col: Int, row: Int, text: Array[Array[Char]]) {
super.onBufferRawSetText(col, row, text) super.onBufferRawSetText(col, row, text)
owner.host.markChanged() owner.host.markChanged()

View File

@ -5,7 +5,7 @@ import li.cil.oc.api
import li.cil.oc.api.internal.TextBuffer import li.cil.oc.api.internal.TextBuffer
import li.cil.oc.util.PackedColor import li.cil.oc.util.PackedColor
trait TextBufferProxy extends api.internal.TextBuffer { trait TextBufferProxy extends api.internal.TextBuffer with VideoRamAware {
def data: util.TextBuffer def data: util.TextBuffer
override def getWidth: Int = data.width override def getWidth: Int = data.width

View File

@ -0,0 +1,72 @@
package li.cil.oc.common.component.traits
import li.cil.oc.common.component
import net.minecraft.nbt.NBTTagCompound
trait VideoRamAware {
private val internalBuffers = new scala.collection.mutable.HashMap[Int, component.GpuTextBuffer]
val RESERVED_SCREEN_INDEX: Int = 0
def onBufferBitBlt(col: Int, row: Int, w: Int, h: Int, id: Int, fromCol: Int, fromRow: Int): Unit = {}
def onBufferRamInit(id: Int, ram: TextBufferProxy): Unit = {}
def onBufferRamDestroy(ids: Array[Int]): Unit = {}
def bufferIndexes(): Array[Int] = internalBuffers.collect {
case (index: Int, _: Any) => index
}.toArray
def addBuffer(buffer: component.GpuTextBuffer): Boolean = {
val preexists = internalBuffers.contains(buffer.id)
if (!preexists) {
internalBuffers += buffer.id -> buffer
}
if (!preexists || buffer.dirty) {
buffer.onBufferRamInit(buffer.id, buffer)
onBufferRamInit(buffer.id, buffer)
}
preexists
}
def removeBuffers(ids: Array[Int]): Boolean = {
var allRemoved: Boolean = true
if (ids.nonEmpty) {
onBufferRamDestroy(ids)
for (id <- ids) {
if (internalBuffers.remove(id).isEmpty)
allRemoved = false
}
}
allRemoved
}
def removeAllBuffers(): Boolean = removeBuffers(bufferIndexes())
def loadBuffer(id: Int, nbt: NBTTagCompound): Unit = {
val src = new li.cil.oc.util.TextBuffer(width = 1, height = 1, li.cil.oc.util.PackedColor.SingleBitFormat)
src.load(nbt)
addBuffer(component.GpuTextBuffer.wrap(id, src))
}
def getBuffer(id: Int): Option[component.GpuTextBuffer] = {
if (internalBuffers.contains(id))
Option(internalBuffers(id))
else
None
}
def nextAvailableBufferIndex: Int = {
var index = RESERVED_SCREEN_INDEX + 1
while (internalBuffers.contains(index)) {
index += 1;
}
index
}
def calculateUsedMemory(): Int = {
var sum: Int = 0
for ((_, buffer: component.GpuTextBuffer) <- internalBuffers) {
sum += buffer.data.width * buffer.data.height
}
sum
}
}

View File

@ -665,6 +665,33 @@ object PacketSender {
pb.writeBoolean(vertical) pb.writeBoolean(vertical)
} }
def appendTextBufferBitBlt(pb: PacketBuilder, col: Int, row: Int, w: Int, h: Int, id: Int, fromCol: Int, fromRow: Int): Unit = {
pb.writePacketType(PacketType.TextBufferBitBlt)
pb.writeInt(col)
pb.writeInt(row)
pb.writeInt(w)
pb.writeInt(h)
pb.writeInt(id)
pb.writeInt(fromCol)
pb.writeInt(fromRow)
}
def appendTextBufferRamInit(pb: PacketBuilder, id: Int, nbt: NBTTagCompound): Unit = {
pb.writePacketType(PacketType.TextBufferRamInit)
pb.writeInt(id)
pb.writeNBT(nbt)
}
def appendTextBufferRamDestroy(pb: PacketBuilder, ids: Array[Int]): Unit = {
pb.writePacketType(PacketType.TextBufferRamDestroy)
pb.writeInt(ids.length)
for (idx <- ids) {
pb.writeInt(idx)
}
}
def appendTextBufferRawSetText(pb: PacketBuilder, col: Int, row: Int, text: Array[Array[Char]]) { def appendTextBufferRawSetText(pb: PacketBuilder, col: Int, row: Int, text: Array[Array[Char]]) {
pb.writePacketType(PacketType.TextBufferMultiRawSetText) pb.writePacketType(PacketType.TextBufferMultiRawSetText)

View File

@ -2,10 +2,7 @@ package li.cil.oc.server.component
import java.util import java.util
import li.cil.oc.Constants import li.cil.oc.{Constants, Localization, Settings, api}
import li.cil.oc.Localization
import li.cil.oc.Settings
import li.cil.oc.api
import li.cil.oc.api.Network import li.cil.oc.api.Network
import li.cil.oc.api.driver.DeviceInfo import li.cil.oc.api.driver.DeviceInfo
import li.cil.oc.api.driver.DeviceInfo.DeviceAttribute import li.cil.oc.api.driver.DeviceInfo.DeviceAttribute
@ -16,7 +13,9 @@ import li.cil.oc.api.machine.Context
import li.cil.oc.api.network._ import li.cil.oc.api.network._
import li.cil.oc.api.prefab import li.cil.oc.api.prefab
import li.cil.oc.util.PackedColor import li.cil.oc.util.PackedColor
import net.minecraft.nbt.NBTTagCompound import net.minecraft.nbt.{NBTTagCompound, NBTTagList}
import li.cil.oc.common.component
import li.cil.oc.common.component.GpuTextBuffer
import scala.collection.convert.WrapAsJava._ import scala.collection.convert.WrapAsJava._
import scala.util.matching.Regex import scala.util.matching.Regex
@ -33,7 +32,7 @@ import scala.util.matching.Regex
// saved, but before the computer was saved, leading to mismatching states in // saved, but before the computer was saved, leading to mismatching states in
// the save file - a Bad Thing (TM). // the save file - a Bad Thing (TM).
class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceInfo { class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceInfo with component.traits.VideoRamAware {
override val node = Network.newNode(this, Visibility.Neighbors). override val node = Network.newNode(this, Visibility.Neighbors).
withComponent("gpu"). withComponent("gpu").
withConnector(). withConnector().
@ -47,17 +46,32 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI
private var screenInstance: Option[api.internal.TextBuffer] = None private var screenInstance: Option[api.internal.TextBuffer] = None
private def screen(f: (api.internal.TextBuffer) => Array[AnyRef]) = screenInstance match { private var bufferIndex: Int = RESERVED_SCREEN_INDEX // screen is index zero
case Some(screen) => screen.synchronized(f(screen))
case _ => Array(Unit, "no screen") private def screen(index: Int, f: (api.internal.TextBuffer) => Array[AnyRef]): Array[AnyRef] = {
if (index == RESERVED_SCREEN_INDEX) {
screenInstance match {
case Some(screen) => screen.synchronized(f(screen))
case _ => Array(Unit, "no screen")
}
} else {
getBuffer(index) match {
case Some(buffer: api.internal.TextBuffer) => f(buffer)
case _ => Array(Unit, "invalid buffer index")
}
}
} }
private def screen(f: (api.internal.TextBuffer) => Array[AnyRef]): Array[AnyRef] = screen(bufferIndex, f)
final val setBackgroundCosts = Array(1.0 / 32, 1.0 / 64, 1.0 / 128) final val setBackgroundCosts = Array(1.0 / 32, 1.0 / 64, 1.0 / 128)
final val setForegroundCosts = Array(1.0 / 32, 1.0 / 64, 1.0 / 128) final val setForegroundCosts = Array(1.0 / 32, 1.0 / 64, 1.0 / 128)
final val setPaletteColorCosts = Array(1.0 / 2, 1.0 / 8, 1.0 / 16) final val setPaletteColorCosts = Array(1.0 / 2, 1.0 / 8, 1.0 / 16)
final val setCosts = Array(1.0 / 64, 1.0 / 128, 1.0 / 256) final val setCosts = Array(1.0 / 64, 1.0 / 128, 1.0 / 256)
final val copyCosts = Array(1.0 / 16, 1.0 / 32, 1.0 / 64) final val copyCosts = Array(1.0 / 16, 1.0 / 32, 1.0 / 64)
final val fillCosts = Array(1.0 / 32, 1.0 / 64, 1.0 / 128) final val fillCosts = Array(1.0 / 32, 1.0 / 64, 1.0 / 128)
final val bitbltCosts = Array(2.0, 1.0, 1.0 / 2.0)
final val totalVRAM: Int = (maxResolution._1 * maxResolution._2) * Settings.get.vramSizes(0 max tier min Settings.get.vramSizes.length)
// ----------------------------------------------------------------------- // // ----------------------------------------------------------------------- //
@ -81,30 +95,134 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI
// ----------------------------------------------------------------------- // // ----------------------------------------------------------------------- //
private def getViewportOverlapSize(s: api.internal.TextBuffer, x1: Int, y1: Int, x2: Int, y2: Int): Int = { private def consumeViewportPower(buffer: api.internal.TextBuffer, context: Context, budgetCost: Double, units: Int, factor: Double): Boolean = {
val width = s.getViewportWidth buffer match {
val height = s.getViewportHeight case _: component.GpuTextBuffer => true
val left = math.min(x1, x2); case _ =>
val right = math.max(x1, x2); context.consumeCallBudget(budgetCost)
val top = math.min(y1, y2); consumePower(units, factor)
val bottom = math.max(y1, y2); }
if (right < 0 || left >= width || top >= height || bottom < 0)
return 0
val box_left = math.max(0, left)
val box_right = math.min(width - 1, right)
val box_top = math.max(0, top)
val box_bottom = math.min(height - 1, bottom)
(box_right - box_left + 1) * (box_bottom - box_top + 1)
} }
private def consumeViewportPower(overlap: Int, context: Context, budgetCost: Double, callFactor: Double): Boolean = { @Callback(direct = true, doc = """function(): number -- returns the index of the currently selected buffer. 0 is reserved for the screen. Can return 0 even when there is no screen""")
if (overlap == 0) true def getBuffer(context: Context, args: Arguments): Array[AnyRef] = {
else { result(bufferIndex)
context.consumeCallBudget(budgetCost) }
consumePower(overlap, callFactor)
@Callback(direct = true, doc = """function(index: number): number -- Sets the active buffer to `index`. 1 is the first vram buffer and 0 is reserved for the screen. returns nil for invalid index (0 is always valid)""")
def setBuffer(context: Context, args: Arguments): Array[AnyRef] = {
val previousIndex: Int = bufferIndex
val newIndex: Int = args.checkInteger(0)
if (newIndex != RESERVED_SCREEN_INDEX && getBuffer(newIndex).isEmpty) {
result(Unit, "invalid buffer index")
} else {
bufferIndex = newIndex
if (bufferIndex == RESERVED_SCREEN_INDEX) {
screen(s => result(true))
}
result(previousIndex)
} }
} }
@Callback(direct = true, doc = """function(): number -- Returns an array of indexes of the allocated buffers""")
def buffers(context: Context, args: Arguments): Array[AnyRef] = {
result(bufferIndexes())
}
@Callback(direct = true, doc = """function([width: number, height: number]): number -- allocates a new buffer with dimensions width*height (defaults to max resolution) and appends it to the buffer list. Returns the index of the new buffer and returns nil with an error message on failure. A buffer can be allocated even when there is no screen bound to this gpu. Index 0 is always reserved for the screen and thus the lowest index of an allocated buffer is always 1.""")
def allocateBuffer(context: Context, args: Arguments): Array[AnyRef] = {
val width: Int = args.optInteger(0, maxResolution._1)
val height: Int = args.optInteger(1, maxResolution._2)
val size: Int = width * height
if (width <= 0 || height <= 0) {
result(Unit, "invalid page dimensions: must be greater than zero")
}
else if (size > (totalVRAM - calculateUsedMemory)) {
result(Unit, "not enough video memory")
} else {
val format: PackedColor.ColorFormat = PackedColor.Depth.format(Settings.screenDepthsByTier(tier))
val buffer = new li.cil.oc.util.TextBuffer(width, height, format)
val page = component.GpuTextBuffer.wrap(nextAvailableBufferIndex, buffer)
addBuffer(page)
result(page.id)
}
}
// this event occurs when the gpu is told a page was removed - we need to notify the screen of this
// we do this because the VideoRamAware trait only notifies itself, it doesn't assume there is a screen
override def onBufferRamDestroy(ids: Array[Int]): Unit = {
// first protect our buffer index - it needs to fall back to the screen if its buffer was removed
if (ids.contains(bufferIndex)) {
bufferIndex = RESERVED_SCREEN_INDEX
}
if (ids.nonEmpty) {
screen(RESERVED_SCREEN_INDEX, s => s match {
case oc: component.traits.VideoRamAware => result(oc.removeBuffers(ids))
case _ => result(true)// addon mod screen type that is not video ram aware
})
} else result(true)
}
@Callback(direct = true, doc = """function(index: number): boolean -- Closes buffer at `index`. Returns true if a buffer closed. If the current buffer is closed, index moves to 0""")
def freeBuffer(context: Context, args: Arguments): Array[AnyRef] = {
val index: Int = args.checkInteger(0)
if (removeBuffers(Array(index))) result(true)
else result(Unit, "no buffer at index")
}
@Callback(direct = true, doc = """function(): number -- Closes all buffers and returns true on success. If the active buffer is closed, index moves to 0""")
def freeAllBuffers(context: Context, args: Arguments): Array[AnyRef] = result(removeAllBuffers())
@Callback(direct = true, doc = """function(): number -- returns the total memory size of the gpu vram. This does not include the screen.""")
def totalMemory(context: Context, args: Arguments): Array[AnyRef] = {
result(totalVRAM)
}
@Callback(direct = true, doc = """function(): number -- returns the total free memory not allocated to buffers. This does not include the screen.""")
def freeMemory(context: Context, args: Arguments): Array[AnyRef] = {
result(totalVRAM - calculateUsedMemory)
}
@Callback(direct = true, doc = """function(index: number): number, number -- returns the buffer size at index. Returns the screen resolution for index 0. returns nil for invalid indexes""")
def getBufferSize(context: Context, args: Arguments): Array[AnyRef] = {
val idx = args.checkInteger(0)
screen(idx, s => result(s.getWidth, s.getHeight))
}
@Callback(direct = true, doc = """function([dst: number, col: number, row: number, width: number, height: number, src: number, fromCol: number, fromRow: number]):boolean -- bitblt from buffer to screen. All parameters are optional. Writes to `dst` page in rectangle `x, y, width, height`, defaults to the bound screen and its viewport. Reads data from `src` page at `fx, fy`, default is the active page from position 1, 1""")
def bitblt(context: Context, args: Arguments): Array[AnyRef] = {
val dstIdx = args.optInteger(0, RESERVED_SCREEN_INDEX)
screen(dstIdx, dst => {
val col = args.optInteger(1, 1)
val row = args.optInteger(2, 1)
val w = args.optInteger(3, dst.getWidth)
val h = args.optInteger(4, dst.getHeight)
val srcIdx = args.optInteger(5, bufferIndex)
screen(srcIdx, src => {
val fromCol = args.optInteger(6, 1)
val fromRow = args.optInteger(7, 1)
// if src is vram and dirty, bltbit cost is large
val dirtyPage = src match {
case vram: GpuTextBuffer => vram.dirty
case _ => false
}
if (consumeViewportPower(dst, context, if (dirtyPage) bitbltCosts(tier) else setCosts(tier), w * h, Settings.get.gpuCopyCost)) {
if (dstIdx == srcIdx) {
val tx = col - fromCol
val ty = row - fromRow
dst.copy(col, row, w, h, tx, ty)
result(true)
} else {
// at least one of the two buffers is a gpu buffer
component.GpuTextBuffer.bitblt(dst, col, row, w, h, src, fromRow, fromCol)
result(true)
}
} else result(Unit, "not enough energy")
})
})
}
@Callback(doc = """function(address:string[, reset:boolean=true]):boolean -- Binds the GPU to the screen with the specified address and resets screen settings if `reset` is true.""") @Callback(doc = """function(address:string[, reset:boolean=true]):boolean -- Binds the GPU to the screen with the specified address and resets screen settings if `reset` is true.""")
def bind(context: Context, args: Arguments): Array[AnyRef] = { def bind(context: Context, args: Arguments): Array[AnyRef] = {
val address = args.checkString(0) val address = args.checkString(0)
@ -123,6 +241,10 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI
s.setColorDepth(api.internal.TextBuffer.ColorDepth.values.apply(math.min(maxDepth.ordinal, s.getMaximumColorDepth.ordinal))) s.setColorDepth(api.internal.TextBuffer.ColorDepth.values.apply(math.min(maxDepth.ordinal, s.getMaximumColorDepth.ordinal)))
s.setForegroundColor(0xFFFFFF) s.setForegroundColor(0xFFFFFF)
s.setBackgroundColor(0x000000) s.setBackgroundColor(0x000000)
s match {
case oc: component.traits.VideoRamAware => oc.removeAllBuffers()
case _ =>
}
} }
else context.pause(0) // To discourage outputting "in realtime" to multiple screens using one GPU. else context.pause(0) // To discourage outputting "in realtime" to multiple screens using one GPU.
result(true) result(true)
@ -132,7 +254,13 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI
} }
@Callback(direct = true, doc = """function():string -- Get the address of the screen the GPU is currently bound to.""") @Callback(direct = true, doc = """function():string -- Get the address of the screen the GPU is currently bound to.""")
def getScreen(context: Context, args: Arguments): Array[AnyRef] = screen(s => result(s.node.address)) def getScreen(context: Context, args: Arguments): Array[AnyRef] = {
if (bufferIndex == RESERVED_SCREEN_INDEX) {
screen(s => result(s.node.address))
} else {
result(Unit, "the current text buffer is video ram")
}
}
@Callback(direct = true, doc = """function():number, boolean -- Get the current background color and whether it's from the palette or not.""") @Callback(direct = true, doc = """function():number, boolean -- Get the current background color and whether it's from the palette or not.""")
def getBackground(context: Context, args: Arguments): Array[AnyRef] = def getBackground(context: Context, args: Arguments): Array[AnyRef] =
@ -307,8 +435,7 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI
screen(s => { screen(s => {
val x2 = if (vertical) x else x + value.length - 1 val x2 = if (vertical) x else x + value.length - 1
val y2 = if (!vertical) y else y + value.length - 1 val y2 = if (!vertical) y else y + value.length - 1
val overlap: Int = getViewportOverlapSize(s, x, y, x2, y2) if (consumeViewportPower(s, context, setCosts(tier), value.length, Settings.get.gpuSetCost)) {
if (consumeViewportPower(overlap, context, setCosts(tier), Settings.get.gpuSetCost)) {
s.set(x, y, value, vertical) s.set(x, y, value, vertical)
result(true) result(true)
} }
@ -325,8 +452,7 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI
val tx = args.checkInteger(4) val tx = args.checkInteger(4)
val ty = args.checkInteger(5) val ty = args.checkInteger(5)
screen(s => { screen(s => {
val overlap: Int = getViewportOverlapSize(s, x + tx, y + ty, x + tx + w - 1, y + ty + h - 1) if (consumeViewportPower(s, context, copyCosts(tier), w * h, Settings.get.gpuCopyCost)) {
if (consumeViewportPower(overlap, context, copyCosts(tier), Settings.get.gpuCopyCost)) {
s.copy(x, y, w, h, tx, ty) s.copy(x, y, w, h, tx, ty)
result(true) result(true)
} }
@ -336,7 +462,6 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI
@Callback(direct = true, doc = """function(x:number, y:number, width:number, height:number, char:string):boolean -- Fills a portion of the screen at the specified position with the specified size with the specified character.""") @Callback(direct = true, doc = """function(x:number, y:number, width:number, height:number, char:string):boolean -- Fills a portion of the screen at the specified position with the specified size with the specified character.""")
def fill(context: Context, args: Arguments): Array[AnyRef] = { def fill(context: Context, args: Arguments): Array[AnyRef] = {
context.consumeCallBudget(fillCosts(tier))
val x = args.checkInteger(0) - 1 val x = args.checkInteger(0) - 1
val y = args.checkInteger(1) - 1 val y = args.checkInteger(1) - 1
val w = math.max(0, args.checkInteger(2)) val w = math.max(0, args.checkInteger(2))
@ -345,8 +470,7 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI
if (value.length == 1) screen(s => { if (value.length == 1) screen(s => {
val c = value.charAt(0) val c = value.charAt(0)
val cost = if (c == ' ') Settings.get.gpuClearCost else Settings.get.gpuFillCost val cost = if (c == ' ') Settings.get.gpuClearCost else Settings.get.gpuFillCost
val overlap: Int = getViewportOverlapSize(s, x, y, x + w - 1, y + h - 1) if (consumeViewportPower(s, context, fillCosts(tier), w * h, cost)) {
if (consumeViewportPower(overlap, context, fillCosts(tier), cost)) {
s.fill(x, y, w, h, value.charAt(0)) s.fill(x, y, w, h, value.charAt(0))
result(true) result(true)
} }
@ -363,6 +487,13 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI
override def onMessage(message: Message) { override def onMessage(message: Message) {
super.onMessage(message) super.onMessage(message)
if (node.isNeighborOf(message.source)) {
if (message.name == "computer.stopped" || message.name == "computer.started") {
bufferIndex = RESERVED_SCREEN_INDEX
removeAllBuffers()
}
}
if (message.name == "computer.stopped" && node.isNeighborOf(message.source)) { if (message.name == "computer.stopped" && node.isNeighborOf(message.source)) {
screen(s => { screen(s => {
val (gmw, gmh) = maxResolution val (gmw, gmh) = maxResolution
@ -426,23 +557,69 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI
// ----------------------------------------------------------------------- // // ----------------------------------------------------------------------- //
private val SCREEN_KEY: String = "screen"
private val BUFFER_INDEX_KEY: String = "bufferIndex"
private val VIDEO_RAM_KEY: String = "videoRam"
private final val NBT_PAGES: String = "pages"
private final val NBT_PAGE_IDX: String = "page_idx"
private final val NBT_PAGE_DATA: String = "page_data"
private val COMPOUND_ID = (new NBTTagCompound).getId
override def load(nbt: NBTTagCompound) { override def load(nbt: NBTTagCompound) {
super.load(nbt) super.load(nbt)
if (nbt.hasKey("screen")) { if (nbt.hasKey(SCREEN_KEY)) {
nbt.getString("screen") match { nbt.getString(SCREEN_KEY) match {
case screen: String if !screen.isEmpty => screenAddress = Some(screen) case screen: String if !screen.isEmpty => screenAddress = Some(screen)
case _ => screenAddress = None case _ => screenAddress = None
} }
screenInstance = None screenInstance = None
} }
if (nbt.hasKey(BUFFER_INDEX_KEY)) {
bufferIndex = nbt.getInteger(BUFFER_INDEX_KEY)
}
removeAllBuffers() // JUST in case
if (nbt.hasKey(VIDEO_RAM_KEY)) {
val videoRamNbt = nbt.getCompoundTag(VIDEO_RAM_KEY)
val nbtPages = videoRamNbt.getTagList(NBT_PAGES, COMPOUND_ID)
for (i <- 0 until nbtPages.tagCount) {
val nbtPage = nbtPages.getCompoundTagAt(i)
val idx: Int = nbtPage.getInteger(NBT_PAGE_IDX)
val data = nbtPage.getCompoundTag(NBT_PAGE_DATA)
loadBuffer(idx, data)
}
}
} }
override def save(nbt: NBTTagCompound) { override def save(nbt: NBTTagCompound) {
super.save(nbt) super.save(nbt)
if (screenAddress.isDefined) { if (screenAddress.isDefined) {
nbt.setString("screen", screenAddress.get) nbt.setString(SCREEN_KEY, screenAddress.get)
} }
nbt.setInteger(BUFFER_INDEX_KEY, bufferIndex)
val videoRamNbt = new NBTTagCompound
val nbtPages = new NBTTagList
val indexes = bufferIndexes()
for (idx: Int <- indexes) {
getBuffer(idx) match {
case Some(page) => {
val nbtPage = new NBTTagCompound
nbtPage.setInteger(NBT_PAGE_IDX, idx)
val data = new NBTTagCompound
page.data.save(data)
nbtPage.setTag(NBT_PAGE_DATA, data)
nbtPages.appendTag(nbtPage)
}
case _ => // ignore
}
}
videoRamNbt.setTag(NBT_PAGES, nbtPages)
nbt.setTag(VIDEO_RAM_KEY, videoRamNbt)
} }
} }

View File

@ -205,6 +205,28 @@ class TextBuffer(var width: Int, var height: Int, initialFormat: PackedColor.Col
changed changed
} }
// copy a portion of another buffer into this buffer
def rawcopy(col: Int, row: Int, w: Int, h: Int, src: TextBuffer, fromCol: Int, fromRow: Int): Boolean = {
var changed: Boolean = false
val col_index = col - 1
val row_index = row - 1
for (yOffset <- 0 until h) {
val dstCharLine = buffer(row_index + yOffset)
val dstColorLine = color(row_index + yOffset)
for (xOffset <- 0 until w) {
val srcChar = src.buffer(fromRow + yOffset - 1)(fromCol + xOffset - 1)
val srcColor = src.color(fromRow + yOffset - 1)(fromCol + xOffset - 1)
if (srcChar != dstCharLine(col_index + xOffset) || srcColor != dstColorLine(col_index + xOffset)) {
changed = true
dstCharLine(col_index + xOffset) = srcChar
dstColorLine(col_index + xOffset) = srcColor
}
}
}
changed
}
private def setChar(line: Array[Char], lineColor: Array[Short], x: Int, c: Char) { private def setChar(line: Array[Char], lineColor: Array[Short], x: Int, c: Char) {
if (FontUtils.wcwidth(c) > 1 && x >= line.length - 1) { if (FontUtils.wcwidth(c) > 1 && x >= line.length - 1) {
// Don't allow setting wide chars in right-most col. // Don't allow setting wide chars in right-most col.