the multiview emulator thingamajig

This commit is contained in:
minjaesong
2022-10-23 19:40:23 +09:00
parent 5222d9c962
commit 477156e1bd
8 changed files with 602 additions and 3 deletions

View File

@@ -0,0 +1,52 @@
package net.torvald.terrarum
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.graphics.g2d.TextureRegion
/**
* Don't flip the assets! Flip the draw command instead!
*
* Created by minjaesong on 2021-12-13.
*/
class FlippingSpriteBatch : SpriteBatch() {
/**
* This function draws the flipped version of the image by giving flipped uv-coord to the SpriteBatch
*/
override fun draw(texture: Texture, x: Float, y: Float, width: Float, height: Float) =
draw(texture, x, y, width, height, 0f, 0f, 1f, 1f)
override fun draw(texture: Texture, x: Float, y: Float) =
draw(texture, x, y, texture.width.toFloat(), texture.height.toFloat(), 0f, 0f, 1f, 1f)
fun drawFlipped(texture: Texture, x: Float, y: Float, width: Float, height: Float) =
draw(texture, x, y, width, height, 0f, 1f, 1f, 0f)
fun drawFlipped(texture: Texture, x: Float, y: Float) =
draw(texture, x, y, texture.width.toFloat(), texture.height.toFloat(), 0f, 1f, 1f, 0f)
/**
* This function does obey the flipping set to the TextureRegion and try to draw flipped version of it,
* without touching the flipping setting of the given region.
*/
override fun draw(region: TextureRegion, x: Float, y: Float, width: Float, height: Float) =
draw(region.texture, x, y, width, height, region.u, region.v, region.u2, region.v2)
override fun draw(region: TextureRegion, x: Float, y: Float) =
draw(region.texture, x, y, region.regionWidth.toFloat(), region.regionHeight.toFloat(), region.u, region.v, region.u2, region.v2)
fun drawFlipped(region: TextureRegion, x: Float, y: Float, width: Float, height: Float) =
draw(region.texture, x, y, width, height, region.u, region.v2, region.u2, region.v)
fun drawFlipped(region: TextureRegion, x: Float, y: Float) =
draw(region.texture, x, y, region.regionWidth.toFloat(), region.regionHeight.toFloat(), region.u, region.v2, region.u2, region.v)
/**
* NOTE TO SELF:
*
* It seems that original SpriteBatch Y-flips when it's drawing a texture, but NOT when it's drawing a textureregion
*
* (textureregion's default uv-coord is (0,0,1,1)
*/
}

View File

@@ -0,0 +1,106 @@
package net.torvald.terrarum.imagefont
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.g2d.Batch
import com.badlogic.gdx.graphics.g2d.BitmapFont
import com.badlogic.gdx.graphics.g2d.GlyphLayout
import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack
import kotlin.math.roundToInt
/**
* Created by minjaesong on 2016-04-15.
*/
object TinyAlphNum : BitmapFont() {
internal val W = 7
internal val H = 13
internal val fontSheet = TextureRegionPack(Gdx.files.internal("net/torvald/terrarum/imagefont/font.tga"), W, H)
init {
setOwnsTexture(true)
setUseIntegerPositions(true)
}
fun getWidth(str: String): Int {
var l = 0
for (char in str) {
if (!isColourCodeHigh(char) && !isColourCodeLow(char)) {
l += 1
}
}
return W * l
}
lateinit var colMain: Color
lateinit var colShadow: Color
override fun draw(batch: Batch, text: CharSequence, x: Float, y: Float): GlyphLayout? {
val originalColour = batch.color.cpy()
colMain = batch.color.cpy()
colShadow = colMain.cpy().mul(0.5f, 0.5f, 0.5f, 1f)
val x = x.roundToInt().toFloat()
val y = y.roundToInt().toFloat()
var charsPrinted = 0
text.forEachIndexed { index, c ->
if (isColourCodeHigh(c)) {
val cchigh = c
val cclow = text[index + 1]
val colour = getColour(cchigh, cclow)
colMain = colour
colShadow = colMain.cpy().mul(0.5f, 0.5f, 0.5f, 1f)
}
else if (c in 0.toChar()..255.toChar()) {
batch.color = colShadow
batch.draw(fontSheet.get(c.toInt() % 16, c.toInt() / 16), x + charsPrinted * W + 1, y)
batch.draw(fontSheet.get(c.toInt() % 16, c.toInt() / 16), x + charsPrinted * W, y + 1)
batch.draw(fontSheet.get(c.toInt() % 16, c.toInt() / 16), x + charsPrinted * W + 1, y + 1)
batch.color = colMain
batch.draw(fontSheet.get(c.toInt() % 16, c.toInt() / 16), x + charsPrinted * W, y)
charsPrinted += 1
}
}
batch.color = originalColour
return null
}
override fun getLineHeight() = H.toFloat()
override fun getCapHeight() = getLineHeight()
override fun getXHeight() = getLineHeight()
private fun isColourCodeHigh(c: Char) = c.toInt() in 0b110110_1111000000..0b110110_1111111111
private fun isColourCodeLow(c: Char) = c.toInt() in 0b110111_0000000000..0b110111_1111111111
private fun getColour(charHigh: Char, charLow: Char): Color { // input: 0x10ARGB, out: RGBA8888
val codePoint = Character.toCodePoint(charHigh, charLow)
if (colourBuffer.containsKey(codePoint))
return colourBuffer[codePoint]!!
val a = codePoint.and(0xF000).ushr(12)
val r = codePoint.and(0x0F00).ushr(8)
val g = codePoint.and(0x00F0).ushr(4)
val b = codePoint.and(0x000F)
val col = Color(r.shl(28) or r.shl(24) or g.shl(20) or g.shl(16) or b.shl(12) or b.shl(8) or a.shl(4) or a)
colourBuffer[codePoint] = col
return col
}
private val colourBuffer = HashMap<Int, Color>()
}

Binary file not shown.

View File

@@ -0,0 +1,103 @@
/*
* Terrarum Sans Bitmap
*
* Copyright (c) 2017-2021 Minjae Song (Torvald)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.torvald.terrarumsansbitmap.gdx
import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.TextureRegion
import com.badlogic.gdx.utils.Disposable
/**
* Created by minjaesong on 2017-06-15.
*/
class TextureRegionPack(
val texture: Texture,
val tileW: Int,
val tileH: Int,
val hGap: Int = 0,
val vGap: Int = 0,
val hFrame: Int = 0,
val vFrame: Int = 0,
val xySwapped: Boolean = false, // because Unicode chart does, duh
val flipX: Boolean = false,
val flipY: Boolean = false
): Disposable {
constructor(ref: String, tileW: Int, tileH: Int, hGap: Int = 0, vGap: Int = 0, hFrame: Int = 0, vFrame: Int = 0, xySwapped: Boolean = false, flipX: Boolean = false, flipY: Boolean = false) :
this(Texture(ref), tileW, tileH, hGap, vGap, hFrame, vFrame, xySwapped, flipX, flipY)
constructor(fileHandle: FileHandle, tileW: Int, tileH: Int, hGap: Int = 0, vGap: Int = 0, hFrame: Int = 0, vFrame: Int = 0, xySwapped: Boolean = false, flipX: Boolean = false, flipY: Boolean = false) :
this(Texture(fileHandle), tileW, tileH, hGap, vGap, hFrame, vFrame, xySwapped, flipX, flipY)
companion object {
}
val regions: Array<TextureRegion>
val horizontalCount = (texture.width - 2 * hFrame + hGap) / (tileW + hGap)
val verticalCount = (texture.height - 2 * vFrame + vGap) / (tileH + vGap)
init {
//println("texture: $texture, dim: ${texture.width} x ${texture.height}, grid: $horizontalCount x $verticalCount, cellDim: $tileW x $tileH")
if (!xySwapped) {
regions = Array<TextureRegion>(horizontalCount * verticalCount) {
val region = TextureRegion()
val rx = (it % horizontalCount * (tileW + hGap)) + hFrame
val ry = (it / horizontalCount * (tileH + vGap)) + vFrame
region.setRegion(texture)
region.setRegion(rx, ry, tileW, tileH)
region.flip(flipX, flipY)
/*return*/region
}
}
else {
regions = Array<TextureRegion>(horizontalCount * verticalCount) {
val region = TextureRegion()
val rx = (it / verticalCount * (tileW + hGap)) + hFrame
val ry = (it % verticalCount * (tileH + vGap)) + vFrame
region.setRegion(texture)
region.setRegion(rx, ry, tileW, tileH)
region.flip(flipX, flipY)
/*return*/region
}
}
}
fun get(x: Int, y: Int) = regions[y * horizontalCount + x]
fun forEach(action: (TextureRegion) -> Unit) = regions.forEach(action)
override fun dispose() {
texture.dispose()
}
}

View File

@@ -14,8 +14,8 @@ public class AppLoader {
public static String appTitle = "tsvm";
public static Lwjgl3ApplicationConfiguration appConfig;
public static int WIDTH = 1080;//640;
public static int HEIGHT = 436;//480;
public static int WIDTH = 1280;//1080;//640;
public static int HEIGHT = 960;//436;//480;
public static void main(String[] args) {
ShaderProgram.pedantic = false;
@@ -62,6 +62,6 @@ public class AppLoader {
pipvm, 160, 140
))));*/
new Lwjgl3Application(new VMGUI(portable, WIDTH, HEIGHT), appConfig);
new Lwjgl3Application(new VMGUI(reference, 640, 480), appConfig);
}
}

View File

@@ -0,0 +1,37 @@
package net.torvald.tsvm;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import net.torvald.tsvm.peripheral.*;
import java.io.File;
/**
* Created by minjaesong on 2022-10-22.
*/
public class TsvmEmulator {
public static String appTitle = "tsvm";
public static Lwjgl3ApplicationConfiguration appConfig;
public static int WIDTH = 640 * 2;
public static int HEIGHT = 480 * 2;
public static void main(String[] args) {
ShaderProgram.pedantic = false;
appConfig = new Lwjgl3ApplicationConfiguration();
appConfig.setIdleFPS(60);
appConfig.setForegroundFPS(60);
appConfig.useVsync(false);
appConfig.setResizable(false);
appConfig.setTitle(appTitle);
appConfig.setWindowedMode(WIDTH, HEIGHT);
new Lwjgl3Application(new VMEmuExecutable(640, 480, 2, 2,"assets/"), appConfig);
}
}

View File

@@ -0,0 +1,295 @@
package net.torvald.tsvm
import com.badlogic.gdx.ApplicationAdapter
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.*
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import net.torvald.terrarum.FlippingSpriteBatch
import net.torvald.terrarum.imagefont.TinyAlphNum
import net.torvald.tsvm.peripheral.GraphicsAdapter
import net.torvald.tsvm.peripheral.ReferenceGraphicsAdapter2
import net.torvald.tsvm.peripheral.TestDiskDrive
import net.torvald.tsvm.peripheral.TsvmBios
import java.io.File
import java.util.*
import kotlin.collections.HashMap
/**
* Created by minjaesong on 2022-10-22.
*/
class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX: Int, var panelsY: Int, val diskPathRoot: String) : ApplicationAdapter() {
private data class VMRunnerInfo(val vm: VM, val name: String)
private val vms = arrayOfNulls<VMRunnerInfo>(this.panelsX * this.panelsY - 1) // index: # of the window where the reboot was requested
private var currentVMselection = 0
lateinit var batch: SpriteBatch
lateinit var fbatch: FlippingSpriteBatch
lateinit var camera: OrthographicCamera
var vmRunners = HashMap<Int, VMRunner>() // <VM's identifier, VMRunner>
var coroutineJobs = HashMap<Int, Job>() // <VM's identifier, Job>
lateinit var fullscreenQuad: Mesh
private lateinit var sqtex: Texture
private lateinit var font: TinyAlphNum
override fun create() {
super.create()
sqtex = Texture(Gdx.files.internal("net/torvald/tsvm/sq.tga"))
font = TinyAlphNum
fullscreenQuad = Mesh(
true, 4, 6,
VertexAttribute.Position(),
VertexAttribute.ColorUnpacked(),
VertexAttribute.TexCoords(0)
)
updateFullscreenQuad(AppLoader.WIDTH, AppLoader.HEIGHT)
batch = SpriteBatch()
fbatch = FlippingSpriteBatch()
camera = OrthographicCamera(AppLoader.WIDTH.toFloat(), AppLoader.HEIGHT.toFloat())
camera.setToOrtho(true)
camera.update()
batch.projectionMatrix = camera.combined
fbatch.projectionMatrix = camera.combined
// install the default VM on slot 0
val vm = VM("./assets", 8192 shl 10, TheRealWorld(), arrayOf(TsvmBios), 8)
vm.getIO().blockTransferPorts[0].attachDevice(TestDiskDrive(vm, 0, File("assets/disk0")))
initVMenv(vm)
vms[0] = VMRunnerInfo(vm, "Initial VM")
val vm2 = VM("./assets", 64 shl 10, TheRealWorld(), arrayOf(TsvmBios), 8)
vm2.getIO().blockTransferPorts[0].attachDevice(TestDiskDrive(vm2, 0, File("assets/disk0")))
initVMenv(vm2)
vms[1] = VMRunnerInfo(vm2, "Initial VM2")
init()
}
private fun init() {
changeActiveSession(0)
}
private fun changeActiveSession(index: Int) {
currentVMselection = index
// TODO somehow implement the inputstream that cares about the currentVMselection
Gdx.input.inputProcessor = vms[currentVMselection]?.vm?.getIO()
}
private fun initVMenv(vm: VM) {
vm.peripheralTable.getOrNull(1)?.peripheral?.dispose()
val gpu = ReferenceGraphicsAdapter2("./assets", vm)
vm.peripheralTable[1] = PeripheralEntry(gpu, GraphicsAdapter.VRAM_SIZE, 16, 0)
vm.getPrintStream = { gpu.getPrintStream() }
vm.getErrorStream = { gpu.getErrorStream() }
vm.getInputStream = { gpu.getInputStream() }
vmRunners[vm.id] = VMRunnerFactory(vm.assetsDir, vm, "js")
coroutineJobs[vm.id] = GlobalScope.launch { vmRunners[vm.id]?.executeCommand(vm.roms[0]!!.readAll()) }
}
private fun setCameraPosition(newX: Float, newY: Float) {
camera.position.set((-newX + AppLoader.WIDTH / 2), (-newY + AppLoader.HEIGHT / 2), 0f) // deliberate integer division
camera.update()
batch.setProjectionMatrix(camera.combined)
}
private fun gdxClearAndSetBlend(r: Float, g: Float, b: Float, a: Float) {
Gdx.gl.glClearColor(r,g,b,a)
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
Gdx.gl.glEnable(GL20.GL_TEXTURE_2D)
Gdx.gl.glEnable(GL20.GL_BLEND)
}
private fun updateFullscreenQuad(WIDTH: Int, HEIGHT: Int) { // NOT y-flipped quads!
fullscreenQuad.setVertices(floatArrayOf(
0f, 0f, 0f, 1f, 1f, 1f, 1f, 0f, 1f,
WIDTH.toFloat(), 0f, 0f, 1f, 1f, 1f, 1f, 1f, 1f,
WIDTH.toFloat(), HEIGHT.toFloat(), 0f, 1f, 1f, 1f, 1f, 1f, 0f,
0f, HEIGHT.toFloat(), 0f, 1f, 1f, 1f, 1f, 0f, 0f
))
fullscreenQuad.setIndices(shortArrayOf(0, 1, 2, 2, 3, 0))
}
private var updateAkku = 0.0
private var updateRate = 1f / 60f
override fun render() {
gdxClearAndSetBlend(.094f, .094f, .094f, 0f)
setCameraPosition(0f, 0f)
// update window title with contents of the 'built-in status display'
Gdx.graphics.setTitle("tsvm $EMDASH F: ${Gdx.graphics.framesPerSecond}")
super.render()
val dt = Gdx.graphics.rawDeltaTime
updateAkku += dt
var i = 0L
while (updateAkku >= updateRate) {
updateGame(updateRate)
updateAkku -= updateRate
i += 1
}
renderGame(dt)
}
private fun reboot(vm: VM) {
vmRunners[vm.id]!!.close()
coroutineJobs[vm.id]!!.cancel("reboot requested")
vm.init()
initVMenv(vm)
}
private fun updateGame(delta: Float) {
// update currently selected viewport
val mouseX = Gdx.input.x
val mouseY = Gdx.input.y
if (Gdx.input.justTouched()) {
val px = mouseX / windowWidth
val py = mouseY / windowHeight
val panel = py * panelsX + px
if (panel < panelsX * panelsY - 1) {
changeActiveSession(panel)
}
}
vms.forEachIndexed { index, it ->
if (it?.vm?.resetDown == true && index == currentVMselection) { reboot(it.vm) }
it?.vm?.update(delta)
}
}
private val defaultGuiBackgroundColour = Color(0x444444ff)
private fun renderGame(delta: Float) {
vms.forEachIndexed { index, vmInfo ->
drawVMtoCanvas(delta, batch, vmInfo?.vm, index % panelsX, index / panelsX)
// draw Window frames and whatnot
val xoff = (index % panelsX) * windowWidth
val yoff = (index / panelsX) * windowHeight
batch.color =
if (index == currentVMselection) EmulatorGuiToolkit.Theme.COL_HIGHLIGHT else EmulatorGuiToolkit.Theme.COL_INACTIVE
batch.inUse {
batch.fillRect(xoff, yoff, windowWidth, 2)
batch.fillRect(xoff, yoff + windowHeight - 2, windowWidth, 2)
batch.fillRect(xoff, yoff, 2, windowHeight)
batch.fillRect(xoff + windowWidth - 2, yoff, 2, windowHeight)
}
}
}
private fun drawVMtoCanvas(delta: Float, batch: SpriteBatch, vm: VM?, pposX: Int, pposY: Int) {
vm.let { vm ->
// assuming the reference adapter of 560x448
val xoff = pposX * windowWidth.toFloat()
val yoff = pposY * windowHeight.toFloat()
if (vm != null) {
(vm.peripheralTable.getOrNull(1)?.peripheral as? GraphicsAdapter).let { gpu ->
if (gpu != null) {
val clearCol = gpu.getBackgroundColour()
// clear the viewport by drawing coloured rectangle becausewhynot
batch.color = clearCol
batch.inUse {
batch.fillRect(pposX * windowWidth, pposY * windowHeight, windowWidth, windowHeight)
}
gpu.render(delta, fbatch, xoff + 40f, yoff + 16f, false, null)
}
else {
// no graphics device available
fbatch.inUse {
fbatch.color = defaultGuiBackgroundColour
fbatch.fillRect(pposX * windowWidth, pposY * windowHeight, windowWidth, windowHeight)
// draw text
fbatch.color = EmulatorGuiToolkit.Theme.COL_INACTIVE
font.draw(fbatch, "no graphics device available", xoff + (windowWidth - 196) / 2, yoff + (windowHeight - 12) / 2)
}
}
}
}
else {
// no vm on the viewport
fbatch.inUse {
fbatch.color = defaultGuiBackgroundColour
fbatch.fillRect(pposX * windowWidth, pposY * windowHeight, windowWidth, windowHeight)
// draw text
fbatch.color = EmulatorGuiToolkit.Theme.COL_INACTIVE
font.draw(fbatch, "no vm on this viewport", xoff + (windowWidth - 154) / 2, yoff + (windowHeight - 12) / 2)
}
}
}
}
private fun resizePanel(panelsX: Int, panelsY: Int) {
if (panelsX > 16 || panelsY > 16) throw IllegalArgumentException("Panel count too large: ($panelsX, $panelsY)")
if (panelsX * panelsY <= 0) throw IllegalArgumentException("Illegal panel count: ($panelsX, $panelsY)")
this.panelsX = panelsX
this.panelsY = panelsY
resize(windowWidth * panelsX, windowHeight * panelsY)
}
override fun resize(width: Int, height: Int) {
super.resize(width, height)
updateFullscreenQuad(width, height)
camera.setToOrtho(true, width.toFloat(), height.toFloat())
camera.update()
batch.projectionMatrix = camera.combined
fbatch.projectionMatrix = camera.combined
}
override fun dispose() {
super.dispose()
sqtex.dispose()
batch.dispose()
fbatch.dispose()
fullscreenQuad.dispose()
coroutineJobs.values.forEach { it.cancel() }
vms.forEach { it?.vm?.dispose() }
}
fun SpriteBatch.fillRect(x: Int, y: Int, w: Int, h: Int) {
this.draw(sqtex, x.toFloat(), y.toFloat(), w.toFloat(), h.toFloat())
}
fun SpriteBatch.inUse(f: (SpriteBatch) -> Unit) {
this.begin()
f(this)
this.end()
}
}
object EmulatorGuiToolkit {
object Theme {
val COL_INACTIVE = Color.LIGHT_GRAY
val COL_ACTIVE = Color(0xfff066_ff.toInt()) // yellow
val COL_HIGHLIGHT = Color(0x00f8ff_ff) // cyan
val COL_DISABLED = Color(0xaaaaaaff.toInt())
}
}

Binary file not shown.