diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index ed82cf87a..6a0337422 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -244,6 +244,16 @@ opencomputers { 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 # 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 diff --git a/src/main/scala/li/cil/oc/Settings.scala b/src/main/scala/li/cil/oc/Settings.scala index bf72e01b8..51742fbbd 100644 --- a/src/main/scala/li/cil/oc/Settings.scala +++ b/src/main/scala/li/cil/oc/Settings.scala @@ -93,6 +93,12 @@ class Settings(val config: Config) { OpenComputers.log.warn("Bad number of RAM sizes, ignoring.") 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 maxTotalRam = config.getInt("computer.lua.maxTotalRam") max 0 diff --git a/src/main/scala/li/cil/oc/client/PacketHandler.scala b/src/main/scala/li/cil/oc/client/PacketHandler.scala index 4f6d19d97..1e8f70e13 100644 --- a/src/main/scala/li/cil/oc/client/PacketHandler.scala +++ b/src/main/scala/li/cil/oc/client/PacketHandler.scala @@ -13,6 +13,7 @@ import li.cil.oc.api.event.NetworkActivityEvent import li.cil.oc.client.renderer.PetRenderer import li.cil.oc.common.Loot import li.cil.oc.common.PacketType +import li.cil.oc.common.component import li.cil.oc.common.container import li.cil.oc.common.nanomachines.ControllerImpl import li.cil.oc.common.tileentity._ @@ -620,6 +621,9 @@ object PacketHandler extends CommonPacketHandler { case PacketType.TextBufferMultiViewportResolutionChange => onTextBufferMultiViewportResolutionChange(p, buffer) case PacketType.TextBufferMultiMaxResolutionChange => onTextBufferMultiMaxResolutionChange(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.TextBufferMultiRawSetBackground => onTextBufferMultiRawSetBackground(p, buffer) case PacketType.TextBufferMultiRawSetForeground => onTextBufferMultiRawSetForeground(p, buffer) @@ -700,6 +704,41 @@ object PacketHandler extends CommonPacketHandler { 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) { val col = p.readInt() val row = p.readInt() diff --git a/src/main/scala/li/cil/oc/common/PacketType.scala b/src/main/scala/li/cil/oc/common/PacketType.scala index 4a260d41a..c665fe5f9 100644 --- a/src/main/scala/li/cil/oc/common/PacketType.scala +++ b/src/main/scala/li/cil/oc/common/PacketType.scala @@ -51,6 +51,9 @@ object PacketType extends Enumeration { SwitchActivity, TextBufferInit, // Goes both ways. TextBufferMulti, + TextBufferRamInit, + TextBufferBitBlt, + TextBufferRamDestroy, TextBufferMultiColorChange, TextBufferMultiCopy, TextBufferMultiDepthChange, diff --git a/src/main/scala/li/cil/oc/common/component/GpuTextBuffer.scala b/src/main/scala/li/cil/oc/common/component/GpuTextBuffer.scala new file mode 100644 index 000000000..7acbd49fa --- /dev/null +++ b/src/main/scala/li/cil/oc/common/component/GpuTextBuffer.scala @@ -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) + } +} diff --git a/src/main/scala/li/cil/oc/common/component/TextBuffer.scala b/src/main/scala/li/cil/oc/common/component/TextBuffer.scala index dcd01663b..755aa3563 100644 --- a/src/main/scala/li/cil/oc/common/component/TextBuffer.scala +++ b/src/main/scala/li/cil/oc/common/component/TextBuffer.scala @@ -22,6 +22,7 @@ import li.cil.oc.client.renderer.font.TextBufferRenderData import li.cil.oc.client.{ComponentTracker => ClientComponentTracker} import li.cil.oc.client.{PacketSender => ClientPacketSender} 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.{ComponentTracker => ServerComponentTracker} import li.cil.oc.server.{PacketSender => ServerPacketSender} @@ -34,7 +35,6 @@ import net.minecraft.entity.player.EntityPlayer import net.minecraft.nbt.NBTTagCompound import net.minecraftforge.event.world.ChunkEvent import net.minecraftforge.event.world.WorldEvent -import tconstruct.client.tabs.InventoryTabVanilla import scala.collection.convert.WrapAsJava._ import scala.collection.convert.WrapAsScala._ @@ -311,67 +311,28 @@ class TextBuffer(val host: EnvironmentHost) extends prefab.ManagedEnvironment wi override def onBufferColorChange(): Unit = 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 = { - // only notify about viewport changes - 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) - } + proxy.onBufferCopy(col, row, w, h, tx, ty) } 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) - 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 { - "" - } + proxy.onBufferFill(col, row, w, h, c) } override def onBufferSet(col: Int, row: Int, s: String, vertical: Boolean): Unit = { - // only notify about viewport changes - val truncatedString = truncateToViewport(col, row, s, vertical) - if (!truncatedString.isEmpty) { - proxy.onBufferSet(col, row, truncatedString, vertical) - } + proxy.onBufferSet(col, row, s, 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 = { @@ -595,6 +556,18 @@ object TextBuffer { 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]]) { owner.relativeLitArea = -1 } @@ -678,6 +651,19 @@ object TextBuffer { 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) { debug(s"{type = keyDown, char = $character, code = $code}") ClientPacketSender.sendKeyDown(nodeAddress, character, code) @@ -780,6 +766,26 @@ object TextBuffer { 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]]) { super.onBufferRawSetText(col, row, text) owner.host.markChanged() diff --git a/src/main/scala/li/cil/oc/common/component/traits/TextBufferProxy.scala b/src/main/scala/li/cil/oc/common/component/traits/TextBufferProxy.scala index ae55093ff..082670998 100644 --- a/src/main/scala/li/cil/oc/common/component/traits/TextBufferProxy.scala +++ b/src/main/scala/li/cil/oc/common/component/traits/TextBufferProxy.scala @@ -5,7 +5,7 @@ import li.cil.oc.api import li.cil.oc.api.internal.TextBuffer import li.cil.oc.util.PackedColor -trait TextBufferProxy extends api.internal.TextBuffer { +trait TextBufferProxy extends api.internal.TextBuffer with VideoRamAware { def data: util.TextBuffer override def getWidth: Int = data.width diff --git a/src/main/scala/li/cil/oc/common/component/traits/VideoRamAware.scala b/src/main/scala/li/cil/oc/common/component/traits/VideoRamAware.scala new file mode 100644 index 000000000..124280e01 --- /dev/null +++ b/src/main/scala/li/cil/oc/common/component/traits/VideoRamAware.scala @@ -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 + } +} diff --git a/src/main/scala/li/cil/oc/server/PacketSender.scala b/src/main/scala/li/cil/oc/server/PacketSender.scala index 1d9279ffe..8036e115e 100644 --- a/src/main/scala/li/cil/oc/server/PacketSender.scala +++ b/src/main/scala/li/cil/oc/server/PacketSender.scala @@ -665,6 +665,33 @@ object PacketSender { 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]]) { pb.writePacketType(PacketType.TextBufferMultiRawSetText) diff --git a/src/main/scala/li/cil/oc/server/component/GraphicsCard.scala b/src/main/scala/li/cil/oc/server/component/GraphicsCard.scala index edc7ce2dd..f2c6a1b06 100644 --- a/src/main/scala/li/cil/oc/server/component/GraphicsCard.scala +++ b/src/main/scala/li/cil/oc/server/component/GraphicsCard.scala @@ -2,10 +2,7 @@ package li.cil.oc.server.component import java.util -import li.cil.oc.Constants -import li.cil.oc.Localization -import li.cil.oc.Settings -import li.cil.oc.api +import li.cil.oc.{Constants, Localization, Settings, api} import li.cil.oc.api.Network import li.cil.oc.api.driver.DeviceInfo 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.prefab 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.util.matching.Regex @@ -33,7 +32,7 @@ import scala.util.matching.Regex // saved, but before the computer was saved, leading to mismatching states in // 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). withComponent("gpu"). withConnector(). @@ -47,17 +46,32 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI private var screenInstance: Option[api.internal.TextBuffer] = None - private def screen(f: (api.internal.TextBuffer) => Array[AnyRef]) = screenInstance match { - case Some(screen) => screen.synchronized(f(screen)) - case _ => Array(Unit, "no screen") + private var bufferIndex: Int = RESERVED_SCREEN_INDEX // screen is index zero + + 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 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 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 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 = { - val width = s.getViewportWidth - val height = s.getViewportHeight - val left = math.min(x1, x2); - val right = math.max(x1, x2); - val top = math.min(y1, y2); - 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(buffer: api.internal.TextBuffer, context: Context, budgetCost: Double, units: Int, factor: Double): Boolean = { + buffer match { + case _: component.GpuTextBuffer => true + case _ => + context.consumeCallBudget(budgetCost) + consumePower(units, factor) + } } - private def consumeViewportPower(overlap: Int, context: Context, budgetCost: Double, callFactor: Double): Boolean = { - if (overlap == 0) true - else { - context.consumeCallBudget(budgetCost) - consumePower(overlap, callFactor) + @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""") + def getBuffer(context: Context, args: Arguments): Array[AnyRef] = { + result(bufferIndex) + } + + @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.""") def bind(context: Context, args: Arguments): Array[AnyRef] = { 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.setForegroundColor(0xFFFFFF) 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. 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.""") - 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.""") def getBackground(context: Context, args: Arguments): Array[AnyRef] = @@ -307,8 +435,7 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI screen(s => { val x2 = if (vertical) x else x + value.length - 1 val y2 = if (!vertical) y else y + value.length - 1 - val overlap: Int = getViewportOverlapSize(s, x, y, x2, y2) - if (consumeViewportPower(overlap, context, setCosts(tier), Settings.get.gpuSetCost)) { + if (consumeViewportPower(s, context, setCosts(tier), value.length, Settings.get.gpuSetCost)) { s.set(x, y, value, vertical) result(true) } @@ -325,8 +452,7 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI val tx = args.checkInteger(4) val ty = args.checkInteger(5) screen(s => { - val overlap: Int = getViewportOverlapSize(s, x + tx, y + ty, x + tx + w - 1, y + ty + h - 1) - if (consumeViewportPower(overlap, context, copyCosts(tier), Settings.get.gpuCopyCost)) { + if (consumeViewportPower(s, context, copyCosts(tier), w * h, Settings.get.gpuCopyCost)) { s.copy(x, y, w, h, tx, ty) 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.""") def fill(context: Context, args: Arguments): Array[AnyRef] = { - context.consumeCallBudget(fillCosts(tier)) val x = args.checkInteger(0) - 1 val y = args.checkInteger(1) - 1 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 => { val c = value.charAt(0) 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(overlap, context, fillCosts(tier), cost)) { + if (consumeViewportPower(s, context, fillCosts(tier), w * h, cost)) { s.fill(x, y, w, h, value.charAt(0)) result(true) } @@ -363,6 +487,13 @@ class GraphicsCard(val tier: Int) extends prefab.ManagedEnvironment with DeviceI override def onMessage(message: 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)) { screen(s => { 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) { super.load(nbt) - if (nbt.hasKey("screen")) { - nbt.getString("screen") match { + if (nbt.hasKey(SCREEN_KEY)) { + nbt.getString(SCREEN_KEY) match { case screen: String if !screen.isEmpty => screenAddress = Some(screen) case _ => screenAddress = 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) { super.save(nbt) 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) } } diff --git a/src/main/scala/li/cil/oc/util/TextBuffer.scala b/src/main/scala/li/cil/oc/util/TextBuffer.scala index 31503ae80..3b4de3105 100644 --- a/src/main/scala/li/cil/oc/util/TextBuffer.scala +++ b/src/main/scala/li/cil/oc/util/TextBuffer.scala @@ -205,6 +205,28 @@ class TextBuffer(var width: Int, var height: Int, initialFormat: PackedColor.Col 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) { if (FontUtils.wcwidth(c) > 1 && x >= line.length - 1) { // Don't allow setting wide chars in right-most col.