Add platform-specific requestAnimationFrame, refactor redundant string accessor methods, and introduce BufferedImage and Color classes for emulator graphics rendering.

This commit is contained in:
2025-08-14 16:49:39 +02:00
parent c7552c2a95
commit 9f295b2fb9
16 changed files with 215 additions and 175 deletions

View File

@@ -400,7 +400,7 @@ class Assembler {
): Graphic {
val graphic = Graphic(labelTokens, token.line)
val filename = token.stringValue
val file = File(File(this.srcName).getParent(), filename)
val file = File(File(this.srcName).parent, filename)
val index = graphics.size
data.setValue(token, byteArrayOf(((index shr 8) and 0xFF).toByte(), (index and 0xFF).toByte()))
@@ -732,7 +732,7 @@ class Assembler {
val first = tokenizer!!.consume()
tokens.add(first)
while (tokenizer!!.more() &&
first.line == tokenizer!!.currentToken().line
first.line == tokenizer!!.getCurrentToken().line
) {
tokens.add(tokenizer!!.consume())
}

View File

@@ -0,0 +1,28 @@
package mtmc.emulator
class BufferedImage(width: Int, height: Int, type: Int) {
fun getWidth(): Int {
TODO("Not yet implemented")
}
fun getHeight(): Int {
TODO("Not yet implemented")
}
fun getRGB(x: Int, y: Int): Int {
TODO("Not yet implemented")
}
fun getType(): Int {
TODO("Not yet implemented")
}
fun setRGB(x: Int, y: Int, intVal: Int) {
TODO("Not yet implemented")
}
}
class Dimension(
val width: Int,
val height: Int
)

View File

@@ -0,0 +1,8 @@
package mtmc.emulator
class Color(
val r: Int,
val g: Int,
val b: Int,
) {
}

View File

@@ -2,6 +2,7 @@ package mtmc.emulator
import mtmc.emulator.MonTanaMiniComputer.ComputerStatus
import mtmc.util.currentTimeMillis
import mtmc.util.requestAnimationFrame
import kotlin.math.max
/**
@@ -11,49 +12,43 @@ import kotlin.math.max
class MTMCClock(
private val computer: MonTanaMiniComputer
) {
var instructions: Long = 0
var ips: Long = 0
var expected: Long = 0
var virtual: Long = 0
var startTime = currentTimeMillis()
var speed: Long = 0
var instructionsToRun = 0.0
fun run() {
var instructions: Long = 0
var ips: Long = 0
var expected: Long = 0
var virtual: Long = 0
requestAnimationFrame { handleFrame(it) }
}
var startTime = currentTimeMillis()
var deltaStart: Long
var delta: Long
fun handleFrame(time: Double) {
// figure out how many instructions to execute this 'time' duration
// maximize time so we don't hang if the emulator is too slow
val actualTime = max(time, 0.16)
var speed: Long = 0
var pulse: Long
var ms: Long = 10
while (computer.getStatus() == ComputerStatus.EXECUTING) {
// assume 1Hz = 1 instruction/second
if (computer.getStatus() == ComputerStatus.EXECUTING) {
speed = max(computer.speed, 0).toLong()
pulse = (if (speed <= 0) 1000000 else max(speed / 100, 1))
ms = (if (pulse < 10) 1000 / speed else 10)
deltaStart = currentTimeMillis()
delta = ms - (currentTimeMillis() - deltaStart)
/* We've lost more than a second. Recalibrate. */
if ((expected - virtual) > pulse * 100) {
startTime = deltaStart
virtual = 0
if (speed == 0L) {
speed = 1000000L
}
/* Throttles to every 10ms, but "catches up" if we're behind */
if (delta > 0 && (expected - virtual) < pulse && speed != 0L) {
try {
Thread.sleep(delta)
} catch (e: InterruptedException) {
}
}
instructionsToRun += time * speed
val pulse: Long = instructionsToRun.toLong()
instructionsToRun -= pulse
instructions += computer.pulse(pulse)
virtual += pulse
ips = (virtual * 1000) / max(1, currentTimeMillis() - startTime)
expected = (currentTimeMillis() - startTime) * speed / 1000
virtual += instructions
ips = (instructions / time).toLong()
requestAnimationFrame { handleFrame(it) }
}
//println("Executed " + instructions + " instructions at a rate of " + ips + " ips (speed = " + speed + ")")

View File

@@ -1,22 +1,13 @@
package mtmc.emulator
import java.awt.Color
import java.awt.Dimension
import java.awt.RenderingHints
import java.awt.image.BufferedImage
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.*
import javax.imageio.ImageIO
import kotlin.math.min
class MTMCDisplay(private val computer: MonTanaMiniComputer) {
private val buffer = BufferedImage(
COLS,
ROWS,
BufferedImage.TYPE_INT_ARGB
)
1
) // BufferedImage.TYPE_INT_ARGB
private var currentColor: DisplayColor? = null
private var graphics: Array<BufferedImage> = arrayOf()
private var byteArray: ByteArray = ByteArray(0)
@@ -69,24 +60,25 @@ class MTMCDisplay(private val computer: MonTanaMiniComputer) {
}
private fun loadSplashScreen() {
try {
val bytes: ByteArray = Base64.getDecoder().decode(SPLASH_SCREEN)
val bais = ByteArrayInputStream(bytes)
var img: BufferedImage? = null
img = ImageIO.read(bais)
loadScaledImage(img)
} catch (e: IOException) {
e.printStackTrace()
}
/* try {
val bytes: ByteArray = Base64.getDecoder().decode(SPLASH_SCREEN)
val bais = ByteArrayInputStream(bytes)
var img: BufferedImage? = null
img = ImageIO.read(bais)
loadScaledImage(img)
} catch (e: IOException) {
e.printStackTrace()
}*/
}
private fun loadImage(data: ByteArray): BufferedImage? {
try {
return ImageIO.read(ByteArrayInputStream(data))
} catch (e: IOException) {
e.printStackTrace()
throw IllegalStateException(e)
}
/* try {
return ImageIO.read(ByteArrayInputStream(data))
} catch (e: IOException) {
e.printStackTrace()
throw IllegalStateException(e)
}*/
return null
}
fun loadGraphics(data: Array<ByteArray>) {
@@ -96,7 +88,7 @@ class MTMCDisplay(private val computer: MonTanaMiniComputer) {
}
graphics = Array(data.size) { index ->
loadImage(data[index]) ?: BufferedImage(160, 144, BufferedImage.TYPE_INT_RGB)
loadImage(data[index]) ?: BufferedImage(160, 144, 1) //BufferedImage.TYPE_INT_RGB)
}
}
@@ -128,31 +120,31 @@ class MTMCDisplay(private val computer: MonTanaMiniComputer) {
}
fun drawLine(startCol: Short, startRow: Short, endCol: Short, endRow: Short) {
val graphics = buffer.getGraphics()
graphics.setColor(currentColor!!.javaColor)
graphics.drawLine(startCol.toInt(), startRow.toInt(), endCol.toInt(), endRow.toInt())
graphics.dispose()
// val graphics = buffer.getGraphics()
// graphics.setColor(currentColor!!.javaColor)
// graphics.drawLine(startCol.toInt(), startRow.toInt(), endCol.toInt(), endRow.toInt())
// graphics.dispose()
}
fun drawRectangle(startCol: Short, startRow: Short, width: Short, height: Short) {
val graphics = buffer.getGraphics()
graphics.setColor(currentColor!!.javaColor)
graphics.fillRect(startCol.toInt(), startRow.toInt(), width.toInt(), height.toInt())
graphics.dispose()
// val graphics = buffer.getGraphics()
// graphics.setColor(currentColor!!.javaColor)
// graphics.fillRect(startCol.toInt(), startRow.toInt(), width.toInt(), height.toInt())
// graphics.dispose()
}
fun drawImage(image: Int, x: Int, y: Int) {
val graphic = graphics!![image]
val graphics = buffer.getGraphics()
graphics.drawImage(graphic, x, y, null)
graphics.dispose()
// val graphics = buffer.getGraphics()
// graphics.drawImage(graphic, x, y, null)
// graphics.dispose()
}
fun drawImage(image: Int, x: Int, y: Int, width: Int, height: Int) {
val graphic = graphics!![image]
val graphics = buffer.getGraphics()
graphics.drawImage(graphic, x, y, width, height, null)
graphics.dispose()
// val graphics = buffer.getGraphics()
// graphics.drawImage(graphic, x, y, width, height, null)
// graphics.dispose()
}
fun drawImage(
@@ -167,20 +159,20 @@ class MTMCDisplay(private val computer: MonTanaMiniComputer) {
dh: Int
) {
val graphic = graphics!![image]
val graphics = buffer.getGraphics()
graphics.drawImage(graphic, dx, dy, dx + dw, dy + dh, sx, sy, sx + sw, sy + sh, null)
graphics.dispose()
// val graphics = buffer.getGraphics()
// graphics.drawImage(graphic, dx, dy, dx + dw, dy + dh, sx, sy, sx + sw, sy + sh, null)
// graphics.dispose()
}
fun sync() {
val baos = ByteArrayOutputStream()
try {
buffer.flush()
ImageIO.write(buffer, "png", baos)
byteArray = baos.toByteArray()
} catch (e: IOException) {
throw RuntimeException(e)
}
/* val baos = ByteArrayOutputStream()
try {
buffer.flush()
ImageIO.write(buffer, "png", baos)
byteArray = baos.toByteArray()
} catch (e: IOException) {
throw RuntimeException(e)
}*/
computer.notifyOfDisplayUpdate()
}
@@ -240,21 +232,24 @@ class MTMCDisplay(private val computer: MonTanaMiniComputer) {
fun scaleImage(original: BufferedImage, scaleDimensions: Dimension): BufferedImage {
val resized = BufferedImage(scaleDimensions.width, scaleDimensions.height, original.getType())
val g = resized.createGraphics()
g.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR
)
g.drawImage(
original, 0, 0, scaleDimensions.width, scaleDimensions.height, 0, 0, original.getWidth(),
original.getHeight(), null
)
g.dispose()
/*
val g = resized.createGraphics()
g.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR
)
g.drawImage(
original, 0, 0, scaleDimensions.width, scaleDimensions.height, 0, 0, original.getWidth(),
original.getHeight(), null
)
g.dispose()
*/
return resized
}
fun convertImage(image: BufferedImage): BufferedImage {
val arbg = BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_4BYTE_ABGR)
val arbg =
BufferedImage(image.getWidth(), image.getHeight(), 2) //BufferedImage.TYPE_4BYTE_ABGR)
for (x in 0..<image.getWidth()) {
for (y in 0..<image.getHeight()) {
val rgb = image.getRGB(x, y)

View File

@@ -51,59 +51,59 @@ class MTOS(private val computer: MonTanaMiniComputer) {
fun handleSysCall(syscallNumber: Short) {
if (syscallNumber == getValue("exit").toShort()) {
computer.setStatus(MonTanaMiniComputer.ComputerStatus.FINISHED)
} else if (syscallNumber == getValue("rint").toShort()) {
// rint
if (!computer.console.hasShortValue()) {
computer.notifyOfRequestInteger()
}
while (!computer.console.hasShortValue() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) {
try {
Thread.sleep(10)
} catch (e: InterruptedException) {
}
}
val `val` = computer.console.readInt()
computer.setRegisterValue(Register.RV, `val`.toInt())
// } else if (syscallNumber == getValue("rint").toShort()) {
// // rint
// if (!computer.console.hasShortValue()) {
// computer.notifyOfRequestInteger()
// }
// while (!computer.console.hasShortValue() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) {
// try {
// Thread.sleep(10)
// } catch (e: InterruptedException) {
// }
// }
// val `val` = computer.console.readInt()
// computer.setRegisterValue(Register.RV, `val`.toInt())
} else if (syscallNumber == getValue("wint").toShort()) {
// wint
val value = computer.getRegisterValue(Register.A0)
computer.console.writeInt(value)
} else if (syscallNumber == getValue("rchr").toShort()) {
if (!computer.console.hasShortValue()) {
computer.notifyOfRequestCharacter()
}
while (!computer.console.hasShortValue() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) {
try {
Thread.sleep(10)
} catch (e: InterruptedException) {
}
}
val `val` = computer.console.readChar()
computer.setRegisterValue(Register.RV, `val`.code)
// } else if (syscallNumber == getValue("rchr").toShort()) {
// if (!computer.console.hasShortValue()) {
// computer.notifyOfRequestCharacter()
// }
// while (!computer.console.hasShortValue() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) {
// try {
// Thread.sleep(10)
// } catch (e: InterruptedException) {
// }
// }
// val `val` = computer.console.readChar()
// computer.setRegisterValue(Register.RV, `val`.code)
} else if (syscallNumber == getValue("wchr").toShort()) {
val value = computer.getRegisterValue(Register.A0)
computer.console.print("" + Char(value.toUShort()))
} else if (syscallNumber == getValue("rstr").toShort()) {
// rstr
val pointer = computer.getRegisterValue(Register.A0)
val maxLen = computer.getRegisterValue(Register.A1)
if (!computer.console.hasReadString()) {
computer.notifyOfRequestString()
}
while (!computer.console.hasReadString() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) {
try {
Thread.sleep(10)
} catch (e: InterruptedException) {
}
}
val string = computer.console.readString()
val bytes = string!!.encodeToByteArray()
val bytesToRead = min(bytes.size, maxLen.toInt())
for (i in 0..<bytesToRead) {
val aByte = bytes[i]
computer.writeByteToMemory(pointer + i, aByte)
}
computer.setRegisterValue(Register.RV, bytesToRead)
// } else if (syscallNumber == getValue("rstr").toShort()) {
// // rstr
// val pointer = computer.getRegisterValue(Register.A0)
// val maxLen = computer.getRegisterValue(Register.A1)
// if (!computer.console.hasReadString()) {
// computer.notifyOfRequestString()
// }
// while (!computer.console.hasReadString() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) {
// try {
// Thread.sleep(10)
// } catch (e: InterruptedException) {
// }
// }
// val string = computer.console.readString()
// val bytes = string!!.encodeToByteArray()
// val bytesToRead = min(bytes.size, maxLen.toInt())
// for (i in 0..<bytesToRead) {
// val aByte = bytes[i]
// computer.writeByteToMemory(pointer + i, aByte)
// }
// computer.setRegisterValue(Register.RV, bytesToRead)
} else if (syscallNumber == getValue("wstr").toShort()) {
// wstr
val pointer = computer.getRegisterValue(Register.A0)
@@ -171,14 +171,14 @@ class MTOS(private val computer: MonTanaMiniComputer) {
}
computer.setRegisterValue(Register.RV, random.nextInt(low.toInt(), high + 1))
} else if (syscallNumber == getValue("sleep").toShort()) {
// sleep
val millis = computer.getRegisterValue(Register.A0)
try {
if (millis > 0) Thread.sleep(millis.toLong())
} catch (e: InterruptedException) {
throw RuntimeException(e)
}
// } else if (syscallNumber == getValue("sleep").toShort()) {
// // sleep
// val millis = computer.getRegisterValue(Register.A0)
// try {
// if (millis > 0) Thread.sleep(millis.toLong())
// } catch (e: InterruptedException) {
// throw RuntimeException(e)
// }
} else if (syscallNumber == getValue("fbreset").toShort()) {
// fbreset
computer.display.reset()

View File

@@ -20,8 +20,6 @@ class File(
constructor(name: String) : this(null, name)
fun getParent(): File = parent ?: error("No parent")
fun getPath(): String = if (parent == null) {
name
} else {

View File

@@ -12,4 +12,14 @@ class Path {
fun resolve(filename: String): Path {
TODO("Not yet implemented")
}
fun toAbsolutePath(): Path {
TODO("Not yet implemented")
}
companion object {
fun of(string: String): Path {
TODO()
}
}
}

View File

@@ -96,7 +96,7 @@ object Shell {
return
}
} else {
cmd = identifier.stringValue()
cmd = identifier.stringValue
}
if (isCommand(cmd)) {
COMMANDS[cmd.lowercase()]!!.exec(tokens, computer)
@@ -106,7 +106,7 @@ object Shell {
asm.addAll(tokens.tokens)
val updatedAsm = Assembler.transformSyntheticInstructions(asm)
val firstToken = updatedAsm.peekFirst()
val firstTokenStr = firstToken?.stringValue() ?: error("Unexpected null token")
val firstTokenStr = firstToken?.stringValue ?: error("Unexpected null token")
if (!updatedAsm.isEmpty() && isInstruction(firstTokenStr)) {
val assembler = Assembler()
val result = assembler.assemble(command)

View File

@@ -17,11 +17,11 @@ class GetCommand : ShellCommand() {
if (memLocation == null) {
val register = tokens.matchAndConsume(MTMCToken.TokenType.IDENTIFIER)
if (register == null) usageException()
val reg = Register.toInteger(register!!.stringValue())
val reg = Register.toInteger(register!!.stringValue)
if (reg >= 0) {
computer.console.println(register.stringValue() + ": " + computer.getRegisterValue(reg))
computer.console.println(register.stringValue + ": " + computer.getRegisterValue(reg))
} else {
throw IllegalArgumentException("Bad register: " + register.stringValue())
throw IllegalArgumentException("Bad register: " + register.stringValue)
}
} else {
if (memLocation.type === MTMCToken.TokenType.INTEGER || memLocation.type === MTMCToken.TokenType.BINARY || memLocation.type === MTMCToken.TokenType.HEX) {

View File

@@ -23,11 +23,11 @@ class SetCommand : ShellCommand() {
MTMCToken.TokenType.BINARY
)
if (value == null) usageException()
val reg = Register.toInteger(register!!.stringValue())
val reg = Register.toInteger(register!!.stringValue)
if (reg >= 0) {
computer.setRegisterValue(reg, value!!.intValue())
} else {
throw IllegalArgumentException("Bad register: " + register.stringValue())
throw IllegalArgumentException("Bad register: " + register.stringValue)
}
} else {
val value = tokens.matchAndConsume(
@@ -42,7 +42,7 @@ class SetCommand : ShellCommand() {
} else {
computer.writeStringToMemory(
memLocation.intValue(),
value!!.stringValue().encodeToByteArray()
value!!.stringValue.encodeToByteArray()
)
}
}

View File

@@ -12,10 +12,6 @@ data class MTMCToken(
return stringValue
}
fun stringValue(): String {
return stringValue
}
fun charValue(): Char {
return stringValue.get(0)
}

View File

@@ -7,7 +7,7 @@ class MTMCTokenizer(var source: String, lineCommentStart: String) {
var tokens: MutableList<MTMCToken> = MTMCScanner(source, lineCommentStart).tokenize()
var currentToken: Int = 0
fun currentToken(): MTMCToken {
fun getCurrentToken(): MTMCToken {
return tokens.get(currentToken)
}
@@ -28,8 +28,8 @@ class MTMCTokenizer(var source: String, lineCommentStart: String) {
}
fun matchAndConsume(identifier: String?): Boolean {
if (currentToken().type == MTMCToken.TokenType.IDENTIFIER &&
currentToken().stringValue == identifier
if (getCurrentToken().type == MTMCToken.TokenType.IDENTIFIER &&
getCurrentToken().stringValue == identifier
) {
return true
} else {
@@ -39,7 +39,7 @@ class MTMCTokenizer(var source: String, lineCommentStart: String) {
fun match(vararg type: MTMCToken.TokenType?): Boolean {
for (tokenType in type) {
if (currentToken().type == tokenType) {
if (getCurrentToken().type == tokenType) {
return true
}
}
@@ -51,7 +51,7 @@ class MTMCTokenizer(var source: String, lineCommentStart: String) {
}
fun more(): Boolean {
return currentToken().type != MTMCToken.TokenType.EOF
return getCurrentToken().type != MTMCToken.TokenType.EOF
}
fun previousToken(): MTMCToken? {
@@ -83,7 +83,7 @@ class MTMCTokenizer(var source: String, lineCommentStart: String) {
do {
last = consume()
sb.append(last.stringValue)
next = currentToken()
next = getCurrentToken()
} while (more() && last.end == next.start)
return sb.toString()
} else {

View File

@@ -1,3 +1,6 @@
package mtmc.util
expect fun currentTimeMillis(): Long
expect fun currentTimeMillis(): Long
expect fun requestAnimationFrame(action: (Double) -> Unit)

View File

@@ -1,5 +1,9 @@
package mtmc.util
import kotlinx.browser.window
import kotlin.js.Date
actual fun currentTimeMillis(): Long = Date().getTime().toLong()
actual fun requestAnimationFrame(action: (Double) -> Unit) {
window.requestAnimationFrame(action)
}

View File

@@ -1,3 +1,6 @@
package mtmc.util
actual fun currentTimeMillis(): Long = System.currentTimeMillis()
actual fun currentTimeMillis(): Long = System.currentTimeMillis()
actual fun requestAnimationFrame(action: (Double) -> Unit) {
error("requestAnimationFrame is not supported on JVM")
}