codes split into modules: tsvm_core, tsvm_executable, TerranBASICexecutable

This commit is contained in:
minjaesong
2021-12-03 11:57:31 +09:00
parent 20b34c8d19
commit 5e290061f4
49 changed files with 582 additions and 281 deletions

View File

@@ -0,0 +1 @@
org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory

View File

@@ -0,0 +1,21 @@
package net.torvald.tsvm
import com.badlogic.gdx.utils.Base64Coder
object Base64Delegate {
fun atob(inputstr: String): ByteArray {
return Base64Coder.decode(inputstr)
}
fun atostr(inputstr: String): String {
return Base64Coder.decode(inputstr).toString(VM.CHARSET)
}
fun btoa(inputbytes: ByteArray): String {
val sb = StringBuilder()
sb.append(Base64Coder.encode(inputbytes))
return sb.toString()
}
}

View File

@@ -0,0 +1,193 @@
package net.torvald.tsvm
import java.util.*
/**
* buffer[head] contains the most recent item, whereas buffer[tail] contains the oldest one.
*
* Notes for particle storage:
* Particles does not need to be removed, just let it overwrite as their operation is rather
* lightweight. So, just flagDespawn = true if it need to be "deleted" so that it won't update
* anymore.
*
* Created by minjaesong on 2017-01-22.
*/
internal class CircularArray<T>(val size: Int, val overwriteOnOverflow: Boolean): Iterable<T> {
/**
* What to do RIGHT BEFORE old element is being overridden by the new element (only makes sense when ```overwriteOnOverflow = true```)
*
* This function will not be called when ```removeHead()``` or ```removeTail()``` is called.
*/
var overwritingPolicy: (T) -> Unit = {
// do nothing
}
val buffer: Array<T> = arrayOfNulls<Any>(size) as Array<T>
/** Tail stands for the oldest element. The tail index points AT the tail element */
var tail: Int = 0; private set
/** Head stands for the youngest element. The head index points AFTER the head element */
var head: Int = 0; private set
private var overflow = false
val lastIndex = size - 1
/**
* Number of elements that forEach() or fold() would iterate.
*/
val elemCount: Int
get() = if (overflow) size else head - tail
val isEmpty: Boolean
get() = !overflow && head == tail
private inline fun incHead() { head = (head + 1).wrap() }
private inline fun decHead() { head = (head - 1).wrap() }
private inline fun incTail() { tail = (tail + 1).wrap() }
private inline fun decTail() { tail = (tail - 1).wrap() }
fun clear() {
tail = 0
head = 0
overflow = false
}
/**
* When the overflowing is enabled, tail element (ultimate element) will be changed into the penultimate element.
*/
fun appendHead(item: T) {
if (overflow && !overwriteOnOverflow) {
throw StackOverflowError()
}
else {
if (overflow) {
overwritingPolicy.invoke(buffer[head])
}
buffer[head] = item
incHead()
}
if (overflow) {
incTail()
}
// must be checked AFTER the actual head increment; otherwise this condition doesn't make sense
if (tail == head) {
overflow = true
}
}
fun appendTail(item: T) {
// even if overflowing is enabled, appending at tail causes head element to be altered, therefore such action
// must be blocked by throwing overflow error
// if you think this behaviour is wrong, you're confusing appendHead() with appendTail(). Use appendHead() and removeTail()
if (overflow) {
throw StackOverflowError()
}
else {
decTail()
buffer[tail] = item
}
// must be checked AFTER the actual head increment; otherwise this condition doesn't make sense
if (tail == head) {
overflow = true
}
}
fun removeHead(): T? {
if (isEmpty) return null
decHead()
overflow = false
return buffer[head]
}
fun removeTail(): T? {
if (isEmpty) return null
val ret = buffer[tail]
incTail()
overflow = false
return ret
}
/** Returns the youngest (last of the array) element */
fun getHeadElem(): T? = if (isEmpty) null else buffer[(head - 1).wrap()]
/** Returns the oldest (first of the array) element */
fun getTailElem(): T? = if (isEmpty) null else buffer[tail]
/**
* Relative-indexed get. Index of zero will return the head element.
*/
operator fun get(index: Int): T? = buffer[(head - 1 - index).wrap()]
private fun getAbsoluteRange() = 0 until when {
head == tail -> buffer.size
tail > head -> buffer.size - (((head - 1).wrap()) - tail)
else -> head - tail
}
override fun iterator(): Iterator<T> {
if (isEmpty) {
return object : Iterator<T> {
override fun next(): T = throw EmptyStackException()
override fun hasNext() = false
}
}
val rangeMax = getAbsoluteRange().last
var counter = 0
return object : Iterator<T> {
override fun next(): T {
val ret = buffer[(counter + tail).wrap()]
counter += 1
return ret
}
override fun hasNext() = (counter <= rangeMax)
}
}
/**
* Iterates the array with oldest element (tail) first.
*/
fun forEach(action: (T) -> Unit) {
// for (element in buffer) action(element)
// return nothing
iterator().forEach(action)
}
fun <R> fold(initial: R, operation: (R, T) -> R): R {
// accumulator = initial
// for (element in buffer) accumulator = operation(accumulator, element)
// return accumulator
var accumulator = initial
if (isEmpty)
return initial
else {
iterator().forEach {
accumulator = operation(accumulator, it)
}
}
return accumulator
}
private inline fun Int.wrap() = this fmod size
override fun toString(): String {
return "CircularArray(size=${buffer.size}, head=$head, tail=$tail, overflow=$overflow)"
}
private inline infix fun Int.fmod(other: Int) = Math.floorMod(this, other)
}

View File

@@ -0,0 +1,42 @@
package net.torvald.tsvm
import com.badlogic.gdx.utils.compression.Lzma
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
object CompressorDelegate {
/*fun comp(ba: ByteArray): ByteArray {
val bin = ByteArrayInputStream(ba)
val bout = ByteArrayOutputStream(256)
Lzma.compress(bin, bout)
return bout.toByteArray()
}
fun decomp(ba: ByteArray): ByteArray {
val bin = ByteArrayInputStream(ba)
val bout = ByteArrayOutputStream(256)
Lzma.decompress(bin, bout)
return bout.toByteArray()
}*/
fun comp(ba: ByteArray): ByteArray {
val baos = ByteArrayOutputStream()
val gz = GZIPOutputStream(baos)
gz.write(ba); gz.flush(); gz.finish()
baos.flush(); baos.close()
return baos.toByteArray()
}
fun decomp(ba: ByteArray): ByteArray {
val bais = ByteArrayInputStream(ba)
val gz = GZIPInputStream(bais)
val ret = gz.readBytes()
gz.close(); bais.close()
return ret
}
val GZIP_HEADER = byteArrayOf(31,-117,8) // .gz in DEFLATE
}

View File

@@ -0,0 +1,79 @@
package net.torvald.tsvm
import net.torvald.UnsafeHelper
import net.torvald.tsvm.peripheral.GraphicsAdapter
/**
* Created by minjaesong on 2021-10-15.
*/
class DMADelegate(val vm: VM) {
private val READ = "READ".toByteArray(VM.CHARSET)
private val FLUSH = "FLUSH".toByteArray(VM.CHARSET)
private val CLOSE = "CLOSE".toByteArray(VM.CHARSET)
private fun WRITE(n: Int) = "WRITE$n".toByteArray(VM.CHARSET)
fun ramToFrame(from: Int, devnum: Int, offset: Int, length: Int) {
(vm.peripheralTable[devnum].peripheral as? GraphicsAdapter)?.let {
val data = ByteArray(length)
UnsafeHelper.memcpyRaw(null, vm.usermem.ptr + from, data, UnsafeHelper.getArrayOffset(data), length.toLong())
it.framebuffer.pixels.position(offset)
it.framebuffer.pixels.put(data)
it.framebuffer.pixels.position(0) // rewinding to avoid graphical glitch
}
}
fun ramToFrame(from: Int, to: Int, length: Int) {
for (i in 0..7) {
if (vm.peripheralTable[i].type == VM.PERITYPE_GPU_AND_TERM) {
ramToFrame(from, i, to, length)
return
}
}
}
fun frameToRam(from: Int, to: Int, devnum: Int, length: Int) {
(vm.peripheralTable[devnum].peripheral as? GraphicsAdapter)?.let {
val data = ByteArray(length)
it.framebuffer.pixels.position(from)
it.framebuffer.pixels.get(data)
it.framebuffer.pixels.position(0) // rewinding to avoid graphical glitch
UnsafeHelper.memcpyRaw(data, UnsafeHelper.getArrayOffset(data), null, vm.usermem.ptr + to, length.toLong())
}
}
fun frameToRam(from: Int, to: Int, length: Int) {
for (i in 0..7) {
if (vm.peripheralTable[i].type == VM.PERITYPE_GPU_AND_TERM) {
frameToRam(from, to, i, length)
return
}
}
}
fun comToRam(portNo: Int, srcOff: Int, destOff: Int, length: Int) {
SerialHelper.sendMessage(vm, portNo, READ)
val response = SerialHelper.getStatusCode(vm, portNo)
if (response == 0) {
val file = SerialHelper.pullMessage(vm, portNo)
UnsafeHelper.memcpyRaw(file, UnsafeHelper.getArrayOffset(file) + srcOff.toLong(), null, vm.usermem.ptr + destOff, length.toLong())
}
}
fun ramToCom(srcOff: Int, portNo: Int, length: Int) {
SerialHelper.sendMessage(vm, portNo, WRITE(length))
val response = SerialHelper.getStatusCode(vm, portNo)
if (response == 0) {
val msg = ByteArray(length)
UnsafeHelper.memcpyRaw(null, vm.usermem.ptr + srcOff, msg, UnsafeHelper.getArrayOffset(msg), length.toLong())
SerialHelper.sendMessage(vm, portNo, msg)
SerialHelper.sendMessage(vm, portNo, FLUSH)
SerialHelper.sendMessage(vm, portNo, CLOSE)
}
}
fun ramToRam(from: Int, to: Int, length: Int) {
UnsafeHelper.memcpy(vm.usermem.ptr + from, vm.usermem.ptr + to, length.toLong())
}
}

View File

@@ -0,0 +1,66 @@
package net.torvald.tsvm
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.toUint
import java.io.InputStream
class VmFilesystemDelegate(val vm: VM, val portNo: Int) {
fun getFileInputStream(path: String) = DiskDriveFileInputStream(vm, portNo, path)
}
class DiskDriveFileInputStream(val vm: VM, val portNo: Int, val path: String) : InputStream() {
private val contents: ByteArray
private var readCursor = 0
init {
SerialHelper.sendMessage(vm, portNo, "OPENR\"$path\"".toByteArray(VM.CHARSET))
SerialHelper.waitUntilReady(vm, portNo)
contents = SerialHelper.sendMessageGetBytes(vm, portNo, "READ".toByteArray(VM.CHARSET))
}
override fun markSupported() = true
override fun read(): Int {
if (readCursor >= contents.size) return -1
val ret = contents[readCursor].toUint()
readCursor += 1
return ret
}
override fun skip(n: Long): Long {
val newReadCursor = minOf(contents.size.toLong(), readCursor + n)
val diff = newReadCursor - readCursor
readCursor = newReadCursor.toInt()
return diff
}
override fun reset() {
readCursor = 0
}
override fun mark(i: Int) {
TODO()
}
override fun read(p0: ByteArray): Int {
var readBytes = 0
for (k in p0.indices) {
val r = read()
p0[k] = r.toByte()
if (r >= 0) readBytes += 1
}
return readBytes
}
override fun read(p0: ByteArray, p1: Int, p2: Int): Int {
TODO()
}
override fun close() {
SerialHelper.sendMessage(vm, portNo, "CLOSE".toByteArray(VM.CHARSET))
}
}

View File

@@ -0,0 +1,173 @@
package net.torvald.tsvm
import net.torvald.UnsafeHelper
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.toUint
import net.torvald.tsvm.peripheral.GraphicsAdapter
import net.torvald.tsvm.peripheral.fmod
class GraphicsJSR223Delegate(val vm: VM) {
private fun getFirstGPU(): GraphicsAdapter? {
return vm.findPeribyType(VM.PERITYPE_GPU_AND_TERM)?.peripheral as? GraphicsAdapter
}
fun resetPalette() {
getFirstGPU()?.poke(250883L, 1)
}
/**
* @param index which palette number to modify, 0-255
* @param r g - b - a - RGBA value, 0-15
*/
fun setPalette(index: Int, r: Int, g: Int, b: Int, a: Int = 16) {
getFirstGPU()?.let {
it.paletteOfFloats[index * 4] = (r and 15) / 15f
it.paletteOfFloats[index * 4 + 1] = (g and 15) / 15f
it.paletteOfFloats[index * 4 + 2] = (b and 15) / 15f
it.paletteOfFloats[index * 4 + 3] = (a and 15) / 15f
}
}
/*fun loadBulk(fromAddr: Int, toAddr: Int, length: Int) {
getFirstGPU()?._loadbulk(fromAddr, toAddr, length)
}
fun storeBulk(fromAddr: Int, toAddr: Int, length: Int) {
getFirstGPU()?._storebulk(fromAddr, toAddr, length)
}*/
fun plotPixel(x: Int, y: Int, color: Int) {
getFirstGPU()?.let {
if (x in 0 until it.config.width && y in 0 until it.config.height) {
it.poke(y.toLong() * it.config.width + x, color.toByte())
}
}
}
/**
* Sets absolute position of scrolling
*/
fun setFramebufferScroll(x: Int, y: Int) {
getFirstGPU()?.let {
it.framebufferScrollX = x
it.framebufferScrollY = y
}
}
fun scrollFrame(xdelta: Int, ydelta: Int) {
getFirstGPU()?.let {
it.framebufferScrollX = (it.framebufferScrollX + xdelta) fmod it.framebuffer.width
it.framebufferScrollY = (it.framebufferScrollY + ydelta) fmod it.framebuffer.height
}
}
fun setLineOffset(line: Int, offset: Int) {
getFirstGPU()?.let {
it.poke(250900L + 2*line, offset.shr(8).toByte()) // absolutely not USHR
it.poke(250901L + 2*line, offset.toByte())
}
}
fun getLineOffset(line: Int): Int {
getFirstGPU()?.let {
var xoff = it.peek(250900L + 2*line)!!.toUint().shl(8) or it.peek(250901L + 2*line)!!.toUint()
if (xoff.and(0x8000) != 0) xoff = xoff or 0xFFFF0000.toInt()
return xoff
}
return 0
}
fun getPixelDimension(): IntArray {
getFirstGPU()?.let { return intArrayOf(it.framebuffer.width, it.framebuffer.height) }
return intArrayOf(-1, -1)
}
fun getTermDimension(): IntArray {
getFirstGPU()?.let { return intArrayOf(it.TEXT_ROWS, it.TEXT_COLS) }
return intArrayOf(-1, -1)
}
fun getCursorYX(): IntArray {
getFirstGPU()?.let {
val (cx, cy) = it.getCursorPos()
return intArrayOf(cy + 1, cx + 1)
}
return intArrayOf(-1, -1)
}
fun setCursorYX(cy: Int, cx: Int) {
getFirstGPU()?.let {
it.setCursorPos(cy - 1, cx - 1)
}
}
fun setBackground(r: Int, g: Int, b: Int) {
getFirstGPU()?.let {
it.poke(250880, r.toByte())
it.poke(250881, g.toByte())
it.poke(250882, b.toByte())
}
}
fun clearText() {
getFirstGPU()?.eraseInDisp(2)
}
fun clearPixels(col: Int) {
getFirstGPU()?.poke(250884L, col.toByte())
getFirstGPU()?.poke(250883L, 2)
}
/**
* prints a char as-is; won't interpret them as an escape sequence
*/
fun putSymbol(c: Int) {
getFirstGPU()?.let {
val (cx, cy) = it.getCursorPos()
it.putChar(cx, cy, c.toByte())
}
}
fun putSymbolAt(cy: Int, cx: Int, c: Int) {
getFirstGPU()?.let {
it.putChar(cx - 1, cy - 1, c.toByte())
}
}
/*private fun GraphicsAdapter._loadbulk(fromAddr: Int, toAddr: Int, length: Int) {
UnsafeHelper.memcpy(
vm.usermem.ptr + fromAddr,
(this.framebuffer.pixels as DirectBuffer).address() + toAddr,
length.toLong()
)
}
private fun GraphicsAdapter._storebulk(fromAddr: Int, toAddr: Int, length: Int) {
UnsafeHelper.memcpy(
(this.framebuffer.pixels as DirectBuffer).address() + fromAddr,
vm.usermem.ptr + toAddr,
length.toLong()
)
}*/
private fun GraphicsAdapter._loadSprite(spriteNum: Int, ptr: Int) {
UnsafeHelper.memcpy(
vm.usermem.ptr + ptr,
(this.textArea).ptr + (260 * spriteNum) + 4,
256
)
}
private fun GraphicsAdapter._storeSprite(spriteNum: Int, ptr: Int) {
UnsafeHelper.memcpy(
(this.textArea).ptr + (260 * spriteNum) + 4,
vm.usermem.ptr + ptr,
256
)
}
}

View File

@@ -0,0 +1,18 @@
package net.torvald.tsvm
import com.badlogic.gdx.graphics.glutils.ShaderProgram
/**
* Created by minjaesong on 2021-12-03.
*/
internal object LoadShader {
operator fun invoke(vert: String, frag: String): ShaderProgram {
val s = ShaderProgram(vert, frag)
if (s.log.toLowerCase().contains("error")) {
throw Error(String.format("Shader program loaded with %s, %s failed:\n%s", vert, frag, s.log))
}
return s
}
}

View File

@@ -0,0 +1,150 @@
package net.torvald.tsvm
import net.torvald.UnsafeHelper
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.toUint
import net.torvald.tsvm.peripheral.BlockTransferInterface.Companion.BLOCK_SIZE
import net.torvald.tsvm.peripheral.BlockTransferInterface.Companion.END_OF_SEND_BLOCK
import net.torvald.tsvm.peripheral.trimNull
import java.io.ByteArrayOutputStream
import kotlin.experimental.and
import kotlin.experimental.or
import kotlin.math.ceil
object SerialHelper {
private const val SLEEP_TIME = 4L
fun sendMessageGetBytes(vm: VM, portNo: Int, message: ByteArray): ByteArray {
sendMessage(vm, portNo, message)
waitUntilReady(vm, portNo)
return fetchResponse(vm, portNo)
}
fun sendMessage(vm: VM, portNo: Int, message: ByteArray) {
if (!checkIfDeviceIsThere(vm, portNo)) throw IllegalStateException("Device not connected")
/*UnsafeHelper.memcpyRaw(
message, UnsafeHelper.getArrayOffset(message),
null, vm.getIO().blockTransferTx[portNo].ptr,
minOf(BLOCK_SIZE, message.size).toLong()
)*/
for (blockCount in 0 until ceil(message.size.toFloat() / BLOCK_SIZE).toInt()) {
for (k in 0 until BLOCK_SIZE) {
val index = BLOCK_SIZE * blockCount + k
vm.getIO().blockTransferTx[portNo][k.toLong()] = if (index >= message.size) 0 else message[index]
}
initiateWriting(vm, portNo)
waitUntilReady(vm, portNo)
}
// TODO assuming the write operation is finished... (wait for something?)
getReady(vm, portNo)
}
// Returns what's on the RX buffer after sendMessage()
// i.e won't (actually shouldn't) clobber the device's message compose buffer (see TestDiskDrive)
fun fetchResponse(vm: VM, portNo: Int): ByteArray {
val incomingMsg = ByteArray(BLOCK_SIZE)
// incoming message is always 4K long and unused bytes are zero-filled. THIS IS INTENTIONAL
UnsafeHelper.memcpyRaw(
null, vm.getIO().blockTransferRx[portNo].ptr,
incomingMsg, UnsafeHelper.getArrayOffset(incomingMsg),
BLOCK_SIZE.toLong()
)
return incomingMsg
}
// Initiates startSend() function from the connected device
fun pullMessage(vm: VM, portNo: Int): ByteArray {
val msgBuffer = ByteArrayOutputStream(BLOCK_SIZE)
// pull all the blocks of messages
do {
initiateReading(vm, portNo)
waitUntilReady(vm, portNo)
val transStat = getBlockTransferStatus(vm, portNo)
val receivedLen = transStat.first//vm.getIO().blockTransferPorts[portNo].yourBlockSize()
//println("[SerialHelper.pullMessage()] received length: $receivedLen")
for (k in 0 until minOf(BLOCK_SIZE, receivedLen)) {
msgBuffer.write(vm.getIO().blockTransferRx[portNo][k.toLong()].toInt())
}
} while (transStat.second)
getReady(vm, portNo)
return msgBuffer.toByteArray()
}
fun getDeviceStatus(vm: VM, portNo: Int): DeviceStatus {
val msgStr = sendMessageGetBytes(vm, portNo, "DEVSTU$END_OF_SEND_BLOCK".toByteArray(VM.CHARSET))
return DeviceStatus(
msgStr[1].toUint(),
msgStr.sliceArray(3 until msgStr.size - 1).toString(VM.CHARSET)
)
}
fun waitUntilReady(vm: VM, portNo: Int) {
while (!checkIfDeviceIsReady(vm, portNo)) { Thread.sleep(SLEEP_TIME) }
}
fun getStatusCode(vm: VM, portNo: Int) = vm.getIO().mmio_read(4080L + portNo)!!.toInt().and(255)
fun checkIfDeviceIsThere(vm: VM, portNo: Int) =
(vm.getIO().mmio_read(4092L + portNo)!! and 1.toByte()) == 1.toByte()
fun checkIfDeviceIsReady(vm: VM, portNo: Int) =
(vm.getIO().mmio_read(4092L + portNo)!! and 0b111.toByte()) == 0b011.toByte()
private fun initiateWriting(vm: VM, portNo: Int) {
vm.getIO().mmio_write(4092L + portNo, 0b1110)
}
private fun initiateReading(vm: VM, portNo: Int) {
vm.getIO().mmio_write(4092L + portNo, 0b0110)
}
private fun getReady(vm: VM, portNo: Int) {
val flags = vm.getIO().mmio_read(4092L + portNo)!!
val newFlags = flags.and(0b1111_1001.toByte()).or(0b0000_0010)
vm.getIO().mmio_write(4092L + portNo, newFlags)
}
private fun setBlockTransferStatus(vm: VM, portNo: Int, blockSize: Int, moreToSend: Boolean = false) {
vm.getIO().mmio_write(4084L + (portNo * 2), (blockSize and 255).toByte())
vm.getIO().mmio_write(4085L + (portNo * 2),
((blockSize ushr 8).and(15) or (moreToSend.toInt() shl 7)).toByte()
)
}
private fun getBlockTransferStatus(vm: VM, portNo: Int): Pair<Int, Boolean> {
val bits = vm.getIO().mmio_read(4084L + (portNo * 2))!!.toUint() or
(vm.getIO().mmio_read(4085L + (portNo * 2))!!.toUint() shl 8)
val rawcnt = bits.and(4095)
val gotMore = bits.and(0x8000) != 0
return (if (rawcnt == 0) BLOCK_SIZE else rawcnt) to gotMore
}
private fun Boolean.toInt() = if (this) 1 else 0
data class DeviceStatus(val code: Int, val message: String)
}
class SerialHelperDelegate(val vm: VM) {
fun sendMessage(portNo: Int, message: String) = SerialHelper.sendMessage(vm, portNo, message.toByteArray(VM.CHARSET))
fun pullMessage(portNo: Int) = SerialHelper.pullMessage(vm, portNo).toString(VM.CHARSET)
fun sendMessageGetBytes(portNo: Int, message: String) = SerialHelper.sendMessageGetBytes(vm, portNo, message.toByteArray(VM.CHARSET)).toString(VM.CHARSET)
fun fetchResponse(portNo: Int) = SerialHelper.fetchResponse(vm, portNo).toString(VM.CHARSET)
fun waitUntilReady(portNo: Int) = SerialHelper.waitUntilReady(vm, portNo)
fun getStatusCode(portNo: Int) = SerialHelper.getStatusCode(vm, portNo)
/** @return Object where { code: <Int>, message: <String> } */
fun getDeviceStatus(portNo: Int) = SerialHelper.getDeviceStatus(vm, portNo)
fun areYouThere(portNo: Int) = SerialHelper.checkIfDeviceIsThere(vm, portNo)
}

View File

@@ -0,0 +1,100 @@
/*
* Terrarum Sans Bitmap
*
* Copyright (c) 2017 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.tsvm
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
): Disposable {
constructor(ref: String, tileW: Int, tileH: Int, hGap: Int = 0, vGap: Int = 0, hFrame: Int = 0, vFrame: Int = 0, xySwapped: Boolean = false) :
this(Texture(ref), tileW, tileH, hGap, vGap, hFrame, vFrame, xySwapped)
constructor(fileHandle: FileHandle, tileW: Int, tileH: Int, hGap: Int = 0, vGap: Int = 0, hFrame: Int = 0, vFrame: Int = 0, xySwapped: Boolean = false) :
this(Texture(fileHandle), tileW, tileH, hGap, vGap, hFrame, vFrame, xySwapped)
companion object {
/** Intented for Y-down coord system, typically fon Non-GDX codebase */
var globalFlipY = false
}
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(false, globalFlipY)
/*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(false, globalFlipY)
/*return*/region
}
}
}
fun get(x: Int, y: Int) = regions[y * horizontalCount + x]
override fun dispose() {
texture.dispose()
}
}

View File

@@ -0,0 +1,150 @@
package net.torvald
import sun.misc.Unsafe
import java.io.PrintStream
/**
* Further read:
* - http://www.docjar.com/docs/api/sun/misc/Unsafe.html
*
* Created by minjaesong on 2019-06-21.
*/
internal object UnsafeHelper {
val unsafe: Unsafe
init {
val unsafeConstructor = Unsafe::class.java.getDeclaredConstructor()
unsafeConstructor.isAccessible = true
unsafe = unsafeConstructor.newInstance()
}
/**
* A factory method to allocate a memory of given size and return its starting address as a pointer.
*/
fun allocate(size: Long): UnsafePtr {
val ptr = unsafe.allocateMemory(size)
return UnsafePtr(ptr, size)
}
fun memcpy(src: UnsafePtr, fromIndex: Long, dest: UnsafePtr, toIndex: Long, copyLength: Long) =
unsafe.copyMemory(src.ptr + fromIndex, dest.ptr + toIndex, copyLength)
fun memcpy(srcAddress: Long, destAddress: Long, copyLength: Long) =
unsafe.copyMemory(srcAddress, destAddress, copyLength)
fun memcpyRaw(srcObj: Any?, srcPos: Long, destObj: Any?, destPos: Long, len: Long) =
unsafe.copyMemory(srcObj, srcPos, destObj, destPos, len)
/**
* The array object in JVM is stored in this memory map:
*
* 0 w 2w *
* | Some identifier | Other identifier | the actual data ... |
*
* (where w = 4 for 32-bit JVM and 8 for 64-bit JVM. If Compressed-OOP is involved, things may get complicated)
*
* @return offset from the array's base memory address (aka pointer) that the actual data begins.
*/
fun getArrayOffset(obj: Any) = unsafe.arrayBaseOffset(obj.javaClass).toLong()
}
/**
* To allocate a memory, use UnsafeHelper.allocate(long)
*
* All the getFloat/Int/whatever methods will follow the endianness of your system,
* e.g. it'll be Little Endian on x86, Big Endian on PPC, User-defined on ARM; therefore these functions should not be
* used when the portability matters (e.g. Savefile). In such situations, do byte-wise operations will be needed.
*
* Use of hashCode() is forbidden, use the pointer instead.
*/
internal class UnsafePtr(pointer: Long, allocSize: Long) {
var destroyed = false
private set
var ptr: Long = pointer
private set
var size: Long = allocSize
private set
fun realloc(newSize: Long) {
ptr = UnsafeHelper.unsafe.reallocateMemory(ptr, newSize)
size = newSize
}
fun destroy() {
if (!destroyed) {
println("[UnsafePtr] Destroying pointer $this; called from:")
printStackTrace(this)
UnsafeHelper.unsafe.freeMemory(ptr)
destroyed = true
}
}
private inline fun checkNullPtr(index: Long) { // ignore what IDEA says and do inline this
//// commenting out because of the suspected (or minor?) performance impact.
//// You may break the glass and use this tool when some fucking incomprehensible bugs ("vittujen vitun bugit")
//// appear (e.g. getting garbage values when it fucking shouldn't)
//assert(!destroyed) { throw NullPointerException("The pointer is already destroyed ($this)") }
//if (index !in 0 until size) throw IndexOutOfBoundsException("Index: $index; alloc size: $size")
}
operator fun get(index: Long): Byte {
checkNullPtr(index)
return UnsafeHelper.unsafe.getByte(ptr + index)
}
operator fun set(index: Long, value: Byte) {
checkNullPtr(index)
UnsafeHelper.unsafe.putByte(ptr + index, value)
}
// NOTE: get/set multibyte values are NOT BYTE-ALIGNED!
fun getFloat(index: Long): Float {
checkNullPtr(index)
return UnsafeHelper.unsafe.getFloat(ptr + index)
}
fun getInt(index: Long): Int {
checkNullPtr(index)
return UnsafeHelper.unsafe.getInt(ptr + index)
}
fun getShort(index: Long): Short {
checkNullPtr(index)
return UnsafeHelper.unsafe.getShort(ptr + index)
}
fun setFloat(index: Long, value: Float) {
checkNullPtr(index)
UnsafeHelper.unsafe.putFloat(ptr + index, value)
}
fun setInt(index: Long, value: Int) {
checkNullPtr(index)
UnsafeHelper.unsafe.putInt(ptr + index, value)
}
fun setShort(index: Long, value: Short) {
checkNullPtr(index)
UnsafeHelper.unsafe.putShort(ptr + index, value)
}
fun fillWith(byte: Byte) {
UnsafeHelper.unsafe.setMemory(ptr, size, byte)
}
override fun toString() = "0x${ptr.toString(16)} with size $size"
override fun equals(other: Any?) = this.ptr == (other as UnsafePtr).ptr && this.size == other.size
inline fun printStackTrace(obj: Any) = printStackTrace(obj, System.out) // because of Java
fun printStackTrace(obj: Any, out: PrintStream = System.out) {
Thread.currentThread().stackTrace.forEach {
out.println("[${obj.javaClass.simpleName}] ... $it")
}
}
}

View File

@@ -0,0 +1,215 @@
package net.torvald.tsvm
import net.torvald.UnsafeHelper
import net.torvald.UnsafePtr
import net.torvald.tsvm.peripheral.IOSpace
import net.torvald.tsvm.peripheral.PeriBase
import net.torvald.tsvm.peripheral.VMProgramRom
import java.io.InputStream
import java.io.OutputStream
import java.util.*
import kotlin.math.ceil
class ErrorIllegalAccess(vm: VM, addr: Long) : RuntimeException("Segmentation fault at 0x${addr.toString(16).padStart(8, '0')} on VM id ${vm.id}")
/**
* A class representing an instance of a Virtual Machine
*/
class VM(
_memsize: Long,
val worldInterface: WorldInterface,
val roms: Array<VMProgramRom?> // first ROM must contain the BIOS
) {
val id = java.util.Random().nextInt()
val memsize = minOf(USER_SPACE_SIZE, _memsize.toLong())
private val MALLOC_UNIT = 64
private val mallocBlockSize = (memsize / MALLOC_UNIT).toInt()
internal val usermem = UnsafeHelper.allocate(memsize)
val peripheralTable = Array(8) { PeripheralEntry() }
fun getIO(): IOSpace = peripheralTable[0].peripheral as IOSpace
//lateinit var printStream: OutputStream
//lateinit var errorStream: OutputStream
//lateinit var inputStream: InputStream // InputStream should not be a singleton, as it HAS TO open and close the stream.
// Printstreams don't need that so they're singleton.
var getPrintStream: () -> OutputStream = { TODO() }
var getErrorStream: () -> OutputStream = { TODO() }
var getInputStream: () -> InputStream = { TODO() }
var startTime: Long = -1
var resetDown = false
var stopDown = false
var romMapping = 255
internal set
init {
init()
}
fun init() {
peripheralTable[0] = PeripheralEntry(
IOSpace(this),
HW_RESERVE_SIZE,
MMIO_SIZE.toInt() - 256,
64
)
println("[VM] Creating new VM with ID of $id, memsize $memsize")
startTime = System.nanoTime()
}
fun findPeribyType(searchTerm: String): PeripheralEntry? {
for (i in 0..7) {
if (peripheralTable[i].type == searchTerm) return peripheralTable[i]
}
return null
}
fun update(delta: Float) {
getIO().update(delta)
}
fun dispose() {
usermem.destroy()
peripheralTable.forEach { it.peripheral?.dispose() }
}
open fun getUptime() = System.nanoTime() - startTime
/*
NOTE: re-fill peripheralTable whenever the VM cold-boots!
you are absolutely not supposed to hot-swap peripheral cards when the computer is on
*/
companion object {
val CHARSET = Charsets.ISO_8859_1
val MMIO_SIZE = 128.kB()
val HW_RESERVE_SIZE = 1024.kB()
val USER_SPACE_SIZE = 8192.kB()
const val PERITYPE_GPU_AND_TERM = "gpu"
}
internal fun translateAddr(addr: Long): Pair<Any?, Long> {
return when (addr) {
// DO note that numbers in Lua are double precision floats (ignore Lua 5.3 for now)
in 0..8192.kB() - 1 -> usermem to addr
in -1024.kB()..-1 -> peripheralTable[0].peripheral to (-addr - 1)
in -2048.kB()..-1024.kB() - 1 -> peripheralTable[1].peripheral to (-addr - 1 - 1024.kB())
in -3072.kB()..-2048.kB() - 1 -> peripheralTable[2].peripheral to (-addr - 1 - 2048.kB())
in -4096.kB()..-3072.kB() - 1 -> peripheralTable[3].peripheral to (-addr - 1 - 3072.kB())
in -5120.kB()..-4096.kB() - 1 -> peripheralTable[4].peripheral to (-addr - 1 - 4096.kB())
in -6144.kB()..-5120.kB() - 1 -> peripheralTable[5].peripheral to (-addr - 1 - 5120.kB())
in -7168.kB()..-6144.kB() - 1 -> peripheralTable[6].peripheral to (-addr - 1 - 6144.kB())
in -8192.kB()..-7168.kB() - 1 -> peripheralTable[7].peripheral to (-addr - 1 - 7168.kB())
else -> null to addr
}
}
fun poke(addr: Long, value: Byte) {
val (memspace, offset) = translateAddr(addr)
if (memspace == null)
throw ErrorIllegalAccess(this, addr)
else if (memspace is UnsafePtr) {
if (addr >= memspace.size)
throw ErrorIllegalAccess(this, addr)
else
memspace.set(offset, value)
}
else
(memspace as PeriBase).poke(offset, value)
}
fun peek(addr:Long): Byte? {
val (memspace, offset) = translateAddr(addr)
return if (memspace == null)
null
else if (memspace is UnsafePtr) {
if (addr >= memspace.size)
throw ErrorIllegalAccess(this, addr)
else
memspace.get(offset)
}
else
(memspace as PeriBase).peek(offset)
}
private val mallocMap = BitSet(mallocBlockSize)
private val mallocSizes = HashMap<Int, Int>() // HashMap<Block Index, Block Count>
private fun findEmptySpace(blockSize: Int): Int? {
var cursorHead = 0
var cursorTail: Int
val cursorHeadMaxInclusive = mallocBlockSize - blockSize
while (cursorHead <= cursorHeadMaxInclusive) {
cursorHead = mallocMap.nextClearBit(cursorHead)
cursorTail = cursorHead + blockSize - 1
if (cursorTail > mallocBlockSize) return null
if (mallocMap.get(cursorTail) == false) {
var isNotEmpty = false
for (k in cursorHead..cursorTail) {
isNotEmpty = isNotEmpty or mallocMap[k]
}
if (!isNotEmpty) {
mallocMap.set(cursorHead, cursorTail + 1)
return cursorHead
}
}
cursorHead = cursorTail + 1
}
return null
}
internal fun malloc(size: Int): Int {
val allocBlocks = ceil(size.toDouble() / MALLOC_UNIT).toInt()
val blockStart = findEmptySpace(allocBlocks) ?: throw OutOfMemoryError()
mallocSizes[blockStart] = allocBlocks
return blockStart * MALLOC_UNIT
}
internal fun free(ptr: Int) {
val index = ptr / MALLOC_UNIT
val count = mallocSizes[index] ?: throw OutOfMemoryError()
mallocMap.set(index, index + count, false)
mallocSizes.remove(index)
}
internal data class VMNativePtr(val address: Int, val size: Int)
}
class PeripheralEntry(
val peripheral: PeriBase? = null,
val memsize: Long = 0,
val mmioSize: Int = 0,
val interruptCount: Int = 0, // max: 4
) {
val type = peripheral?.typestring
}
class PeripheralEntry2(
val memsize: Long = 0,
val mmioSize: Int = 0,
val interruptCount: Int = 0, // max: 4
val peripheralClassname: String,
vararg val args: Any
)
fun Int.kB() = this * 1024L

View File

@@ -0,0 +1,145 @@
package net.torvald.tsvm
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.toUlong
import java.nio.charset.Charset
/**
* Pass the instance of the class to the ScriptEngine's binding, preferably under the namespace of "vm"
*/
class VMJSR223Delegate(val vm: VM) {
fun poke(addr: Int, value: Int) = vm.poke(addr.toLong(), value.toByte())
fun peek(addr: Int) = vm.peek(addr.toLong())!!.toInt().and(255)
fun nanoTime() = System.nanoTime()
fun malloc(size: Int) = vm.malloc(size)
fun free(ptr: Int) = vm.free(ptr)
fun mapRom(slot: Int) {
vm.romMapping = slot.and(255)
}
fun romReadAll(): String {
if (vm.romMapping == 255 || vm.romMapping !in vm.roms.indices || vm.roms[vm.romMapping] == null) return ""
return vm.roms[vm.romMapping]!!.readAll()
}
fun uptime(): Long {
vm.poke(-69, -1)
var r = 0L
for (i in 0L..7L) {
r = r or vm.peek(-73 - i)!!.toUlong().shl(8 * i.toInt())
}
return r
}
fun currentTimeInMills(): Long {
vm.poke(-69, -1)
var r = 0L
for (i in 0L..7L) {
r = r or vm.peek(-81 - i)!!.toUlong().shl(8 * i.toInt())
}
return r
}
fun print(s: Any) {
//System.out.print("[Nashorn] $s")
//System.out.print(s)
vm.getPrintStream().write("$s".toByteArray(VM.CHARSET))
}
fun println(s: Any = "") {
System.out.println("[Graal] $s")
//System.out.println(s)
vm.getPrintStream().write(("$s\n").toByteArray(VM.CHARSET))
}
/**
* @return key being hit, of which:
* a-zA-Z1-9: corresponding ASCII code
*
* Up: 200
* Left: 203
* Down: 208
* Right: 205
*
* PgUp: 201
* PgDn: 209
* Home: 199
* End: 207
* Ins: 201
* Del: 211
*
* Return: 13 (^M)
* Bksp: 8 (^H)
*
* ^A-^Z: 1 through 26
*/
fun readKey(): Int {
val inputStream = vm.getInputStream()
var key: Int = inputStream.read()
inputStream.close()
return key
}
/**
* Read series of key inputs until Enter/Return key is pressed. Backspace will work but any other non-printable
* characters (e.g. arrow keys) won't work.
*/
fun read(): String {
val inputStream = vm.getInputStream()
val sb = StringBuilder()
var key: Int
do {
key = inputStream.read()
if ((key == 8 && sb.isNotEmpty()) || key in 0x20..0x7E) {
this.print("${key.toChar()}")
}
when (key) {
8 -> if (sb.isNotEmpty()) sb.deleteCharAt(sb.lastIndex)
in 0x20..0x7E -> sb.append(key.toChar())
}
} while (key != 13 && key != 10)
this.print("\n") // printout \n
inputStream.close()
return sb.toString()
}
/**
* Read series of key inputs until Enter/Return key is pressed. Backspace will work but any other non-printable
* characters (e.g. arrow keys) won't work.
*/
fun readNoEcho(): String {
val inputStream = vm.getInputStream()
val sb = StringBuilder()
var key: Int
do {
key = inputStream.read()
when (key) {
8 -> if (sb.isNotEmpty()) sb.deleteCharAt(sb.lastIndex)
in 0x20..0x7E -> sb.append(key.toChar())
}
} while (key != 13 && key != 10)
this.println() // printout \n
inputStream.close()
return sb.toString()
}
fun spin() {
Thread.sleep(4L);
}
fun waitForMemChg(addr: Int, andMask: Int, xorMask: Int) {
while ((peek(addr) xor xorMask) and andMask == 0) {
spin();
}
}
fun waitForMemChg(addr: Int, andMask: Int) = waitForMemChg(addr, andMask, 0)
}
class VMSerialDebugger(val vm: VM) {
fun print(s: Any?) = System.out.print("$s")
fun println(s: Any?) = System.out.println("$s")
fun printerr(s: Any?) = System.err.println("$s")
}

View File

@@ -0,0 +1,103 @@
package net.torvald.tsvm
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.HostAccess
import java.io.FileReader
import javax.script.ScriptEngineManager
abstract class VMRunner(val extension: String) {
abstract suspend fun executeCommand(command: String)
abstract suspend fun evalGlobal(command: String)
abstract fun close()
}
object VMRunnerFactory {
private var firstTime = true
operator fun invoke(vm: VM, extension: String): VMRunner {
if (firstTime) {
firstTime = false
ScriptEngineManager().engineFactories.forEach {
println("[VMRunnerFactory] ext: ${it.extensions}, name: ${it.engineName}")
}
}
return when (extension) {
/*"vt2" -> {
object : VMRunner(extension) {
val engine =
Videotron2K(vm.findPeribyType(VM.PERITYPE_GPU_AND_TERM)!!.peripheral!! as GraphicsAdapter)
override suspend fun executeCommand(command: String) {
engine.eval(command)
}
override suspend fun evalGlobal(command: String) {
TODO("Not yet implemented")
}
override fun close() {
TODO("Not yet implemented")
}
}
}*/
"js" -> {
object : VMRunner(extension) {
private val context = Context.newBuilder("js")
.allowHostAccess(HostAccess.ALL)
.allowHostClassLookup { false }
.allowIO(false)
.build()
private val bind = context.getBindings("js")
init {
// see https://github.com/graalvm/graaljs/blob/master/docs/user/ScriptEngine.md
bind.putMember("polyglot.js.allowHostAccess", true)
bind.putMember("js.console", false)
bind.putMember("sys", VMJSR223Delegate(vm)) // TODO use delegator class to access peripheral (do not expose VM itself)
bind.putMember("graphics", GraphicsJSR223Delegate(vm))
bind.putMember("serial", VMSerialDebugger(vm))
bind.putMember("gzip", CompressorDelegate)
bind.putMember("base64", Base64Delegate)
bind.putMember("com", SerialHelperDelegate(vm))
bind.putMember("dma", DMADelegate(vm))
val fr = FileReader("./assets/JS_INIT.js")
val prg = fr.readText()
fr.close()
context.eval("js", sanitiseJS(prg))
}
override suspend fun executeCommand(command: String) {
try {
context.eval("js", encapsulateJS(sanitiseJS(command)))
}
catch (e: javax.script.ScriptException) {
System.err.println("ScriptException from the script:")
System.err.println(command.substring(0, minOf(1024, command.length)))
throw e
}
}
override suspend fun evalGlobal(command: String) {
context.eval("js", "\"use strict\";" + sanitiseJS(command))
}
override fun close() {
context.close(true)
}
}
}
else -> throw UnsupportedOperationException("Unsupported script extension: $extension")
}
}
private fun sanitiseJS(code: String) = code//.replace("\\", "\\\\")
private fun encapsulateJS(code: String) = "\"use strict\";(function(){$code})()"
}

View File

@@ -0,0 +1,12 @@
package net.torvald.tsvm
interface WorldInterface {
fun currentTimeInMills(): Long
}
/**
* Real world interface for non-ingame testing. For the Ingame, implement your own.
*/
class TheRealWorld : WorldInterface {
override fun currentTimeInMills(): Long = System.currentTimeMillis()
}

View File

@@ -0,0 +1,101 @@
package net.torvald.tsvm.peripheral
abstract class BlockTransferInterface(val isMaster: Boolean, val isSlave: Boolean) {
protected var recipient: BlockTransferInterface? = null
@Volatile var ready = true
@Volatile var busy = false
@Volatile var statusCode = 0
protected var sendmode = false; private set
@Volatile var blockSize = 0
open fun attachDevice(device: BlockTransferInterface?) {
recipient = device
device?.recipient = this
}
open fun areYouReady(): Boolean = recipient?.ready ?: false
open fun areYouBusy(): Boolean = recipient?.busy ?: false
/** Writes a thing to the recipient.
* A method exposed to outside of the box
* @return number of bytes actually sent over*/
abstract fun startSendImpl(recipient: BlockTransferInterface): Int
/** The actual implementation */
fun startSend() {
//if (areYouReady()) {
busy = true
ready = false
recipient?.let {
this.blockSize = startSendImpl(it)
//println("[BlockTransferInterface.startSend()] recipients blocksize = ${this.blockSize}")
}
busy = false
ready = true
//}
//else {
// throw IOException("${this.javaClass.canonicalName}: Device '${recipient?.javaClass?.canonicalName}' is not ready to receive")
//}
}
/** Ask the recipient to start send its thing to me so that I can 'read'
*/
open fun startRead() {
recipient?.startSend()
}
/** A method called by the sender so it can ACTUALLY write its thing onto me.
*
* @param inputData received message, usually 4096 bytes long and null-padded */
abstract fun writeoutImpl(inputData: ByteArray)
/** The actual implementation; must be called by a sender class */
fun writeout(inputData: ByteArray) {
busy = true
ready = false
blockSize = minOf(inputData.size, BLOCK_SIZE)
writeoutImpl(inputData)
busy = false
ready = true
}
abstract fun hasNext(): Boolean
open fun doYouHaveNext(): Boolean = recipient?.hasNext() ?: false
open fun yourBlockSize(): Int = recipient?.blockSize ?: 0
fun getYourStatusCode() = recipient?.statusCode ?: 0
/** @param sendmode TRUE for send, FALSE for receive */
open fun setMode(sendmode: Boolean) {
this.sendmode = sendmode
}
/** @return TRUE for send, FALSE for receive */
open fun getMode(): Boolean = sendmode
open fun cableConnected(): Boolean = recipient?.recipient == this
companion object {
const val BLOCK_SIZE = 4096
// these consts are UNUSABLE on writeoutImpl because wtf
// still possible to use on stringbuilder tho
const val GOOD_NEWS = 0x06.toByte()
const val BAD_NEWS = 0x15.toByte()
const val UNIT_SEP = 0x1F.toByte()
const val END_OF_SEND_BLOCK = 0x17.toByte()
}
}
fun ByteArray.trimNull(): ByteArray {
var cnt = BlockTransferInterface.BLOCK_SIZE - 1
while (cnt >= 0) {
if (this[cnt] != 0.toByte()) break
cnt -= 1
}
return this.sliceArray(0..cnt)
}
fun ByteArray.startsWith(other: ByteArray) = this.sliceArray(other.indices).contentEquals(other)

View File

@@ -0,0 +1,34 @@
package net.torvald.tsvm.peripheral
import net.torvald.UnsafeHelper
import net.torvald.tsvm.VM
/**
* Implementation of single COM port
*/
class BlockTransferPort(val vm: VM, val portno: Int) : BlockTransferInterface(true, false) {
internal var hasNext = false
override fun startSendImpl(recipient: BlockTransferInterface): Int {
recipient.writeout(ByteArray(BLOCK_SIZE) { vm.getIO().blockTransferTx[portno][it.toLong()] })
return blockSize // use MMIO to modify this variable
}
override fun hasNext(): Boolean = hasNext
override fun writeoutImpl(inputData: ByteArray) {
//val copySize = minOf(BLOCK_SIZE, inputData.size).toLong()
//val arrayOffset = UnsafeHelper.getArrayOffset(inputData)
//UnsafeHelper.memcpyRaw(inputData, arrayOffset, null, vm.getIO().blockTransferRx[portno].ptr, copySize)
// not exposing raw memory to block probable security hole
//println("[BlockTranferPort] writeout size: ${inputData.size}")
for (k in 0 until BLOCK_SIZE) {
vm.getIO().blockTransferRx[portno][k.toLong()] = if (k >= inputData.size) 0 else inputData[k]
}
}
}

View File

@@ -0,0 +1,90 @@
package net.torvald.tsvm.peripheral
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.toUlong
import net.torvald.tsvm.TextureRegionPack
import net.torvald.tsvm.VM
class CharacterLCDdisplay(vm: VM) : GraphicsAdapter(vm, AdapterConfig(
"pmlcd_inverted", 240, 64, 40, 8, 253, 255, 262144L, "lcd2.png", 0.7f, TEXT_TILING_SHADER_LCD, DRAW_SHADER_FRAG_LCD, 2f
)
) {
private val machine = Texture("./assets/4008_portable_full.png")
private val lcdFont = TextureRegionPack(Texture("./assets/lcd.png"), 12, 16)
/*override fun peek(addr: Long): Byte? {
return when (addr) {
in 0 until 250880 -> (-1).toByte()
else -> super.peek(addr)
}
}
override fun poke(addr: Long, byte: Byte) {
when (addr) {
in 0 until 250880 -> { /*do nothing*/ }
else -> super.poke(addr, byte)
}
}*/
override fun render(delta: Float, batch: SpriteBatch, xoff: Float, yoff: Float) {
batch.shader = null
batch.inUse {
batch.color = Color.WHITE
batch.draw(machine, xoff, yoff)
}
super.render(delta, batch, xoff+74, yoff+102)
// draw BMS and RTC
val batPerc = "89"
val batVolt = "5.1"
val batText = " $batPerc% ${batVolt}V"
val msg = (1024L until 1048L).map { vm.getIO().mmio_read(it)!!.toInt().and(255) }
vm.poke(-69,2)
val time_t = currentTimeInMills()
val min = (time_t / 60000) % 60
val hour = (time_t / 3600000) % 24
val clock = "${"$hour".padStart(2,'0')}:${"$min".padStart(2,'0')} "
batch.shader = null
batch.inUse {
batch.color = Color.WHITE
val y = yoff + 102 + config.height * config.drawScale
for (x in 0 until config.textCols) {
batch.draw(lcdFont.get(0,0), xoff+74 + x * lcdFont.tileW, y)
}
for (x in clock.indices) {
val ccode = clock[x].toInt()
batch.draw(lcdFont.get(ccode % 16, ccode / 16), xoff+74 + x * lcdFont.tileW, y)
}
for (x in msg.indices) {
val ccode = msg[x]
batch.draw(lcdFont.get(ccode % 16, ccode / 16), xoff+74 + (x + 6) * lcdFont.tileW, y)
}
for (x in batText.indices) {
val ccode = batText[x].toInt()
batch.draw(lcdFont.get(ccode % 16, ccode / 16), xoff+74 + (config.textCols - batText.length + x) * lcdFont.tileW, y)
}
}
}
fun currentTimeInMills(): Long {
vm.poke(-69, -1)
var r = 0L
for (i in 0L..7L) {
r = r or vm.peek(-81 - i)!!.toUlong().shl(8 * i.toInt())
}
return r
}
override fun dispose() {
machine.dispose()
lcdFont.dispose()
super.dispose()
}
}

View File

@@ -0,0 +1,155 @@
package net.torvald.tsvm.peripheral
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.math.Matrix4
import net.torvald.tsvm.LoadShader
import net.torvald.tsvm.VM
/**
* External Display that is always visible through its own UI ingame.
*
* Created by minjaesong on 2021-12-01.
*/
class ExtDisp(val vm: VM, val width: Int, val height: Int) : PeriBase {
constructor(vm: VM, w: java.lang.Integer, h: java.lang.Integer) : this(
vm, w.toInt(), h.toInt()
)
override val typestring = "oled"
override fun getVM(): VM {
return vm
}
internal val framebuffer = Pixmap(width, height, Pixmap.Format.Alpha)
private val outFBObatch = SpriteBatch()
protected val drawShader = LoadShader(GraphicsAdapter.DRAW_SHADER_VERT, OLED_PAL_SHADER)
init {
// no orthographic camera, must be "raw" Matrix4
val m = Matrix4()
m.setToOrtho2D(0f, 0f, width.toFloat(), height.toFloat())
outFBObatch.projectionMatrix = m
framebuffer.blending = Pixmap.Blending.None
framebuffer.setColor(0)
framebuffer.fill()
}
private lateinit var tex: Texture
open fun render(uiBatch: SpriteBatch, xoff: Float, yoff: Float) {
framebuffer.pixels.position(0)
tex = Texture(framebuffer)
uiBatch.inUse {
uiBatch.color = Color.WHITE
uiBatch.shader = drawShader
uiBatch.draw(tex, xoff, yoff)
}
tex.dispose()
}
/**
* Get the next power of two of the given number.
*
* E.g. for an input 100, this returns 128.
* Returns 1 for all numbers <= 1.
*
* @param number The number to obtain the POT for.
* @return The next power of two.
*/
private fun nextPowerOfTwo(number: Int): Int {
var number = number
number--
number = number or (number shr 1)
number = number or (number shr 2)
number = number or (number shr 4)
number = number or (number shr 8)
number = number or (number shr 16)
number++
number += if (number == 0) 1 else 0
return number
}
override fun peek(addr: Long): Byte? {
val adi = addr.toInt()
return when (addr) {
in 0 until width * height -> {
framebuffer.pixels.get(adi)
}
in 0 until nextPowerOfTwo(width * height) -> { null }
else -> peek(addr % nextPowerOfTwo(width * height))
}
}
override fun poke(addr: Long, byte: Byte) {
val adi = addr.toInt()
val bi = byte.toInt().and(255)
when (addr) {
in 0 until width * height -> {
framebuffer.pixels.put(adi, byte)
}
(width * height).toLong() -> {
framebuffer.setColor(bi.shl(24) or bi.shr(16) or bi.shl(8) or bi)
framebuffer.fill()
}
in 0 until nextPowerOfTwo(width * height) -> { /* do nothing */ }
else -> poke(addr % nextPowerOfTwo(width * height), byte)
}
}
override fun mmio_read(addr: Long): Byte? {
TODO("Not yet implemented")
}
override fun mmio_write(addr: Long, byte: Byte) {
TODO("Not yet implemented")
}
override fun dispose() {
try { framebuffer.dispose() } catch (e: Throwable) {}
try { tex.dispose() } catch (e: Throwable) {}
}
companion object {
val OLED_PAL_SHADER = """
#version 130
varying vec4 v_color;
varying vec2 v_texCoords;
uniform sampler2D u_texture;
vec4 pal[16] = vec4[](
vec4(0.0,0.0,0.0,1.0),
vec4(0.0,0.1765,0.6667,1.0),
vec4(0.0,0.6667,0.0,1.0),
vec4(0.0,0.7255,0.6667,1.0),
vec4(0.6667,0.0,0.0,1.0),
vec4(0.6667,0.1765,0.6667,1.0),
vec4(0.6667,0.6667,0.0,1.0),
vec4(0.6667,0.6667,0.6667,1.0),
vec4(0.0,0.0,0.0,1.0),
vec4(0.0,0.2667,1.0,1.0),
vec4(0.0,1.0,0.0,1.0),
vec4(0.0,1.0,1.0,1.0),
vec4(1.0,0.0,0.0,1.0),
vec4(1.0,0.2667,1.0,1.0),
vec4(1.0,1.0,0.0,1.0),
vec4(1.0,1.0,1.0,1.0)
);
void main(void) {
gl_FragColor = pal[int(texture2D(u_texture, v_texCoords).a * 255.0) % 16];
}
"""
}
}

View File

@@ -0,0 +1,357 @@
package net.torvald.tsvm.peripheral
import java.io.InputStream
import java.io.OutputStream
import java.util.*
/**
* Implements standard TTY that can interpret some of the ANSI escape sequences
*
* A paper tty must be able to implemented by extending this class (and butchering some of the features), of which it
* sets limits on some of the functions (notably 'setCursorPos')
*/
abstract class GlassTty(val TEXT_ROWS: Int, val TEXT_COLS: Int) {
/**
* (x, y)
*/
abstract fun getCursorPos(): Pair<Int, Int>
/**
* Think of it as a real paper tty;
* setCursorPos must "wrap" the cursor properly when x-value goes out of screen bound.
* For y-value, only when y < 0, set y to zero and don't care about the y-value goes out of bound.
*/
abstract fun setCursorPos(x: Int, y: Int)
abstract var rawCursorPos: Int
abstract var blinkCursor: Boolean
abstract var ttyFore: Int
abstract var ttyBack: Int
abstract var ttyRawMode: Boolean
abstract fun putChar(x: Int, y: Int, text: Byte, foreColour: Byte = ttyFore.toByte(), backColour: Byte = ttyBack.toByte())
fun writeOut(char: Byte) {
val (cx, cy) = getCursorPos()
val printable = acceptChar(char) // this function processes the escape codes and CRLFs
if (printable) {
putChar(cx, cy, char)
setCursorPos(cx + 1, cy) // should automatically wrap and advance a line for out-of-bound x-value
}
}
private var ttyEscState = TTY_ESC_STATE.INITIAL
private val ttyEscArguments = Stack<Int>()
/**
* ONLY accepts a character to either process the escape sequence, or say the input character is allowed to print.
* This function will alter the internal state of the TTY intepreter (aka this very class)
*
* Any unrecognisable escape sequence will result the internal state to be reset but the character WILL NOT be marked
* as printable.
*
* @return true if character should be printed as-is
*/
private fun acceptChar(char: Byte): Boolean {
fun reject(): Boolean {
ttyEscState = TTY_ESC_STATE.INITIAL
ttyEscArguments.clear()
return true
}
fun accept(execute: () -> Unit): Boolean {
ttyEscState = TTY_ESC_STATE.INITIAL
execute.invoke()
ttyEscArguments.clear()
return false
}
fun registerNewNumberArg(newnum: Byte, newState: TTY_ESC_STATE) {
ttyEscArguments.push(char.toInt() - 0x30)
ttyEscState = newState
}
fun appendToExistingNumber(newnum: Byte) {
ttyEscArguments.push(ttyEscArguments.pop() * 10 + (newnum.toInt() - 0x30))
}
//println("[tty] accepting char $char, state: $ttyEscState")
when (ttyEscState) {
TTY_ESC_STATE.INITIAL -> {
when (char) {
ESC -> ttyEscState = TTY_ESC_STATE.ESC
LF -> crlf()
BS -> backspace()
TAB -> insertTab()
BEL -> ringBell()
in 0x00.toByte()..0x1F.toByte() -> return false
else -> return true
}
}
TTY_ESC_STATE.ESC -> {
when (char.toChar()) {
'c' -> return accept { resetTtyStatus() }
'[' -> ttyEscState = TTY_ESC_STATE.CSI
else -> return reject()
}
}
TTY_ESC_STATE.CSI -> {
when (char.toChar()) {
'A' -> return accept { cursorUp() }
'B' -> return accept { cursorDown() }
'C' -> return accept { cursorFwd() }
'D' -> return accept { cursorBack() }
'E' -> return accept { cursorNextLine() }
'F' -> return accept { cursorPrevLine() }
'G' -> return accept { cursorX() }
'J' -> return accept { eraseInDisp() }
'K' -> return accept { eraseInLine() }
'S' -> return accept { scrollUp() }
'T' -> return accept { scrollDown() }
'm' -> return accept { sgrOneArg() }
'?' -> ttyEscState = TTY_ESC_STATE.PRIVATESEQ
';' -> {
ttyEscArguments.push(0)
ttyEscState = TTY_ESC_STATE.SEP1
}
in '0'..'9' -> registerNewNumberArg(char, TTY_ESC_STATE.NUM1)
else -> return reject()
}
}
TTY_ESC_STATE.PRIVATESEQ -> {
when (char.toChar()) {
in '0'..'9' -> registerNewNumberArg(char, TTY_ESC_STATE.PRIVATENUM)
else -> return reject()
}
}
TTY_ESC_STATE.PRIVATENUM -> {
when (char.toChar()) {
'h' -> return accept { privateSeqH(ttyEscArguments.pop()) }
'l' -> return accept { privateSeqL(ttyEscArguments.pop()) }
in '0'..'9' -> appendToExistingNumber(char)
else -> return reject()
}
}
TTY_ESC_STATE.NUM1 -> {
when (char.toChar()) {
'A' -> return accept { cursorUp(ttyEscArguments.pop()) }
'B' -> return accept { cursorDown(ttyEscArguments.pop()) }
'C' -> return accept { cursorFwd(ttyEscArguments.pop()) }
'D' -> return accept { cursorBack(ttyEscArguments.pop()) }
'E' -> return accept { cursorNextLine(ttyEscArguments.pop()) }
'F' -> return accept { cursorPrevLine(ttyEscArguments.pop()) }
'G' -> return accept { cursorX(ttyEscArguments.pop()) }
'J' -> return accept { eraseInDisp(ttyEscArguments.pop()) }
'K' -> return accept { eraseInLine(ttyEscArguments.pop()) }
'S' -> return accept { scrollUp(ttyEscArguments.pop()) }
'T' -> return accept { scrollDown(ttyEscArguments.pop()) }
'm' -> return accept { sgrOneArg(ttyEscArguments.pop()) }
';' -> ttyEscState = TTY_ESC_STATE.SEP1
in '0'..'9' -> appendToExistingNumber(char)
else -> return reject()
}
}
TTY_ESC_STATE.NUM2 -> {
when (char.toChar()) {
in '0'..'9' -> appendToExistingNumber(char)
'H' -> return accept {
val arg2 = ttyEscArguments.pop()
val arg1 = ttyEscArguments.pop()
cursorXY(arg1, arg2)
}
'm' -> return accept {
val arg2 = ttyEscArguments.pop()
val arg1 = ttyEscArguments.pop()
sgrTwoArg(arg1, arg2)
}
';' -> ttyEscState = TTY_ESC_STATE.SEP2
else -> return reject()
}
}
TTY_ESC_STATE.NUM3 -> {
when (char.toChar()) {
in '0'..'9' -> appendToExistingNumber(char)
'm' -> return accept {
val arg3 = ttyEscArguments.pop()
val arg2 = ttyEscArguments.pop()
val arg1 = ttyEscArguments.pop()
sgrThreeArg(arg1, arg2, arg3)
}
else -> return reject()
}
}
TTY_ESC_STATE.SEP1 -> {
when (char.toChar()) {
in '0'..'9' -> registerNewNumberArg(char, TTY_ESC_STATE.NUM2)
'H' -> return accept {
val arg1 = ttyEscArguments.pop()
cursorXY(arg1, 0)
}
'm' -> return accept {
val arg1 = ttyEscArguments.pop()
sgrTwoArg(arg1, 0)
}
';' -> {
ttyEscArguments.push(0)
ttyEscState = TTY_ESC_STATE.SEP2
}
else -> return reject()
}
}
TTY_ESC_STATE.SEP2 -> {
when (char.toChar()) {
'm' -> return accept {
val arg2 = ttyEscArguments.pop()
val arg1 = ttyEscArguments.pop()
sgrThreeArg(arg1, arg2, 0)
}
in '0'..'9' -> registerNewNumberArg(char, TTY_ESC_STATE.NUM3)
else -> return reject()
}
}
}
return false
}
abstract fun resetTtyStatus()
abstract fun cursorUp(arg: Int = 1)
abstract fun cursorDown(arg: Int = 1)
abstract fun cursorFwd(arg: Int = 1)
abstract fun cursorBack(arg: Int = 1)
abstract fun cursorNextLine(arg: Int = 1)
abstract fun cursorPrevLine(arg: Int = 1)
abstract fun cursorX(arg: Int = 1) // aka Cursor Horizintal Absolute
abstract fun eraseInDisp(arg: Int = 0)
abstract fun eraseInLine(arg: Int = 0)
/** New lines are added at the bottom */
abstract fun scrollUp(arg: Int = 1)
/** New lines are added at the top */
abstract fun scrollDown(arg: Int = 1)
abstract fun sgrOneArg(arg: Int = 0)
abstract fun sgrTwoArg(arg1: Int, arg2: Int)
abstract fun sgrThreeArg(arg1: Int, arg2: Int, arg3: Int)
/** The values are one-based
* @param arg1 y-position (row)
* @param arg2 x-position (column) */
abstract fun cursorXY(arg1: Int, arg2: Int)
abstract fun ringBell()
abstract fun insertTab()
abstract fun crlf()
abstract fun backspace()
abstract fun privateSeqH(arg: Int)
abstract fun privateSeqL(arg: Int)
abstract fun getPrintStream(): OutputStream
abstract fun getErrorStream(): OutputStream
abstract fun getInputStream(): InputStream
private val CR = 0x0D.toByte()
private val LF = 0x0A.toByte()
private val TAB = 0x09.toByte()
private val BS = 0x08.toByte()
private val BEL = 0x07.toByte()
private val ESC = 0x1B.toByte()
private enum class TTY_ESC_STATE {
INITIAL, ESC, CSI, NUM1, SEP1, NUM2, SEP2, NUM3, PRIVATESEQ, PRIVATENUM
}
/**
* Puts a key into a keyboard buffer
*/
abstract fun putKey(key: Int)
/**
* Takes a key from a keyboard buffer
*/
abstract fun takeKey(): Int
}
/*
Note 1. State machine for Escape sequence
digraph G {
ESC -> Reset [label="c"]
ESC -> CSI [label="["]
CSI -> numeral [label="0..9"]
CSI -> CursorUp [label="A"]
CSI -> CursorDown [label="B"]
CSI -> CursorFwd [label="C"]
CSI -> CursorBack [label="D"]
CSI -> CursorNextLine [label="E"]
CSI -> CursorPrevLine [label="F"]
CSI -> CursorX [label="G"]
CSI -> EraseInDisp [label="J"]
CSI -> EraseInLine [label="K"]
CSI -> ScrollUp [label="S"]
CSI -> ScrollDown [label="T"]
CSI -> SGR [label="m"]
CSI -> separator1 [label="; (zero)"]
CSI -> privateseq [label="?"]
privateseq -> privatenum [label="0..9"]
privatenum -> privateSeqH [label=h]
privatenum -> privateSeqL [label=l]
numeral -> numeral [label="0..9"]
numeral -> CursorUp [label="A"]
numeral -> CursorDown [label="B"]
numeral -> CursorFwd [label="C"]
numeral -> CursorBack [label="D"]
numeral -> CursorNextLine [label="E"]
numeral -> CursorPrevLine [label="F"]
numeral -> CursorX [label="G"]
numeral -> EraseInDisp [label="J"]
numeral -> EraseInLine [label="K"]
numeral -> ScrollUp [label="S"]
numeral -> ScrollDown [label="T"]
numeral -> SGR [label="m"]
numeral -> separator1 [label=";"]
separator1 -> numeral2 [label="0..9"]
separator1 -> separator2 [label="; (zero)"]
separator1 -> CursorPos [label="H (zero)"]
separator1 -> SGR2 [label="m (zero)"]
numeral2 -> numeral2 [label="0..9"]
numeral2 -> CursorPos [label="H"]
numeral2 -> SGR2 [label="m"]
numeral2 -> separator2 [label="; (zero)"]
separator2 -> numeral3 [label="0..9"]
numeral3 -> numeral3 [label="0..9"]
separator2 -> SGR3 [label="m (zero)"]
numeral3 -> SGR3 [label="m"]
ESC [shape=Mdiamond]
Reset -> end
CursorUp -> end
CursorDown -> end
CursorFwd -> end
CursorBack -> end
CursorNextLine -> end
CursorPrevLine -> end
CursorX -> end
EraseInDisp -> end
EraseInLine -> end
ScrollUp -> end
ScrollDown -> end
CursorPos -> end
SGR -> end
SGR2 -> end
SGR3 -> end
end [shape=Msquare]
}
*/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,344 @@
package net.torvald.tsvm.peripheral
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Input
import com.badlogic.gdx.InputProcessor
import net.torvald.UnsafeHelper
import net.torvald.tsvm.CircularArray
import net.torvald.tsvm.VM
import kotlin.experimental.and
class IOSpace(val vm: VM) : PeriBase, InputProcessor {
override val typestring = "io"
override fun getVM(): VM {
return vm
}
/** Absolute x-position of the computer GUI */
var guiPosX = 0
/** Absolute y-position of the computer GUI */
var guiPosY = 0
/** Accepts a keycode */
private val keyboardBuffer = CircularArray<Byte>(32, true)
internal val blockTransferRx = arrayOf(
UnsafeHelper.allocate(4096),
UnsafeHelper.allocate(4096),
UnsafeHelper.allocate(4096),
UnsafeHelper.allocate(4096)
)
internal val blockTransferTx = arrayOf(
UnsafeHelper.allocate(4096),
UnsafeHelper.allocate(4096),
UnsafeHelper.allocate(4096),
UnsafeHelper.allocate(4096)
)
/*private*/ val blockTransferPorts = Array(4) { BlockTransferPort(vm, it) }
private val peripheralFast = UnsafeHelper.allocate(1024)
private val keyEventBuffers = ByteArray(8)
init {
//blockTransferPorts[1].attachDevice(TestFunctionGenerator())
//blockTransferPorts[0].attachDevice(TestDiskDrive(vm, 0, File("assets")))
//blockTransferPorts[0].attachDevice(TestDiskDrive(vm, 0, File("assets/disk0")))
// for testers: use EmulInstance
peripheralFast.fillWith(0)
}
private fun composeBlockTransferStatus(portno: Int): Int {
return blockTransferPorts[portno].isMaster.toInt().shl(5) or
blockTransferPorts[portno].isSlave.toInt().shl(4) or
blockTransferPorts[portno].getMode().toInt().shl(3) or
blockTransferPorts[portno].busy.toInt().shl(2) or
blockTransferPorts[portno].areYouReady().toInt().shl(1) or
blockTransferPorts[portno].cableConnected().toInt()
}
private fun setBlockTransferPortStatus(portno: Int, bits: Byte) {
blockTransferPorts[portno].setMode(bits.and(0b0000_1000) != 0.toByte())
blockTransferPorts[portno].ready = bits.and(0b0000_0010) != 0.toByte()
if (bits.and(0b0000_0100) != 0.toByte()) {
if (blockTransferPorts[portno].getMode()) {
//println("[IOSpace] startSend()")
blockTransferPorts[portno].startSend()
}
else {
//println("[IOSpace] startRead()")
blockTransferPorts[portno].startRead()
}
}
}
override fun peek(addr: Long): Byte? {
return mmio_read(addr)
}
override fun poke(addr: Long, byte: Byte) {
mmio_write(addr, byte)
}
override fun mmio_read(addr: Long): Byte? {
val adi = addr.toInt()
return when (addr) {
in 0..31 -> keyboardBuffer[(addr.toInt())] ?: -1
in 32..33 -> (mouseX.toInt() shr (adi - 32).times(8)).toByte()
in 34..35 -> (mouseY.toInt() shr (adi - 34).times(8)).toByte()
36L -> mouseDown.toInt().toByte()
37L -> keyboardBuffer.removeTail() ?: -1
38L -> keyboardInputRequested.toInt().toByte()
39L -> rawInputFunctionLatched.toInt().toByte()
in 40..47 -> keyEventBuffers[adi - 40]
48L -> ((vm.resetDown.toInt() shl 7) or (vm.stopDown.toInt())).toByte()
in 64..67 -> vm.memsize.shr((adi - 64) * 8).toByte()
68L -> (uptimeCounterLatched.toInt() or RTClatched.toInt().shl(1)).toByte()
in 72..79 -> systemUptime.ushr((adi - 72) * 8).and(255).toByte()
in 80..87 -> rtc.ushr((adi - 80) * 8).and(255).toByte()
88L -> vm.romMapping.toByte()
in 1024..2047 -> peripheralFast[addr - 1024]
4076L -> blockTransferPorts[0].statusCode.toByte()
4077L -> blockTransferPorts[1].statusCode.toByte()
4078L -> blockTransferPorts[2].statusCode.toByte()
4079L -> blockTransferPorts[3].statusCode.toByte()
4080L -> blockTransferPorts[0].getYourStatusCode().toByte()
4081L -> blockTransferPorts[1].getYourStatusCode().toByte()
4082L -> blockTransferPorts[2].getYourStatusCode().toByte()
4083L -> blockTransferPorts[3].getYourStatusCode().toByte()
4084L -> (blockTransferPorts[0].yourBlockSize().toByte())
4085L -> (blockTransferPorts[0].doYouHaveNext().toInt().shl(7) or blockTransferPorts[0].yourBlockSize().ushr(8).and(15)).toByte()
4086L -> (blockTransferPorts[1].yourBlockSize().toByte())
4087L -> (blockTransferPorts[1].doYouHaveNext().toInt().shl(7) or blockTransferPorts[1].yourBlockSize().ushr(8).and(15)).toByte()
4088L -> (blockTransferPorts[2].yourBlockSize().toByte())
4089L -> (blockTransferPorts[2].doYouHaveNext().toInt().shl(7) or blockTransferPorts[2].yourBlockSize().ushr(8).and(15)).toByte()
4090L -> (blockTransferPorts[3].yourBlockSize().toByte())
4091L -> (blockTransferPorts[3].doYouHaveNext().toInt().shl(7) or blockTransferPorts[3].yourBlockSize().ushr(8).and(15)).toByte()
in 4092..4095 -> composeBlockTransferStatus(adi - 4092).toByte()
in 4096..8191 -> blockTransferRx[0][addr - 4096]
in 8192..12287 -> blockTransferRx[1][addr - 8192]
in 12288..16383 -> blockTransferRx[2][addr - 12288]
in 16384..20479 -> blockTransferRx[3][addr - 16384]
in 65536..131071 -> if (vm.romMapping == -1) 255.toByte() else vm.roms[vm.romMapping]?.get(adi - 65536)
in 131072..262143 -> vm.peripheralTable[1].peripheral?.mmio_read(addr - 131072)
in 262144..393215 -> vm.peripheralTable[2].peripheral?.mmio_read(addr - 262144)
in 393216..524287 -> vm.peripheralTable[3].peripheral?.mmio_read(addr - 393216)
in 524288..655359 -> vm.peripheralTable[4].peripheral?.mmio_read(addr - 524288)
in 655360..786431 -> vm.peripheralTable[5].peripheral?.mmio_read(addr - 655360)
in 786432..917503 -> vm.peripheralTable[6].peripheral?.mmio_read(addr - 786432)
in 917504..1048575 -> vm.peripheralTable[7].peripheral?.mmio_read(addr - 917504)
else -> null
}
}
override fun mmio_write(addr: Long, byte: Byte) {
val adi = addr.toInt()
val bi = byte.toInt().and(255)
when (addr) {
37L -> keyboardBuffer.appendHead(byte)
38L -> {
keyboardInputRequested = (byte.isNonZero())
if (keyboardInputRequested) keyboardBuffer.clear()
}
39L -> rawInputFunctionLatched = (byte.isNonZero())
in 40..47 -> keyEventBuffers[adi - 40] = byte
68L -> {
uptimeCounterLatched = byte.and(0b01).isNonZero()
RTClatched = byte.and(0b10).isNonZero()
}
88L -> vm.romMapping = bi
in 1024..2047 -> peripheralFast[addr - 1024] = byte
4076L -> blockTransferPorts[0].statusCode = bi
4077L -> blockTransferPorts[1].statusCode = bi
4078L -> blockTransferPorts[2].statusCode = bi
4079L -> blockTransferPorts[3].statusCode = bi
4084L -> blockTransferPorts[0].blockSize = blockTransferPorts[0].blockSize.and(0xFF00) or byte.toInt().and(255)
4085L -> {
blockTransferPorts[0].hasNext = (byte < 0)
blockTransferPorts[0].blockSize = blockTransferPorts[0].blockSize.and(0x00FF) or byte.toInt().and(15)
}
4086L -> blockTransferPorts[1].blockSize = blockTransferPorts[1].blockSize.and(0xFF00) or byte.toInt().and(255)
4087L -> {
blockTransferPorts[1].hasNext = (byte < 0)
blockTransferPorts[1].blockSize = blockTransferPorts[1].blockSize.and(0x00FF) or byte.toInt().and(15)
}
4088L -> blockTransferPorts[2].blockSize = blockTransferPorts[2].blockSize.and(0xFF00) or byte.toInt().and(255)
4089L -> {
blockTransferPorts[2].hasNext = (byte < 0)
blockTransferPorts[2].blockSize = blockTransferPorts[2].blockSize.and(0x00FF) or byte.toInt().and(15)
}
4090L -> blockTransferPorts[3].blockSize = blockTransferPorts[3].blockSize.and(0xFF00) or byte.toInt().and(255)
4091L -> {
blockTransferPorts[3].hasNext = (byte < 0)
blockTransferPorts[3].blockSize = blockTransferPorts[3].blockSize.and(0x00FF) or byte.toInt().and(15)
}
in 4092..4095 -> setBlockTransferPortStatus(adi - 4092, byte)
in 4096..8191 -> blockTransferTx[0][addr - 4096] = byte
in 8192..12287 -> blockTransferTx[1][addr - 8192] = byte
in 12288..16383 -> blockTransferTx[2][addr - 12288] = byte
in 16384..20479 -> blockTransferTx[3][addr - 16384] = byte
in 131072..262143 -> vm.peripheralTable[1].peripheral?.mmio_write(addr - 131072, byte)
in 262144..393215 -> vm.peripheralTable[2].peripheral?.mmio_write(addr - 262144, byte)
in 393216..524287 -> vm.peripheralTable[3].peripheral?.mmio_write(addr - 393216, byte)
in 524288..655359 -> vm.peripheralTable[4].peripheral?.mmio_write(addr - 524288, byte)
in 655360..786431 -> vm.peripheralTable[5].peripheral?.mmio_write(addr - 655360, byte)
in 786432..917503 -> vm.peripheralTable[6].peripheral?.mmio_write(addr - 786432, byte)
in 917504..1048575 -> vm.peripheralTable[7].peripheral?.mmio_write(addr - 917504, byte)
}
}
override fun dispose() {
blockTransferRx.forEach { it.destroy() }
blockTransferTx.forEach { it.destroy() }
peripheralFast.destroy()
}
private var mouseX: Short = 0
private var mouseY: Short = 0
private var mouseDown = false
private var systemUptime = 0L
private var rtc = 0L
fun update(delta: Float) {
if (rawInputFunctionLatched) {
rawInputFunctionLatched = false
// store mouse info
mouseX = (Gdx.input.x + guiPosX).toShort()
mouseY = (Gdx.input.y + guiPosY).toShort()
mouseDown = Gdx.input.isTouched
// strobe keys to fill the key read buffer
var keysPushed = 0
keyEventBuffers.fill(0)
for (k in 1..254) {
if (Gdx.input.isKeyPressed(k)) {
keyEventBuffers[keysPushed] = k.toByte()
keysPushed += 1
}
if (keysPushed >= 8) break
}
}
if (uptimeCounterLatched) {
uptimeCounterLatched = false
systemUptime = vm.getUptime()
}
if (RTClatched) {
RTClatched = false
rtc = vm.worldInterface.currentTimeInMills()
}
// SIGTERM key combination: Ctrl+Shift+T+R
vm.stopDown = Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) &&
Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT) &&
Gdx.input.isKeyPressed(Input.Keys.T) &&
Gdx.input.isKeyPressed(Input.Keys.R)
if (vm.stopDown) println("[VM-${vm.id}] SIGTERM requested")
// RESET key combination: Ctrl+Shift+R+S
vm.resetDown = Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) &&
Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT) &&
Gdx.input.isKeyPressed(Input.Keys.R) &&
Gdx.input.isKeyPressed(Input.Keys.S)
if (vm.resetDown) println("[VM-${vm.id}] RESET requested")
}
override fun touchUp(p0: Int, p1: Int, p2: Int, p3: Int): Boolean {
return false
}
override fun mouseMoved(p0: Int, p1: Int): Boolean {
return false
}
override fun keyTyped(p0: Char): Boolean {
if (keyboardInputRequested && p0.toInt() > 0) {
//println("[IO] key typed = ${p0.toInt()}")
keyboardBuffer.appendHead(p0.toByte())
return true
}
else {
return false
}
}
override fun scrolled(p0: Float, p1: Float): Boolean {
return false
}
override fun keyUp(p0: Int): Boolean {
//ttySpecialKeyLatched = false
return true
}
override fun touchDragged(p0: Int, p1: Int, p2: Int): Boolean {
return false
}
private var keyboardInputRequested = false
private var uptimeCounterLatched = false
private var RTClatched = false
private var rawInputFunctionLatched = false
private var specialKeys = hashMapOf(
Input.Keys.HOME to 199.toByte(),
Input.Keys.UP to 200.toByte(),
Input.Keys.PAGE_UP to 201.toByte(),
Input.Keys.LEFT to 203.toByte(),
Input.Keys.RIGHT to 205.toByte(),
Input.Keys.END to 207.toByte(),
Input.Keys.DOWN to 208.toByte(),
Input.Keys.PAGE_DOWN to 209.toByte(),
Input.Keys.INSERT to 210.toByte(),
Input.Keys.FORWARD_DEL to 211.toByte()
)
override fun keyDown(p0: Int): Boolean {
if (keyboardInputRequested) {
if (p0 in Input.Keys.A..Input.Keys.Z && (Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT) || Gdx.input.isKeyPressed(Input.Keys.CONTROL_RIGHT))) {
keyboardBuffer.appendHead((p0 - 28).toByte())
}
else {
specialKeys[p0]?.let {
//println("[IO] key special = ${it.toUInt()}")
keyboardBuffer.appendHead(it)
}
}
return true
}
else {
return false
}
}
override fun touchDown(p0: Int, p1: Int, p2: Int, p3: Int): Boolean {
return false
}
private fun Boolean.toInt() = if (this) 1 else 0
private fun Byte.isNonZero() = this != 0.toByte()
}

View File

@@ -0,0 +1,25 @@
package net.torvald.tsvm.peripheral
import net.torvald.tsvm.VM
interface PeriBase {
/**
* Addr is not an offset; they can be "wired" into any other "chip" in the card other than its RAM
*/
fun peek(addr: Long): Byte?
/**
* Addr is not an offset; they can be "wired" into any other "chip" in the card other than its RAM
*/
fun poke(addr: Long, byte: Byte)
fun mmio_read(addr: Long): Byte?
fun mmio_write(addr: Long, byte: Byte)
fun dispose()
fun getVM(): VM
val typestring: String
}

View File

@@ -0,0 +1,247 @@
package net.torvald.tsvm.peripheral
import com.badlogic.gdx.graphics.Texture
import net.torvald.UnsafeHelper
import net.torvald.tsvm.VM
import java.io.InputStream
import java.io.OutputStream
class TTY(val vm: VM) : GlassTty(TEXT_ROWS, TEXT_COLS), PeriBase {
override val typestring = VM.PERITYPE_GPU_AND_TERM
companion object {
const val TEXT_ROWS = 25
const val TEXT_COLS = 80
}
private val chrrom = Texture("./assets/tty.png")
private val textBuffer = UnsafeHelper.allocate(TEXT_ROWS * TEXT_COLS * 2L)
override var rawCursorPos = 0
private val TEXT_AREA_SIZE = TEXT_COLS * TEXT_ROWS
private val memTextOffset = 0L
private val memTextAttrOffset = TEXT_AREA_SIZE.toLong()
override var ttyFore = 0 // 0: normal, 1: intense, 2: dim
override var ttyBack: Int
get() = 0
set(value) {}
var ttyInv = false
override var blinkCursor = true
override var ttyRawMode = false
override fun getCursorPos() = rawCursorPos % TEXT_COLS to rawCursorPos / TEXT_COLS
/**
* Think of it as a real paper tty;
* setCursorPos must "wrap" the cursor properly when x-value goes out of screen bound.
* For y-value, only when y < 0, set y to zero and don't care about the y-value goes out of bound.
*/
override fun setCursorPos(x: Int, y: Int) {
var newx = x
var newy = y
if (newx >= TEXT_COLS) {
newx = 0
newy += 1
}
else if (newx < 0) {
newx = 0
}
if (newy < 0) {
newy = 0 // DON'T SCROLL when cursor goes ABOVE the screen
}
rawCursorPos = toTtyTextOffset(newx, newy)
}
private fun toTtyTextOffset(x: Int, y: Int) = y * TEXT_COLS + x
override fun peek(addr: Long): Byte? {
TODO("Not yet implemented")
}
override fun poke(addr: Long, byte: Byte) {
TODO("Not yet implemented")
}
override fun mmio_read(addr: Long): Byte? {
TODO("Not yet implemented")
}
override fun mmio_write(addr: Long, byte: Byte) {
TODO("Not yet implemented")
}
override fun getVM() = vm
override fun putChar(x: Int, y: Int, text: Byte, foreColour: Byte, backColour: Byte) {
val textOff = toTtyTextOffset(x, y)
textBuffer[memTextAttrOffset + textOff] = 0
textBuffer[memTextOffset + textOff] = text
}
override fun resetTtyStatus() {
ttyFore = 0
ttyInv = false
}
override fun cursorUp(arg: Int) {
val (x, y) = getCursorPos()
setCursorPos(x, y - arg)
}
override fun cursorDown(arg: Int) {
val (x, y) = getCursorPos()
val newy = y + arg
setCursorPos(x, if (newy >= TEXT_ROWS) TEXT_ROWS - 1 else newy)
}
override fun cursorFwd(arg: Int) {
val (x, y) = getCursorPos()
setCursorPos(x + arg, y)
}
override fun cursorBack(arg: Int) {
val (x, y) = getCursorPos()
setCursorPos(x - arg, y)
}
override fun cursorNextLine(arg: Int) {
val (_, y) = getCursorPos()
val newy = y + arg
setCursorPos(0, if (newy >= TEXT_ROWS) TEXT_ROWS - 1 else newy)
if (newy >= TEXT_ROWS) {
scrollUp(newy - TEXT_ROWS + 1)
}
}
override fun cursorPrevLine(arg: Int) {
val (_, y) = getCursorPos()
setCursorPos(0, y - arg)
}
override fun cursorX(arg: Int) {
val (_, y) = getCursorPos()
setCursorPos(arg, y)
}
override fun eraseInDisp(arg: Int) {
when (arg) {
else -> TODO()
}
}
override fun eraseInLine(arg: Int) {
when (arg) {
else -> TODO()
}
}
override fun sgrOneArg(arg: Int) {
TODO("Not yet implemented")
}
override fun sgrTwoArg(arg1: Int, arg2: Int) {
TODO("Not yet implemented")
}
override fun sgrThreeArg(arg1: Int, arg2: Int, arg3: Int) {
TODO("Not yet implemented")
}
override fun cursorXY(arg1: Int, arg2: Int) {
TODO("Not yet implemented")
}
override fun ringBell() {
TODO("Not yet implemented")
}
override fun insertTab() {
TODO("Not yet implemented")
}
override fun crlf() {
TODO("Not yet implemented")
}
override fun backspace() {
TODO("Not yet implemented")
}
override fun privateSeqH(arg: Int) {
TODO("Not yet implemented")
}
override fun privateSeqL(arg: Int) {
TODO("Not yet implemented")
}
override fun getPrintStream(): OutputStream {
TODO("Not yet implemented")
}
override fun getErrorStream(): OutputStream {
TODO("Not yet implemented")
}
override fun getInputStream(): InputStream {
TODO("Not yet implemented")
}
override fun putKey(key: Int) {
vm.poke(-39, key.toByte())
}
/**
* @return key code in 0..255 (TODO: JInput Keycode or ASCII-Code?)
*/
override fun takeKey(): Int {
return vm.peek(-38)!!.toInt().and(255)
}
/** New lines are added at the bottom */
override fun scrollUp(arg: Int) {
val displacement = arg.toLong() * TEXT_COLS
UnsafeHelper.memcpy(
textBuffer.ptr + memTextOffset + displacement,
textBuffer.ptr + memTextOffset,
TEXT_AREA_SIZE - displacement
)
UnsafeHelper.memcpy(
textBuffer.ptr + memTextAttrOffset + displacement,
textBuffer.ptr + memTextAttrOffset,
TEXT_AREA_SIZE - displacement
)
for (i in 0 until displacement) {
textBuffer[memTextOffset + TEXT_AREA_SIZE - displacement + i] = 0
textBuffer[memTextAttrOffset + TEXT_AREA_SIZE - displacement + i] = ttyFore.toByte()
}
}
/** New lines are added at the top */
override fun scrollDown(arg: Int) {
val displacement = arg.toLong() * TEXT_COLS
UnsafeHelper.memcpy(
textBuffer.ptr + memTextOffset,
textBuffer.ptr + memTextOffset + displacement,
TEXT_AREA_SIZE - displacement
)
UnsafeHelper.memcpy(
textBuffer.ptr + memTextAttrOffset,
textBuffer.ptr + memTextAttrOffset + displacement,
TEXT_AREA_SIZE - displacement
)
for (i in 0 until displacement) {
textBuffer[memTextOffset + TEXT_AREA_SIZE + i] = 0
textBuffer[memTextAttrOffset + TEXT_AREA_SIZE + i] = ttyFore.toByte()
}
}
override fun dispose() {
chrrom.dispose()
}
}

View File

@@ -0,0 +1,7 @@
package net.torvald.tsvm.peripheral
internal class TermSim {
}

View File

@@ -0,0 +1,456 @@
package net.torvald.tsvm.peripheral
import net.torvald.tsvm.VM
import net.torvald.tsvm.VMJSR223Delegate
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.util.*
class TestDiskDrive(private val vm: VM, private val driveNum: Int, theRootPath: File? = null) : BlockTransferInterface(false, true) {
companion object {
const val STATE_CODE_STANDBY = 0
const val STATE_CODE_OPERATION_FAILED = 1
const val STATE_CODE_ILLEGAL_COMMAND = 128
const val STATE_CODE_FILE_NOT_FOUND = 129
const val STATE_CODE_FILE_ALREADY_OPENED = 130
const val STATE_CODE_OPERATION_NOT_PERMITTED = 131
const val STATE_CODE_READ_ONLY = 132
const val STATE_CODE_NOT_A_FILE = 133
const val STATE_CODE_NOT_A_DIRECTORY = 134
const val STATE_CODE_NO_FILE_OPENED = 135
const val STATE_CODE_SYSTEM_IO_ERROR = 192
const val STATE_CODE_SYSTEM_SECURITY_ERROR = 193
val errorMsgs = Array(256) { "" }
init {
errorMsgs[STATE_CODE_STANDBY] = "READY"
errorMsgs[STATE_CODE_OPERATION_FAILED] = "OPERATION FAILED"
errorMsgs[STATE_CODE_ILLEGAL_COMMAND] = "SYNTAX ERROR"
errorMsgs[STATE_CODE_FILE_NOT_FOUND] = "FILE NOT FOUND"
errorMsgs[STATE_CODE_FILE_ALREADY_OPENED] = "FILE ALREADY OPENED"
errorMsgs[STATE_CODE_SYSTEM_IO_ERROR] = "IO ERROR ON SIMULATED DRIVE"
errorMsgs[STATE_CODE_SYSTEM_SECURITY_ERROR] = "SECURITY ERROR ON SIMULATED DRIVE"
errorMsgs[STATE_CODE_OPERATION_NOT_PERMITTED] = "OPERATION NOT PERMITTED"
errorMsgs[STATE_CODE_NOT_A_FILE] = "NOT A FILE"
errorMsgs[STATE_CODE_NOT_A_DIRECTORY] = "NOT A DIRECTORY"
errorMsgs[STATE_CODE_NO_FILE_OPENED] = "NO FILE OPENED"
}
}
private val DBGPRN = true
private fun printdbg(msg: Any) {
if (DBGPRN) println("[TestDiskDrive] $msg")
}
fun composePositiveAns(vararg msg: String): ByteArray {
val sb = ArrayList<Byte>()
sb.addAll(msg[0].toByteArray().toTypedArray())
for (k in 1 until msg.size) {
sb.add(UNIT_SEP)
sb.addAll(msg[k].toByteArray().toTypedArray())
}
sb.add(END_OF_SEND_BLOCK)
return sb.toByteArray()
}
private val rootPath = theRootPath ?: File("test_assets/test_drive_$driveNum")
private var fileOpen = false
private var fileOpenMode = -1 // 1: 'W", 2: 'A'
private var file = File(rootPath.toURI())
//private var readModeLength = -1 // always 4096
private var writeMode = false
private var writeModeLength = -1
private val messageComposeBuffer = ByteArrayOutputStream(BLOCK_SIZE) // always use this and don't alter blockSendBuffer please
private var blockSendBuffer = ByteArray(1)
private var blockSendCount = 0
init {
statusCode = STATE_CODE_STANDBY
if (!rootPath.exists()) {
rootPath.mkdirs()
}
}
private fun resetBuf() {
blockSendCount = 0
messageComposeBuffer.reset()
}
override fun hasNext(): Boolean {
return (blockSendCount * BLOCK_SIZE < blockSendBuffer.size)
}
/** Computer's attempt to startRead() will result in calling this very function.
*
* Disk drive must send prepared message (or file transfer packet) to the computer.
*/
override fun startSendImpl(recipient: BlockTransferInterface): Int {
if (blockSendCount == 0) {
blockSendBuffer = messageComposeBuffer.toByteArray()
}
val sendSize = if (blockSendBuffer.size - (blockSendCount * BLOCK_SIZE) < BLOCK_SIZE)
blockSendBuffer.size % BLOCK_SIZE
else BLOCK_SIZE
recipient.writeout(ByteArray(sendSize) {
blockSendBuffer[blockSendCount * BLOCK_SIZE + it]
})
blockSendCount += 1
return sendSize
}
private lateinit var writeBuffer: ByteArray
private var writeBufferUsage = 0
/** Computer's attempt to startSend() will result in calling this very function.
* In such cases, `inputData` will be the message the computer sends.
*
* Disk drive must create desired side effects in accordance with the input message.
*/
override fun writeoutImpl(inputData: ByteArray) {
if (writeMode) {
//println("[DiskDrive] writeout with inputdata length of ${inputData.size}")
//println("[DiskDriveMsg] ${inputData.toString(Charsets.UTF_8)}")
if (!fileOpen) throw InternalError("File is not open but the drive is in write mode")
System.arraycopy(inputData, 0, writeBuffer, writeBufferUsage, minOf(writeModeLength - writeBufferUsage, inputData.size, BLOCK_SIZE))
writeBufferUsage += inputData.size
if (writeBufferUsage >= writeModeLength) {
writeMode = false
// commit to the disk
file.writeBytes(writeBuffer)
}
}
else {
val inputString = inputData.trimNull().toString(VM.CHARSET)
if (inputString.startsWith("DEVRST\u0017")) {
printdbg("Device Reset")
//readModeLength = -1
fileOpen = false
fileOpenMode = -1
file = File(rootPath.toURI())
blockSendCount = 0
statusCode = STATE_CODE_STANDBY
writeMode = false
writeModeLength = -1
}
else if (inputString.startsWith("DEVSTU\u0017"))
recipient?.writeout(composePositiveAns("${statusCode.toChar()}", errorMsgs[statusCode]))
else if (inputString.startsWith("DEVTYP\u0017"))
recipient?.writeout(composePositiveAns("STOR"))
else if (inputString.startsWith("DEVNAM\u0017"))
recipient?.writeout(composePositiveAns("Testtec Virtual Disk Drive"))
else if (inputString.startsWith("OPENR\"") || inputString.startsWith("OPENW\"") || inputString.startsWith("OPENA\"")) {
if (fileOpen) {
statusCode = STATE_CODE_FILE_ALREADY_OPENED
return
}
printdbg("msg: $inputString, lastIndex: ${inputString.lastIndex}")
val openMode = inputString[4]
printdbg("open mode: $openMode")
// split inputstring into path and optional drive-number
// get position of latest delimeter (comma)
var commaIndex = inputString.lastIndex
while (commaIndex > 6) {
if (inputString[commaIndex] == ',') break; commaIndex -= 1
}
// sanity check if path is actually enclosed with double-quote
if (commaIndex != 6 && inputString[commaIndex - 1] != '"') {
statusCode = STATE_CODE_ILLEGAL_COMMAND
return
}
val pathStr = inputString.substring(6, if (commaIndex == 6) inputString.lastIndex else commaIndex - 1)
val driveNum =
if (commaIndex == 6) null else inputString.substring(commaIndex + 1, inputString.length).toInt()
val filePath = filterSuperRoot(sanitisePath(pathStr))
// TODO driveNum is for disk drives that may have two or more slots built; for testing purposes we'll ignore it
file = File(rootPath, filePath)
printdbg("file path: ${file.canonicalPath}, drive num: $driveNum")
if (openMode == 'R' && !file.exists()) {
printdbg("! file not found")
statusCode = STATE_CODE_FILE_NOT_FOUND
return
}
statusCode = STATE_CODE_STANDBY
fileOpen = true
fileOpenMode = when (openMode) {
'W' -> 1
'A' -> 2
else -> -1
}
blockSendCount = 0
}
else if (inputString.startsWith("LISTFILES")) {
// TODO temporary behaviour to ignore any arguments
resetBuf()
if (!fileOpen) {
statusCode = STATE_CODE_NO_FILE_OPENED
return
}
try {
if (file.isDirectory) {
file.listFiles()!!.forEachIndexed { index, lsfile ->
if (index != 0) messageComposeBuffer.write(0x1E)
messageComposeBuffer.write(if (lsfile.isDirectory) 0x11 else 0x12)
messageComposeBuffer.write(lsfile.name.toByteArray(VM.CHARSET))
}
statusCode = STATE_CODE_STANDBY
}
else {
statusCode = STATE_CODE_NOT_A_DIRECTORY
return
}
}
catch (e: SecurityException) {
statusCode = STATE_CODE_SYSTEM_SECURITY_ERROR
return
}
catch (e1: IOException) {
statusCode = STATE_CODE_SYSTEM_IO_ERROR
return
}
}
else if (inputString.startsWith("LIST")) {
// TODO temporary behaviour to ignore any arguments
resetBuf()
if (!fileOpen) {
statusCode = STATE_CODE_NO_FILE_OPENED
return
}
messageComposeBuffer.write(getReadableLs().toByteArray(VM.CHARSET))
statusCode = STATE_CODE_STANDBY
}
else if (inputString.startsWith("CLOSE")) {
fileOpen = false
fileOpenMode = -1
statusCode = STATE_CODE_STANDBY
}
else if (inputString.startsWith("READ")) {
//readModeLength = inputString.substring(4 until inputString.length).toInt()
resetBuf()
if (file.isFile) {
try {
messageComposeBuffer.write(file.readBytes())
statusCode = STATE_CODE_STANDBY
}
catch (e: IOException) {
statusCode = STATE_CODE_SYSTEM_IO_ERROR
}
}
else {
statusCode = STATE_CODE_OPERATION_NOT_PERMITTED
return
}
}
else if (inputString.startsWith("LOADBOOT")) {
var commaIndex = 0
while (commaIndex < inputString.length) {
if (inputString[commaIndex] == ',') break
commaIndex += 1
}
val driveNum = if (commaIndex >= inputString.length) null else commaIndex
// TODO driveNum is for disk drives that may have two or more slots built; for testing purposes we'll ignore it
val bootFile = File(rootPath, "!BOOTSEC")
if (!bootFile.exists()) {
statusCode = STATE_CODE_FILE_NOT_FOUND
return
}
val fis = FileInputStream(bootFile)
try {
val retMsg = ByteArray(BLOCK_SIZE)
fis.read(retMsg)
recipient?.writeout(retMsg)
statusCode = STATE_CODE_STANDBY
}
catch (e: IOException) {
statusCode = STATE_CODE_SYSTEM_IO_ERROR
return
}
finally {
fis.close()
}
}
else if (inputString.startsWith("MKDIR")) {
if (!fileOpen) {
statusCode = STATE_CODE_NO_FILE_OPENED
return
}
if (fileOpenMode < 1) {
statusCode = STATE_CODE_OPERATION_NOT_PERMITTED
return
}
try {
val status = file.mkdir()
statusCode = if (status) 0 else 1
}
catch (e: SecurityException) {
statusCode = STATE_CODE_SYSTEM_SECURITY_ERROR
}
}
else if (inputString.startsWith("MKFILE")) {
if (!fileOpen) {
statusCode = STATE_CODE_NO_FILE_OPENED
return
}
if (fileOpenMode < 1) {
statusCode = STATE_CODE_OPERATION_NOT_PERMITTED
return
}
try {
val f1 = file.createNewFile()
statusCode = if (f1) STATE_CODE_STANDBY else STATE_CODE_OPERATION_FAILED
return
}
catch (e: IOException) {
statusCode = STATE_CODE_SYSTEM_IO_ERROR
}
catch (e1: SecurityException) {
statusCode = STATE_CODE_SYSTEM_SECURITY_ERROR
}
}
else if (inputString.startsWith("TOUCH")) {
if (!fileOpen) {
statusCode = STATE_CODE_NO_FILE_OPENED
return
}
if (fileOpenMode < 1) {
statusCode = STATE_CODE_OPERATION_NOT_PERMITTED
return
}
try {
val f1 = file.setLastModified(vm.worldInterface.currentTimeInMills())
statusCode = if (f1) STATE_CODE_STANDBY else STATE_CODE_OPERATION_FAILED
return
}
catch (e: IOException) {
statusCode = STATE_CODE_SYSTEM_IO_ERROR
}
catch (e1: SecurityException) {
statusCode = STATE_CODE_SYSTEM_SECURITY_ERROR
}
}
else if (inputString.startsWith("WRITE")) {
if (!fileOpen) {
statusCode = STATE_CODE_NO_FILE_OPENED
return
}
if (fileOpenMode < 0) {
statusCode = STATE_CODE_OPERATION_NOT_PERMITTED
return
}
writeMode = true
writeModeLength = inputString.substring(5, inputString.length).toInt()
writeBuffer = ByteArray(writeModeLength)
writeBufferUsage = 0
statusCode = STATE_CODE_STANDBY
}
else
statusCode = STATE_CODE_ILLEGAL_COMMAND
}
}
val diskID: UUID = UUID(0, 0)
private fun getReadableLs(): String {
val sb = StringBuilder()
val isRoot = (file.absolutePath == rootPath.absolutePath)
if (file.isFile) sb.append(file.name)
else {
sb.append("Current directory: ")
sb.append(if (isRoot) "(root)" else file.path)
sb.append('\n')
sb.append(".\n")
if (isRoot) sb.append("..\n")
// actual entries
file.listFiles()!!.forEach {
var filenameLen = it.name.length
sb.append(it.name)
if (it.isDirectory) {
sb.append("/")
filenameLen += 1
}
sb.append(" ".repeat(40 - filenameLen))
if (it.isFile) {
sb.append("${it.length()} B")
}
sb.append('\n')
}
}
return if (sb.last() == '\n') sb.substring(0, sb.lastIndex) else sb.toString()
}
private fun sanitisePath(s: String) = s.replace('\\','/').replace(Regex("""\?<>:\*\|"""),"-")
// applies a "cap" if the path attemps to access parent directory of the root
private fun filterSuperRoot(path: String): String {
if (path.isEmpty()) return path
var parentCount = 0
val paths = path.split('/')
val newPaths = ArrayList<String>()
paths.forEach {
if (it.isBlank() || it.isEmpty()) {
/*do nothing*/
}
else if (it == "..") {
parentCount -= -1
}
else if (it != ".") {
parentCount += 1
}
if (parentCount < -1) parentCount = -1
if (parentCount >= 0) {
newPaths.add(it)
}
}
return newPaths.joinToString("/")
}
}

View File

@@ -0,0 +1,185 @@
package net.torvald.tsvm.peripheral
import net.torvald.tsvm.VM
import java.io.ByteArrayOutputStream
import java.util.ArrayList
class TestFunctionGenerator : BlockTransferInterface(true, false) {
val filecontent_lorem = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ipsum magna, ultrices eu leo eu, consequat eleifend arcu. Nam tempor nunc aliquam mi cursus mollis. Aenean dictum iaculis dolor eget porttitor. Fusce vulputate dui id mauris ultricies, non aliquet nulla pulvinar. Integer consectetur nulla at cursus cursus. Nullam enim nisl, elementum a fermentum sed, suscipit id sapien. Duis eget enim lacinia, aliquam sapien ac, commodo risus. Morbi at enim sem. Aenean sollicitudin purus et sem porttitor, convallis ultricies nulla posuere. Suspendisse euismod sagittis vestibulum. Mauris lorem nisl, placerat et finibus non, cursus non ex. Interdum et malesuada fames ac ante ipsum primis in faucibus. Suspendisse finibus non dui vel tempor. Nam rhoncus ligula et massa sagittis fringilla. Cras convallis pellentesque nulla in rutrum.
Quisque ac orci sodales, semper neque eu, consequat lacus. Nulla suscipit orci felis, id tempor quam ultrices quis. Integer eu vulputate risus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Maecenas posuere sem sed erat tristique laoreet. Sed in ante est. Fusce nec est ut nunc aliquam condimentum viverra non ex. Pellentesque nisi ante, efficitur id neque sit amet, convallis tincidunt sapien. Nunc condimentum rutrum nisi, eu lobortis libero tempor non. Morbi euismod venenatis tincidunt. Nulla facilisi. Ut interdum nec nisi pharetra pretium.
Sed condimentum semper erat convallis vulputate. Donec rhoncus sodales faucibus. Morbi pulvinar elit quis lectus accumsan, a sagittis turpis scelerisque. Praesent at interdum quam. In hac habitasse platea dictumst. Vestibulum vulputate sem id massa maximus, quis malesuada nisl vestibulum. Nam fermentum feugiat tortor non imperdiet. Cras elementum ipsum at magna consectetur pellentesque. Etiam gravida mi sed magna venenatis pulvinar. Aenean scelerisque justo eu volutpat mattis.
Pellentesque faucibus tempus nibh, nec ultricies tortor aliquam at. Vestibulum nec imperdiet nulla. Nulla imperdiet neque vel ultrices molestie. Sed elementum sed quam id hendrerit. Ut sit amet scelerisque purus, eu porttitor enim. Curabitur luctus a lectus vitae commodo. Aenean sollicitudin metus non consequat molestie. Nulla ut venenatis lacus. Phasellus cursus erat et lorem sagittis elementum.
Nam in aliquet velit, vitae aliquam sapien. Phasellus imperdiet nulla augue, fermentum malesuada dolor hendrerit nec. Phasellus finibus dictum risus, tincidunt tincidunt turpis egestas ut. Pellentesque dapibus ipsum orci, vel volutpat justo sollicitudin a. Curabitur ut rhoncus ex. Maecenas ac dui vitae mauris iaculis sollicitudin. Etiam ac augue vitae elit consectetur condimentum et non turpis. Phasellus nunc leo, ultricies eu massa ut, elementum auctor odio. Duis interdum in est in suscipit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Suspendisse suscipit vel tortor id lacinia. Nunc maximus turpis maximus arcu viverra, eu facilisis felis tincidunt. Curabitur id lectus libero.
Nunc mollis nibh vitae sapien consequat, ut vestibulum sem pharetra. Aliquam iaculis, felis ut auctor porta, ipsum diam laoreet ex, sed egestas lacus est at neque. Aenean venenatis blandit arcu at porta. Nunc sed est magna. Duis pulvinar, nulla eu tristique mattis, dui diam malesuada sem, ac condimentum turpis nunc iaculis urna. Nam et ligula aliquet, fermentum lectus nec, consectetur ipsum. Proin convallis, mi id consectetur lobortis, urna nulla pellentesque odio, a finibus tortor nisl nec tortor. Suspendisse blandit nisl in magna hendrerit tristique. Cras sit amet metus et lacus rutrum tempus. In sapien elit, facilisis quis tristique a, vestibulum a massa. Donec ligula diam, posuere ac velit eget, lobortis tincidunt ante. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam lectus massa, egestas eu urna id, tempor pulvinar odio. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam in suscipit mauris, quis faucibus dui. Cras tincidunt turpe es.""".toByteArray(Charsets.US_ASCII)
val fileContent_multiblocks = """Zeroth Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
256th Byte 00000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
512nd Byte 00000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
768th Byte 00000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
1024th byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
1280th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
1536th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
1792nd Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
2048th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
2304th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
2560th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
2816th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
3072nd Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
3328th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
3584th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
3840th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
4096th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
4352nd Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
4608th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
4864th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
5120th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
5376th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
5632nd Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
5880th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
6144th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
6400th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
6656th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
6912nd Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
7168th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
7424th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
7680th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
7936th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
8192nd Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
8448th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
8704th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
8960th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
9216th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
9472nd Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
9728th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
9984th Byte 0000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
10240th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
10496th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
10752nd Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
11008th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
11264th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
11520th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
11776th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
12032nd Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
12288th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
12544th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
12800th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
13056th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
13312nd Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
13568th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
13824th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
14080th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
14336th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
14592nd Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
14848th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
15104th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
15360th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
15616th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
15872nd Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
16128th Byte 000111111111111111122222222222222223333333333333333444444444444444455555555555555556666666666666666777777777777777788888888888888889999999999999999AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFF
16384th Byte and some trailing bytes too!""".toByteArray(Charsets.US_ASCII)
private var fileOpen = false
private var blockSendBuffer = ByteArray(1)
private var blockSendCount = 0
private var writeMode = false
private var writeModeLength = -1
private val writeBuffer = ByteArrayOutputStream()
fun composeSerialAns(vararg msg: String): ByteArray {
val sb = ArrayList<Byte>()
sb.addAll(msg[0].toByteArray().toTypedArray())
for (k in 1 until msg.lastIndex) {
sb.add(0x1F)
sb.addAll(msg[k].toByteArray().toTypedArray())
}
sb.add(0x17)
return sb.toByteArray()
}
override fun startSendImpl(recipient: BlockTransferInterface): Int {
if (blockSendCount == 0) {
//blockSendBuffer = messageComposeBuffer.toByteArray()
blockSendBuffer = fileContent_multiblocks
}
val sendSize = if (blockSendBuffer.size - (blockSendCount * BLOCK_SIZE) < BLOCK_SIZE)
blockSendBuffer.size % BLOCK_SIZE
else BLOCK_SIZE
recipient.writeout(ByteArray(sendSize) {
blockSendBuffer[blockSendCount * BLOCK_SIZE + it]
})
blockSendCount += 1
return sendSize
}
override fun hasNext(): Boolean {
return (blockSendCount * BLOCK_SIZE < blockSendBuffer.size)
}
override fun writeoutImpl(inputData: ByteArray) {
if (writeMode) {
inputData.forEach {
if (writeModeLength > 0) {
writeBuffer.write(it.toInt())
writeModeLength -= 1
}
}
if (writeModeLength <= 0) {
writeMode = false
// test printout
val bufout = writeBuffer.toByteArray()
println("[TestFunctionGenerator] written bytes: ${bufout.size}")
println("[TestFunctionGenerator] written data: ${bufout.toString(VM.CHARSET)}")
writeBuffer.reset()
}
}
else {
val inputString = inputData.trimNull().toString(VM.CHARSET)
if (inputString.startsWith("DEVRST\u0017")) {
fileOpen = false
blockSendCount = 0
} else if (inputString.startsWith("DEVTYP\u0017"))
recipient?.writeout(composeSerialAns("STOR"))
else if (inputString.startsWith("DEVNAM\u0017"))
recipient?.writeout(composeSerialAns("Testtec Signal Generator"))
else if (inputString.startsWith("OPENR\""))
fileOpen = true
else if (inputString.startsWith("CLOSE"))
fileOpen = false
//else if (inputString.startsWith("READ"))
// readModeLength = inputString.substring(4 until inputString.length).toInt()
else if (inputString.startsWith("LIST"))
recipient?.writeout("\"LOREM.TXT\" TXT\nTotal 1 files on the disk".toByteArray())
else if (inputString.startsWith("WRITE")) {
writeMode = true
writeModeLength = inputString.substring(5, inputString.length).toInt()
statusCode = 0
}
else
statusCode = 128
blockSendCount = 0
}
}
override fun setMode(sendmode: Boolean) {
}
override fun getMode(): Boolean = true
}

View File

@@ -0,0 +1,93 @@
package net.torvald.tsvm.peripheral
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.GL20
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import net.torvald.tsvm.VM
import net.torvald.tsvm.kB
import kotlin.math.absoluteValue
open class TexticsAdapterBase(vm: VM, config: AdapterConfig) : GraphicsAdapter(vm, config) {
private val crtGradTex = Texture("./assets/crt_grad.png")
companion object {
val crtColor = hashMapOf(
"white" to Color(0xe4eaffff.toInt()),
"amber" to Color(0xffd600ff.toInt()),
"green" to Color(0x4aff00ff)
)
}
override fun peek(addr: Long): Byte? {
return when (addr) {
in 0 until 250972 -> (-1).toByte()
else -> super.peek(addr)
}
}
override fun poke(addr: Long, byte: Byte) {
when (addr) {
in 0 until 250972 -> { /*do nothing*/ }
else -> super.poke(addr, byte)
}
}
private val TEX_HEIGHT = WIDTH * Math.sqrt(HEIGHT.toDouble() / WIDTH).toFloat()
private val ALIGN = (HEIGHT - TEX_HEIGHT).absoluteValue / 2f
private val phosphorCol = crtColor[theme.substring(4)] ?: crtColor["white"]
override fun render(delta: Float, batch: SpriteBatch, xoff: Float, yoff: Float) {
super.render(delta, batch, xoff, yoff)
batch.inUse {
batch.enableBlending()
// phosphor
batch.setBlendFunction(GL20.GL_DST_COLOR, GL20.GL_ONE_MINUS_SRC_ALPHA)
batch.color = phosphorCol
batch.draw(faketex, xoff, HEIGHT + yoff, WIDTH.toFloat(), -HEIGHT.toFloat())
// CRT glass
batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_COLOR)
batch.color = Color.WHITE
batch.draw(crtGradTex, xoff, HEIGHT + ALIGN + yoff, WIDTH.toFloat(), -TEX_HEIGHT)
//batch.draw(crtGradTex, xoff, HEIGHT + yoff, WIDTH.toFloat(), -HEIGHT.toFloat())
}
}
override fun dispose() {
crtGradTex.dispose()
super.dispose()
}
}
class Term(vm: VM) : TexticsAdapterBase(vm, AdapterConfig(
"crt_white",
720,
480,
80,
32,
254,
0,
256.kB(),
"./hp2640.png",
0.32f,
GraphicsAdapter.TEXT_TILING_SHADER_MONOCHROME
))
class WpTerm(vm: VM) : TexticsAdapterBase(vm, AdapterConfig(
"crt_amber",
810,
360,
90,
20,
254,
0,
256.kB(),
"./wpfont.png",
0.32f,
GraphicsAdapter.TEXT_TILING_SHADER_MONOCHROME
))

View File

@@ -0,0 +1,38 @@
package net.torvald.tsvm.peripheral
import net.torvald.tsvm.CompressorDelegate
import net.torvald.tsvm.CompressorDelegate.GZIP_HEADER
import net.torvald.tsvm.VM
import java.io.File
open class VMProgramRom(path: String) {
private val contents: ByteArray
init {
val bytes = File(path).readBytes()
contents = bytes.sliceArray(0 until minOf(65536, bytes.size))
}
fun readAll(): String {
// check if bios is compressed in gzip
return if (contents.startsWith(GZIP_HEADER))
CompressorDelegate.decomp(contents).toString(VM.CHARSET)
else
contents.toString(VM.CHARSET)
}
fun get(addr: Int): Byte = contents[addr]
}
object GenericBios : VMProgramRom("./assets/bios/bios1.bin")
object OEMBios : VMProgramRom("./assets/bios/TBMBIOS.js")
object QuickBios : VMProgramRom("./assets/bios/quick.js")
object BasicBios : VMProgramRom("./assets/bios/basicbios.js")
object TandemBios : VMProgramRom("./assets/bios/tandemport.js")
object TsvmBios : VMProgramRom("./assets/bios/tsvmbios.js")
object BasicRom : VMProgramRom("./assets/bios/basic.bin")
object TBASRelBios : VMProgramRom("./assets/bios/tbasdist.js")
object WPBios : VMProgramRom("./assets/bios/wp.js")
object PipBios : VMProgramRom("./assets/bios/pipboot.rom")
object PipROM : VMProgramRom("./assets/bios/pipcode.bas")

View File

@@ -0,0 +1,10 @@
package net.torvald.tsvm.vdc
import net.torvald.tsvm.VM
import net.torvald.tsvm.peripheral.GraphicsAdapter
fun main() {
val vdc = Videotron2K(null)
vdc.eval(Videotron2K.screenfiller)
}

View File

@@ -0,0 +1,114 @@
package net.torvald.tsvm.vdc
import com.badlogic.gdx.ApplicationAdapter
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration
import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.graphics.glutils.ShaderProgram
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.torvald.tsvm.*
import net.torvald.tsvm.peripheral.GraphicsAdapter
class V2kRunTest : ApplicationAdapter() {
val vm = VM(64.kB(), TheRealWorld(), arrayOf())
lateinit var gpu: GraphicsAdapter
lateinit var batch: SpriteBatch
lateinit var camera: OrthographicCamera
lateinit var vmRunner: VMRunner
lateinit var coroutineJob: Job
lateinit var vdc: Videotron2K
override fun create() {
super.create()
gpu = GraphicsAdapter(vm, GraphicsAdapter.DEFAULT_CONFIG_COLOR_CRT)
vm.peripheralTable[1] = PeripheralEntry(
gpu,
GraphicsAdapter.VRAM_SIZE,
16,
0
)
batch = SpriteBatch()
camera = OrthographicCamera(net.torvald.tsvm.AppLoader.WIDTH.toFloat(), net.torvald.tsvm.AppLoader.WIDTH.toFloat())
camera.setToOrtho(false)
camera.update()
batch.projectionMatrix = camera.combined
Gdx.gl20.glViewport(0, 0, net.torvald.tsvm.AppLoader.WIDTH, net.torvald.tsvm.AppLoader.HEIGHT)
vm.getPrintStream = { gpu.getPrintStream() }
vm.getErrorStream = { gpu.getErrorStream() }
vm.getInputStream = { gpu.getInputStream() }
vdc = Videotron2K(gpu)
vmRunner = VMRunnerFactory(vm, "js")
coroutineJob = GlobalScope.launch {
vdc.eval(Videotron2K.screenfiller)
}
Gdx.input.inputProcessor = vm.getIO()
}
private var updateAkku = 0.0
private var updateRate = 1f / 60f
override fun render() {
Gdx.graphics.setTitle("${net.torvald.tsvm.AppLoader.appTitle} $EMDASH F: ${Gdx.graphics.framesPerSecond} $EMDASH VF: ${(1.0 / vdc.statsFrameTime).toInt()}")
super.render()
val dt = Gdx.graphics.rawDeltaTime
updateAkku += dt
var i = 0L
while (updateAkku >= updateRate) {
updateGame(updateRate)
updateAkku -= updateRate
i += 1
}
renderGame(dt)
}
private var latch = true
private fun updateGame(delta: Float) {
vm.update(delta)
}
private fun renderGame(delta: Float) {
gpu.render(delta, batch, 0f, 0f)
}
override fun dispose() {
super.dispose()
batch.dispose()
coroutineJob.cancel()
vm.dispose()
}
}
fun main() {
ShaderProgram.pedantic = false
val appConfig = Lwjgl3ApplicationConfiguration()
appConfig.setIdleFPS(60)
appConfig.setForegroundFPS(60)
appConfig.useVsync(false)
appConfig.setResizable(false)
appConfig.setTitle("Videotron2K Test")
appConfig.setWindowedMode(560, 448)
Lwjgl3Application(V2kRunTest(), appConfig)
}

View File

@@ -0,0 +1,633 @@
package net.torvald.tsvm.vdc
import net.torvald.UnsafeHelper
import torvald.random.HQRNG
import net.torvald.tsvm.peripheral.GraphicsAdapter
import java.lang.NumberFormatException
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.HashSet
/**
* See ./Videotron2K.md for documentation
*
* ## Variable Namespace
* - Scenes and Variables use same namespace
*
*/
class Videotron2K(var gpu: GraphicsAdapter?) {
companion object {
const val REGISTER_PREFIX = 0x7FFFFFFF_00000000L
const val VARIABLE_PREFIX = 0x3FFFFFFF_00000000L
const val PREFIX_MASK = (0xFFFFFFFFL).shl(32)
const val REG_PX = REGISTER_PREFIX or 12
const val REG_PY = REGISTER_PREFIX or 13
const val REG_FRM = REGISTER_PREFIX or 14
const val REG_TMR = REGISTER_PREFIX or 15
const val REG_R1 = REGISTER_PREFIX
const val REG_C1 = REGISTER_PREFIX + 6
const val INDEXED_SCENE_MAX = 1048575
const val VARIABLE_RATET = VARIABLE_PREFIX or 0
const val VARIABLE_RATEF = VARIABLE_PREFIX or 1
const val VARIABLE_WIDTH = VARIABLE_PREFIX or 2
const val VARIABLE_HEIGHT = VARIABLE_PREFIX or 3
private val specialRegs = hashMapOf(
"px" to REG_PX,
"py" to REG_PY,
"frm" to REG_FRM,
"tmr" to REG_TMR
)
private val reverseSpecialRegs = HashMap(specialRegs.entries.associate { (k, v) -> v to k })
/*
Registers internal variable ID:
r1 = REGISTER_PREFIX + 0
r2 = REGISTER_PREFIX + 1
r3 = REGISTER_PREFIX + 2
r4 = REGISTER_PREFIX + 3
r5 = REGISTER_PREFIX + 4
r6 = REGISTER_PREFIX + 5
c1 = REGISTER_PREFIX + 6
c2 = REGISTER_PREFIX + 7
c3 = REGISTER_PREFIX + 8
c4 = REGISTER_PREFIX + 9
c5 = REGISTER_PREFIX + 10
c6 = REGISTER_PREFIX + 11
px = REGISTER_PREFIX + 12
py = REGISTER_PREFIX + 13
frm = REGISTER_PREFIX + 14
tmr = REGISTER_PREFIX + 15
*/
val screenfiller = """
DEFINE RATEF 60
DEFINE height 448
DEFINE width 560
mov r6 12345
SCENE rng ; r6 is RNG value
mul r6 r6 48271
mod r6 r6 2147483647
exit
END SCENE
SCENE fill_line
@ mov px 0
perform rng
plot r6
; plot c1 ; will auto-increment px by one
; inc c1
; cmp r1 c1 251
; movzr r1 c1 0 ; mov (-zr r1) c1 0 -- first, the comparison is made with r1 then runs 'mov c1 0' if r1 == 0
exitzr px
END SCENE
SCENE loop_frame
@ mov py 0
perform fill_line
inc py
cmp r1 py 448
movzr r1 py 0
next
; exeunt
; there's no EXIT command so this scene will make the program to go loop indefinitely
END SCENE
perform loop_frame
""".trimIndent()
}
internal var regs = UnsafeHelper.allocate(16 * 4)
internal var internalMem = UnsafeHelper.allocate(16384)
internal var callStack = Stack<Pair<Long, Int>>() // Pair of Scene-ID (has SCENE_PREFIX; 0 with no prefix for root scene) and ProgramCounter
/* Compile-time variables */
private var scenes = HashMap<Long, Array<VT2Statement>>() // Long can have either SCENE_PREFIX- or INDEXED_SCENE_PREFIX-prefixed value
private var varIdTable = HashMap<String, Long>() // String is always uppercase, Long always has VARIABLE_PREFIX added
//private var sceneIdTable = HashMap<String, Long>() // String is always uppercase, Long always has SCENE_PREFIX added
internal var currentScene = 0L // VARIABLE_PREFIX is normally added but ROOT scene is just zero. Used by both parser and executor
internal var currentLineIndex = 0
internal var pcLoopedBack = false
internal var exeunt = false
/* Run-time variables and status */
internal var variableMap = HashMap<Long, Int>() // VarId with VARIABLE_PREFIX, Integer-value
internal var sleepLatch = false
// statistics stuffs
internal var performanceCounterTmr = 0L
var statsFrameTime = 0.0 // in seconds
internal set
fun resetVarIdTable() {
varIdTable.clear()
varIdTable["RATET"] = VARIABLE_RATET
varIdTable["RATEF"] = VARIABLE_RATEF
varIdTable["WIDTH"] = VARIABLE_WIDTH
varIdTable["HEIGHT"] = VARIABLE_HEIGHT
}
private val reComment = Regex(""";[^\n]*""")
private val reTokenizer = Regex(""" +""")
private val reGeneralReg = Regex("""[rR][0-9]""")
private val reCountReg = Regex("""[cC][0-9]""")
private val infoPrint = true
private val debugPrint = false
private val rng = torvald.random.HQRNG()
fun eval(command: String) {
val rootStatements = parseCommands(command)
if (infoPrint) {
scenes.forEach { id, statements ->
println("SCENE #${id and 0xFFFFFFFFL}")
statements.forEachIndexed { i, it -> println("I ${i.toString().padEnd(5, ' ')}$it") }
println("END SCENE\n")
}
rootStatements.forEachIndexed { i, it -> println("I ${i.toString().padEnd(5, ' ')}$it") }
}
execute(rootStatements)
}
private fun execute(rootStatements: Array<VT2Statement>) {
variableMap.clear()
currentScene = 0
regs.fillWith(0)
while (!exeunt) {
val scene = if (currentScene == 0L) rootStatements else scenes[currentScene]!!
val it = scene[currentLineIndex]
val oldSceneNo = currentScene
if (it.prefix != StatementPrefix.INIT || it.prefix == StatementPrefix.INIT && !pcLoopedBack) {
if (debugPrint) println("Run-Scene: ${currentScene and 0xFFFFFFFFL}, Lindex: $currentLineIndex, Inst: $it")
Command.checkConditionAndRun(it.command, this, it.args)
if (debugPrint) println("Reg-r1: ${regs.getInt((REG_R1 and 0xF) * 4)}, c1: ${regs.getInt((REG_C1 and 0xF) * 4)}, px: ${regs.getInt((REG_PX and 0xF) * 4)}")
}
// increment PC
currentLineIndex += 1
//// check if PC should loop back into the beginning of the scene
if (currentScene == oldSceneNo && currentLineIndex == scene.size) {
currentLineIndex = 0
pcLoopedBack = true
}
if (currentScene != oldSceneNo) {
pcLoopedBack = false
}
}
exeunt = false
if (infoPrint) println("Ende")
}
/**
* Clobbers scenes, varIdTable, sceneIdTable and temporary variable sceneIdTable
*
* @return root statements; scene statements are stored in 'scenes'
*/
private fun parseCommands(command: String): Array<VT2Statement> {
scenes.clear()
resetVarIdTable()
//sceneIdTable.clear()
val rootStatements = ArrayList<VT2Statement>()
val sceneStatements = ArrayList<VT2Statement>()
command.replace(reComment, "").split('\n')
.mapIndexed { index, s -> index to s }.filter { it.second.isNotBlank() }
.forEach { (lnum, stmt) ->
val stmt = stmt.trim()
val stmtUpper = stmt.toUpperCase()
val wordsUpper = stmtUpper.split(reTokenizer)
if (stmtUpper.startsWith("SCENE_")) { // indexed scene
if (currentScene != 0L) throw IllegalStateException("Line $lnum: Scenes cannot be nested")
val scenenumStr = stmt.substring(6)
try {
val scenenum = scenenumStr.toLong()
currentScene = VARIABLE_PREFIX or scenenum
}
catch (e: NumberFormatException) {
throw IllegalArgumentException("Line $lnum: Illegal scene numeral on $scenenumStr")
}
}
else if (stmtUpper.startsWith("SCENE ")) { // named scene
if (currentScene != 0L) throw IllegalStateException("Line $lnum: Scenes cannot be nested")
val sceneName = wordsUpper[1]
if (sceneName.isNullOrBlank()) {
throw IllegalArgumentException("Line $lnum: Illegal scene name on $stmt")
}
else if (hasVar(sceneName)) {
throw IllegalArgumentException("Line $lnum: Scene name or variable '$sceneName' already exists")
}
currentScene = registerNewVariable(sceneName) // scenes use same addr space as vars, to make things easier on the backend
}
else if (wordsUpper[0] == "END" && wordsUpper[1] == "SCENE") { // END SCENE
if (currentScene == 0L) throw IllegalArgumentException("Line $lnum: END SCENE is called without matching SCENE definition")
scenes[currentScene] = sceneStatements.toTypedArray()
sceneStatements.clear()
currentScene = 0L
}
else {
val cmdBuffer = if (currentScene != 0L) sceneStatements else rootStatements
cmdBuffer.add(translateLine(lnum + 1, stmt))
}
}
return rootStatements.toTypedArray()
}
private fun translateLine(lnum: Int, line: String): VT2Statement {
val tokens = line.split(reTokenizer)
if (tokens.isEmpty()) throw InternalError("Line $lnum: empty line not filtered!")
//println(tokens)
val isInit = tokens[0] == "@"
val cmdstr = tokens[isInit.toInt()].toUpperCase()
val cmd: Int = Command.dict[cmdstr] ?: throw RuntimeException("Undefined instruction on line $lnum: $cmdstr ($line)") // conditional code is pre-added on dict
val args = tokens.subList(1 + isInit.toInt(), tokens.size).map { parseArgString(cmdstr, lnum, it) }
return VT2Statement(
lnum,
if (isInit) StatementPrefix.INIT else StatementPrefix.NONE,
cmd,
args.toLongArray()
)
}
private fun parseArgString(parentCmd: String, lnum: Int, token: String): Long {
if (token.toIntOrNull() != null) // number literal
return token.toLong().and(0xFFFFFFFF)
else if (token.endsWith('h') && token.substring(0, token.lastIndex).toIntOrNull() != null) // hex literal
return token.substring(0, token.lastIndex).toInt(16).toLong().and(0xFFFFFFFF)
else if (specialRegs.contains(token.toLowerCase())) // special registers
return specialRegs[token.toLowerCase()]!!
else if (token.matches(reGeneralReg)) // r-registers
return REGISTER_PREFIX or token.substring(1, token.length).toLong().minus(1).and(0xFFFFFFFF)
else if (token.matches(reCountReg)) // c-registers
return REGISTER_PREFIX or token.substring(1, token.length).toLong().plus(5).and(0xFFFFFFFF)
else {
val varId = varIdTable[token.toUpperCase()] ?: (
if (parentCmd in Command.varDefiningCommands) registerNewVariable(token)
else throw NullPointerException("Undefined variable '$token' in line $lnum")
)
return varId
}
}
private fun registerNewVariable(varName: String): Long {
var id: Long
do {
id = VARIABLE_PREFIX or rng.nextLong().and(0xFFFFFFFFL)
} while (varIdTable.containsValue(id) || (id and 0xFFFFFFFFL) <= INDEXED_SCENE_MAX)
varIdTable[varName.toUpperCase()] = id
return id
}
private fun hasVar(name: String) = (varIdTable.containsKey(name.toUpperCase()))
private class VT2Statement(val lnum: Int, val prefix: Int = StatementPrefix.NONE, val command: Int, val args: LongArray) {
override fun toString(): String {
return "L ${lnum.toString().padEnd(5, ' ')}" + StatementPrefix.toString(prefix) + " " + Command.reverseDict[command] + " " + (args.map { argsToString(it) })
}
private fun argsToString(i: Long): String {
if (reverseSpecialRegs.contains(i))
return reverseSpecialRegs[i]!!
else if (i and REGISTER_PREFIX == REGISTER_PREFIX) {
val regnum = i and 0xFFFFFFFFL
return if (regnum < 6) "r${regnum + 1}" else "c${regnum - 5}"
}
else if (i and VARIABLE_PREFIX == VARIABLE_PREFIX) {
return "var:${i and 0xFFFFFFFF}"
}
else return i.toInt().toString()
}
}
fun dispose() {
regs.destroy()
}
private fun Boolean.toInt() = if (this) 1 else 0
private fun <T> Array<T>.linearSearch(selector: (T) -> Boolean): Int? {
this.forEachIndexed { index, it ->
if (selector.invoke(it)) return index
}
return null
}
}
object StatementPrefix {
const val NONE = 0
const val INIT = 1
fun toString(key: Int) = when(key) {
INIT -> "@"
else -> " "
}
}
object Command {
val conditional = arrayOf("ZR", "NZ", "GT", "LS", "GE", "LE")
const val NOP = 0
const val ADD = 0x8
const val SUB = 0x10
const val MUL = 0x18
const val DIV = 0x20
const val AND = 0x28
const val OR = 0x30
const val XOR = 0x38
const val SHL = 0x40
const val SHR = 0x48
const val USHR = 0x50
const val INC = 0x58
const val DEC = 0x60
const val NOT = 0x68
const val NEG = 0x70
const val MOD = 0x78
const val CMP = 0x80
const val MOV = 0x88
const val DATA = 0x90
const val MCP = 0x98
const val PERFORM = 0x100
const val NEXT = 0x108
const val EXIT = 0x110
const val EXEUNT = 0x118
const val FILLIN = 0x200
const val PLOT = 0x208
const val FILLSCR = 0x210
const val GOTO = 0x218
const val BORDER = 0x220
const val PLOTP = 0x228
const val WAIT = 0x700
const val DEFINE = 0x708
const val INST_ID_MAX = 0x800 // 256 if divided by 8
val dict = HashMap<String, Int>()
val varDefiningCommands = HashSet<String>()
val transferInst = HashSet<Int>()
// fill in conditionals to dict
init {
/* dict = */hashMapOf(
"NOP" to NOP,
"ADD" to ADD,
"SUB" to SUB,
"MUL" to MUL,
"DIV" to DIV,
"AND" to AND,
"OR" to OR,
"XOR" to XOR,
"SHL" to SHL,
"SHR" to SHR,
"USHR" to USHR,
"INC" to INC,
"DEC" to DEC,
"NOT" to NOT,
"NEG" to NEG,
"MOD" to MOD,
"CMP" to CMP,
"MOV" to MOV,
"DATA" to DATA,
"MCP" to MCP,
"PERFORM" to PERFORM,
"NEXT" to NEXT,
"EXIT" to EXIT,
"EXEUNT" to EXEUNT,
"FILLIN" to FILLIN,
"PLOT" to PLOT,
"PLOTP" to PLOTP,
"FILLSCR" to FILLSCR,
"GOTO" to GOTO,
"BORDER" to BORDER,
"WAIT" to WAIT,
"DEFINE" to DEFINE
).entries.forEach { (command, opcode) ->
dict[command] = opcode
conditional.forEachIndexed { i, cond ->
dict[command + cond] = opcode + i + 1
}
}
/* varDefiningCommands = */hashSetOf(
"DEFINE"
).forEach { command ->
varDefiningCommands.add(command)
conditional.forEach { cond ->
varDefiningCommands.add(command + cond)
}
}
/* transferInst = */hashSetOf(
PERFORM, EXIT, EXEUNT
).forEach { command ->
transferInst.add(command)
conditional.forEachIndexed { cond, _ ->
transferInst.add(command + cond)
}
}
}
val reverseDict = HashMap<Int, String>(dict.entries.associate { (k,v)-> v to k })
/**
* function (instance: Videotron2K, args: LongArray)
*/
val instSet = Array<(Videotron2K, LongArray) -> Unit>(256) { { _, _ -> TODO("Instruction ID $it (${reverseDict[it * 8]})") } }
init {
instSet[NOP] = { _, _ -> }
instSet[DEFINE shr 3] = { instance, args -> // DEFINE variable value
if (args.size != 2) throw ArgsCountMismatch(2, args)
instance.variableMap[args[0]] = if (args[1] and Videotron2K.VARIABLE_PREFIX == Videotron2K.VARIABLE_PREFIX) {
instance.variableMap[args[1]] ?: throw NullVar()
}
else if (args[1] and Videotron2K.PREFIX_MASK == 0L) {
args[1].toInt()
}
else {
throw UnknownVariableType(args[1])
}
}
instSet[MOV shr 3] = { instance, args -> // MOV register value
if (args.size != 2) throw ArgsCountMismatch(2, args)
checkRegisterLH(args[0])
instance.regs.setInt((args[0] and 0xF) * 4, resolveVar(instance, args[1]))
}
instSet[MUL shr 3] = { instance, args -> // MUL ACC LH RH
twoArgArithmetic(instance, args) { a,b -> a*b }
}
instSet[ADD shr 3] = { instance, args -> // ADD ACC LH RH
twoArgArithmetic(instance, args) { a,b -> a+b }
}
instSet[MOD shr 3] = { instance, args -> // MOD ACC LH RH
twoArgArithmetic(instance, args) { a,b -> a fmod b }
}
instSet[INC shr 3] = { instance, args -> // INC register
if (args.size != 1) throw ArgsCountMismatch(1, args)
checkRegisterLH(args[0])
instance.regs.setInt((args[0] and 0xF) * 4, 1 + instance.regs.getInt((args[0] and 0xF) * 4))
}
instSet[DEC shr 3] = { instance, args -> // DEC register
if (args.size != 1) throw ArgsCountMismatch(1, args)
checkRegisterLH(args[0])
instance.regs.setInt((args[0] and 0xF) * 4, 1 - instance.regs.getInt((args[0] and 0xF) * 4))
}
instSet[NEXT shr 3] = { instance, _ ->
instance.regs.setInt((Videotron2K.REG_FRM and 0xF) * 4, 1 + instance.regs.getInt((Videotron2K.REG_FRM and 0xF) * 4))
instance.sleepLatch = true
val timeTook = (System.nanoTime() - instance.performanceCounterTmr).toDouble()
instance.statsFrameTime = timeTook * 0.000001
instance.performanceCounterTmr = System.nanoTime()
}
instSet[CMP shr 3] = { instance, args -> // CMP rA rB rC
twoArgArithmetic(instance, args) { a,b -> if (a>b) 1 else if (a<b) -1 else 0 }
}
instSet[PLOT shr 3] = { instance, args -> // PLOT vararg-bytes
if (args.isNotEmpty()) {
val px = instance.regs.getInt((Videotron2K.REG_PX and 0xF) * 4)
val py = instance.regs.getInt((Videotron2K.REG_PY and 0xF) * 4)
val width = instance.variableMap[Videotron2K.VARIABLE_WIDTH]!!
val memAddr = py * width + px
args.forEachIndexed { index, variable ->
val value = resolveVar(instance, variable).toByte()
instance.gpu?.poke(memAddr.toLong() + index, value)
}
// write back auto-incremented value
instance.regs.setInt((Videotron2K.REG_PX and 0xF) * 4, (px + args.size) fmod width)
}
}
instSet[PERFORM shr 3] = { instance, args -> // PERFORM scene
instance.callStack.push(instance.currentScene to instance.currentLineIndex)
instance.currentScene = args[0]
instance.currentLineIndex = -1
}
instSet[EXIT shr 3] = { instance, _ ->
val (scene, line) = instance.callStack.pop()
instance.currentScene = scene
instance.currentLineIndex = line
//println("EXIT!")
//Thread.sleep(1000L)
}
instSet[EXEUNT shr 3] = { instance, _ ->
instance.exeunt = true
}
}
private inline fun twoArgArithmetic(instance: Videotron2K, args: LongArray, operation: (Int, Int) -> Int) {
if (args.size != 3) throw ArgsCountMismatch(3, args)
checkRegisterLH(args[0])
val lh = resolveVar(instance, args[1])
val rh = resolveVar(instance, args[2])
instance.regs.setInt((args[0] and 0xF) * 4, operation(lh, rh))
}
fun checkConditionAndRun(inst: Int, instance: Videotron2K, args: LongArray) {
val opcode = inst shr 3
val condCode = inst and 7
if (condCode == 0) {
//if (inst !in transferInst) instance.currentLineIndex += 1
instSet[opcode].invoke(instance, args)
return
}
val condition = when (condCode) {
1 -> resolveVar(instance, args[0]) == 0
2 -> resolveVar(instance, args[0]) != 0
3 -> resolveVar(instance, args[0]) > 0
4 -> resolveVar(instance, args[0]) < 0
5 -> resolveVar(instance, args[0]) >= 0
6 -> resolveVar(instance, args[0]) <= 0
else -> throw InternalError()
}
if (condition) {
//if (inst !in transferInst) instance.currentLineIndex += 1
instSet[opcode].invoke(instance, args.sliceArray(1 until args.size))
}
}
private fun resolveVar(instance: Videotron2K, arg: Long): Int {
return if (arg and Videotron2K.REGISTER_PREFIX == Videotron2K.REGISTER_PREFIX) {
instance.regs.getInt((arg and 0xF) * 4)
}
else if (arg and Videotron2K.VARIABLE_PREFIX == Videotron2K.VARIABLE_PREFIX) {
instance.variableMap[arg] ?: throw NullVar()
}
else arg.toInt()
}
private fun checkRegisterLH(arg: Long) {
if (arg and Videotron2K.REGISTER_PREFIX != Videotron2K.REGISTER_PREFIX) {
throw BadLeftHand("register")
}
}
private infix fun Int.fmod(other: Int) = Math.floorMod(this, other)
}
internal class ArgsCountMismatch(expected: Int, args: LongArray) : RuntimeException("Argument count mismatch: expected $expected, got ${args.size}")
internal class UnknownVariableType(arg: Long) : RuntimeException("Unknown variable type: ${arg.ushr(32).toString(16)}")
internal class NullVar : RuntimeException("Variable is undeclared")
internal class BadLeftHand(type: String) : RuntimeException("Left-hand argument is not $type")
internal class BadRightHand(type: String) : RuntimeException("Right-hand argument is not $type")