Compare commits

...

13 Commits

Author SHA1 Message Date
4dcfe3a6ff Refactor PlatformSpecific functions with time and setTimeout, optimize RewindStep substeps handling, update MTMCClock timing logic, enhance frame processing, and integrate updateJsDisplay for dynamic updates in Main. 2025-08-23 17:44:25 +02:00
f14f316e38 Refactor BufferedImage logic with WebGL support, update DisplayView for GPU-based rendering, transition timing functions to Double, optimize MTMCClock frame logic, and add dynamic control for update state in ControlView. 2025-08-23 15:51:47 +02:00
11b069ddc5 Refactor MTMCConsole, enhance RegisterView with dynamic flag indicators, update ConsoleView for better output handling, add JavaExec task in Gradle, and update dependencies, styles, and Gradle wrapper version. 2025-08-18 16:41:02 +02:00
d7e331728f Refactor ControlView and update mtmc.css for enhanced control styling, improved layout, and dynamic speed setting functionality. 2025-08-18 11:57:33 +02:00
8457d3a854 Refactor PlatformSpecific, update MonTanaMiniComputer and MTMCDisplay logic, add splash screen rendering with SplashData, and optimize observer calls and color initialization. 2025-08-18 11:36:42 +02:00
37691dc7fa Refactor rewind functionality with circular buffer, update BufferedImageData handling in DisplayView, enhance ConsoleView rendering updates, and integrate SnakeCode data. 2025-08-17 20:32:12 +02:00
f169dce339 Refactor MTMCClock frame handling, enhance emulator performance, integrate immediateTimeout, and optimize rendering logic for RegisterView, MemoryView, and BufferedImage. 2025-08-17 16:16:57 +02:00
6acf781324 Add DiplayControlView for enhanced display rendering, refactor BufferedImage RGB logic, and update related components and styles 2025-08-16 20:48:24 +02:00
40baff5cb0 Update ConsoleView input behavior with auto-focus, enhance console styling, and refine RegisterView rendering 2025-08-15 21:42:50 +02:00
11da7fd588 Add debug println statements in Shell.execCommand and adjust ConsoleView input handling 2025-08-15 14:31:33 +02:00
4b17ce3cf5 Add DisplayView and ConsoleView components, integrate them into MTMCView, and update styles and main initialization logic 2025-08-15 14:27:31 +02:00
d5314ce046 Add MemoryView component for memory rendering, refactor BufferedImage for RGB manipulation, and cleanup unused methods and comments in several classes. 2025-08-14 21:03:03 +02:00
9f295b2fb9 Add platform-specific requestAnimationFrame, refactor redundant string accessor methods, and introduce BufferedImage and Color classes for emulator graphics rendering. 2025-08-14 16:49:39 +02:00
43 changed files with 8098 additions and 286 deletions

7
.idea/runConfigurations/MTMC_debug.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="MTMC debug" type="JavascriptDebugType" uri="http://localhost:4001/">
<method v="2">
<option name="Gradle.BeforeRunTask" enabled="true" tasks="jsBrowserDevelopmentExecutableDistribution" externalProjectPath="$PROJECT_DIR$" vmOptions="" scriptParameters="" />
</method>
</configuration>
</component>

10
.idea/runConfigurations/MainKt.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="MainKt" type="JetRunConfigurationType" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="mtmc.MainKt" />
<module name="mtmc-web.jvmMain" />
<shortenClasspath name="NONE" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

13
LICENSE.txt Normal file
View File

@@ -0,0 +1,13 @@
Zero-Clause BSD
=============
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -1,5 +1,6 @@
@file:OptIn(ExperimentalDistributionDsl::class)
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalDistributionDsl
plugins {
@@ -21,7 +22,15 @@ repositories {
kotlin {
jvmToolchain(21)
jvm()
jvm {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
binaries {
// Configures a JavaExec task named "runJvm" and a Gradle distribution for the "main" compilation in this target
executable {
mainClass.set("mtmc.MainKt")
}
}
}
js {
binaries.executable()
browser {
@@ -40,9 +49,6 @@ kotlin {
val commonMain by getting {
dependencies {
api("nl.astraeus:kotlin-simple-logging:1.1.1")
api("nl.astraeus:kotlin-css-generator:1.0.10")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
}
}
val commonTest by getting
@@ -67,9 +73,9 @@ kotlin {
}
val jsMain by getting {
dependencies {
implementation("nl.astraeus:kotlin-komponent:1.2.5")
implementation("nl.astraeus:kotlin-komponent:1.2.8")
}
}
val jsTest by getting
}
}
}

View File

@@ -1,6 +1,6 @@
#Sun Apr 28 09:54:33 CEST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

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

@@ -34,4 +34,4 @@ class Data(labels: MutableList<MTMCToken>, lineNumber: Int) : ASMElement(labels,
override fun addError(err: String) {
addError(labels.last(), err)
}
}
}

View File

@@ -57,7 +57,7 @@ abstract class Instruction(
if (ls != null) {
return ls
}
val jumpReg = JumpInstruction.disassemble(instruction)
val jumpReg = JumpRegisterInstruction.disassemble(instruction)
if (jumpReg != null) {
return jumpReg
}
@@ -65,7 +65,7 @@ abstract class Instruction(
if (jump != null) {
return jump
}
return "<unknown>"
return ""
}
}
}

View File

@@ -26,10 +26,16 @@ class JumpRegisterInstruction(
companion object {
fun disassemble(instruction: Short): String? {
if (BinaryUtils.getBits(16, 5, instruction).toInt() == 9) {
if (BinaryUtils.getBits(16, 4, instruction).toInt() == 9) {
val reg = BinaryUtils.getBits(4, 4, instruction)
val sb = StringBuilder("jr")
sb.append(fromInteger(reg.toInt()))
if (reg.toInt() == 11) {
return "ret"
} else {
val sb = StringBuilder("jr")
sb.append(" ")
sb.append(fromInteger(reg.toInt()))
return sb.toString()
}
}
return null
}

View File

@@ -0,0 +1,20 @@
package mtmc.emulator
expect fun createBufferedImage(width: Int, height: Int): BufferedImage
expect fun createCanvasImage(width: Int, height: Int): BufferedImage
expect fun createGLImage(width: Int, height: Int): BufferedImage
interface BufferedImage {
val width: Int
val height: Int
fun getRGB(x: Int, y: Int): Int
fun setRGB(x: Int, y: Int, intVal: Int)
}
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,7 +2,11 @@ package mtmc.emulator
import mtmc.emulator.MonTanaMiniComputer.ComputerStatus
import mtmc.util.currentTimeMillis
import mtmc.util.requestAnimationFrame
import mtmc.util.setTimeout
import mtmc.util.time
import kotlin.math.max
import kotlin.math.min
/**
*
@@ -11,49 +15,66 @@ 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 lastFrame = 0.0
var speed: Long = 0
var instructionsToRun = 0.0
var frame = 0
fun run() {
var instructions: Long = 0
var ips: Long = 0
var expected: Long = 0
var virtual: Long = 0
requestAnimationFrame { handleFrame() }
}
var startTime = currentTimeMillis()
var deltaStart: Long
var delta: Long
fun handleFrame() {
// figure out how many instructions to execute this 'time' duration
// maximize time so we don't hang if the emulator is too slow
val time = time()
if (lastFrame == 0.0) {
lastFrame = time
requestAnimationFrame { handleFrame() }
return
}
val delta = time - lastFrame
lastFrame = time
var speed: Long = 0
var pulse: Long
var ms: Long = 10
val timeToProcess = min(delta, 16.0)
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 = 1L
}
instructionsToRun += timeToProcess * speed / 1000.0
val pulse: Long = instructionsToRun.toLong() + 1
instructionsToRun -= pulse
/* 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) {
}
val start = time()
val ir = computer.pulse(pulse)
instructions += ir
val duration = (time() - start)
val actual = ir / (delta / 1000.0)
if (frame % 100 == 0) {
println("Instructions ran: $ir (delta = ${delta.toFloat()}, timeToProcess = ${timeToProcess.toFloat()}, speed = $speed (actual=${actual.toLong()}), duration = $duration)")
}
instructions += computer.pulse(pulse)
virtual += instructions
ips = (instructions / timeToProcess).toLong()
frame++
virtual += pulse
ips = (virtual * 1000) / max(1, currentTimeMillis() - startTime)
expected = (currentTimeMillis() - startTime) * speed / 1000
if (duration > timeToProcess) {
setTimeout({ handleFrame() })
} else {
requestAnimationFrame { handleFrame() }
}
}
//println("Executed " + instructions + " instructions at a rate of " + ips + " ips (speed = " + speed + ")")

View File

@@ -1,17 +1,16 @@
package mtmc.emulator
import mtmc.os.fs.Console
import mtmc.os.fs.System
import mtmc.os.shell.Shell
import mtmc.tokenizer.MTMCScanner
import mtmc.tokenizer.MTMCToken
class MTMCConsole(private val computer: MonTanaMiniComputer) {
var mode: Mode = Mode.NON_INTERACTIVE
var mode: Mode = Mode.INTERACTIVE
var sysConsole: Console? = null
// non-interactive data
private val output = StringBuilder()
private var output = StringBuilder()
private var shortValueSet = false
private var shortValue: Short = 0
private var stringValue: String? = null
@@ -19,12 +18,7 @@ class MTMCConsole(private val computer: MonTanaMiniComputer) {
// TODO invert so shell is driving and console is just IO
fun start() {
mode = Mode.INTERACTIVE
sysConsole = System.console
Shell.printShellWelcome(computer)
while (true) {
val cmd = sysConsole!!.readLine("mtmc > ")
computer.oS.processCommand(cmd)
}
}
fun println(x: String) {
@@ -89,7 +83,9 @@ class MTMCConsole(private val computer: MonTanaMiniComputer) {
val text = if (index >= 0) output.substring(0, index + 1) else ""
if (index >= 0) {
output.removeRange(0, index + 1)
val updated = StringBuilder()
updated.append(output.removeRange(0, index + 1))
output = updated
}
return text
@@ -122,7 +118,7 @@ class MTMCConsole(private val computer: MonTanaMiniComputer) {
}
fun resetOutput() {
output.removeRange(0, output.length)
output.clear()
}
enum class Mode {

File diff suppressed because one or more lines are too long

View File

@@ -18,7 +18,7 @@ class MonTanaMiniComputer {
var memory: ByteArray = ByteArray(MEMORY_SIZE)
var breakpoints: ByteArray = ByteArray(MEMORY_SIZE)
private var status: ComputerStatus? = ComputerStatus.READY
var speed: Int = 1000000
var speed: Int = 1
set(speed) {
field = speed
this.notifyOfExecutionUpdate()
@@ -31,7 +31,9 @@ class MonTanaMiniComputer {
var display: MTMCDisplay = MTMCDisplay(this)
var clock: MTMCClock = MTMCClock(this)
var fileSystem: FileSystem = FileSystem(this)
var rewindSteps = mutableListOf<RewindStep>()
var rewindSteps = Array(MAX_REWIND_STEPS) { RewindStep() }
var rewindIndex = -1
// listeners
private val observers = mutableListOf<MTMCObserver>()
@@ -46,34 +48,33 @@ class MonTanaMiniComputer {
registerFile = ShortArray(Register.entries.size)
memory = ByteArray(MEMORY_SIZE)
breakpoints = ByteArray(MEMORY_SIZE)
rewindSteps.clear()
setRegisterValue(
Register.SP,
MEMORY_SIZE.toShort().toInt()
) // default the stack pointer to the top of memory
rewindSteps.clear()
rewindIndex = -1
observers.forEach { obj ->
obj.computerReset()
}
}
fun load(code: ByteArray, data: ByteArray, debugInfo: DebugInfo?) {
load(code, data, Array(0) { ByteArray(0) }, debugInfo)
}
fun load(code: ByteArray, data: ByteArray, graphics: Array<ByteArray>, debugInfo: DebugInfo?) {
fun load(
code: ByteArray,
data: ByteArray,
graphics: Array<ByteArray> = arrayOf(),
debugInfo: DebugInfo? = null
) {
this.debugInfo = debugInfo
// reset memory
initMemory()
val codeBoundary = code.size
code.copyInto(memory, 0, 0, codeBoundary)
setRegisterValue(Register.CB, codeBoundary - 1)
val dataBoundary = codeBoundary + data.size
code.copyInto(memory, codeBoundary, 0, data.size)
data.copyInto(memory, codeBoundary, 0, data.size)
setRegisterValue(Register.DB, dataBoundary - 1)
@@ -132,10 +133,9 @@ class MonTanaMiniComputer {
}
fun fetchAndExecute() {
currentRewindStep = RewindStep()
currentRewindStep?.let {
rewindSteps.add(0, it)
}
rewindIndex = (rewindIndex + 1) % rewindSteps.size
rewindSteps[rewindIndex].index = 0
fetchCurrentInstruction()
val instruction = getRegisterValue(Register.IR)
if (isDoubleWordInstruction(instruction)) {
@@ -727,7 +727,11 @@ class MonTanaMiniComputer {
private fun badInstruction(instruction: Short) {
setStatus(ComputerStatus.PERMANENT_ERROR)
// TODO implement flags
console.println("BAD INSTRUCTION: 0x" + (instruction.toInt() and 0xFFFF).toHexString())
console.println(
"BAD INSTRUCTION: 0x" + (instruction.toHexString()) + " at PC " + getRegisterValue(
Register.PC
)
)
}
fun fetchCurrentInstruction() {
@@ -749,7 +753,7 @@ class MonTanaMiniComputer {
val upperByte = fetchByteFromMemory(address).toShort()
val lowerByte = fetchByteFromMemory(address + 1)
var value = (upperByte.toInt() shl 8).toShort()
val i = value.toInt() or lowerByte.toInt()
val i = value.toInt() + (lowerByte.toInt() and 255)
value = i.toShort()
return value
}
@@ -785,17 +789,14 @@ class MonTanaMiniComputer {
val currentValue = memory[address]
addRewindStep { memory[address] = currentValue }
memory[address] = value
observers!!.forEach { o: MTMCObserver? ->
observers.forEach { o: MTMCObserver? ->
o!!.memoryUpdated(address, value)
}
}
private fun addRewindStep(runnable: Runnable?) {
if (currentRewindStep != null && rewindSteps != null) {
if (currentRewindStep != null) {
currentRewindStep!!.addSubStep(runnable)
if (rewindSteps.size > MAX_REWIND_STEPS) {
rewindSteps.removeLast()
}
}
}
@@ -812,7 +813,7 @@ class MonTanaMiniComputer {
registerFile[register] = currentValue
}
registerFile[register] = value.toShort()
observers!!.forEach { o: MTMCObserver? ->
observers.forEach { o: MTMCObserver? ->
o!!.registerUpdated(register, value)
}
}
@@ -843,7 +844,7 @@ class MonTanaMiniComputer {
breakpoints[address] = (if (active) 1.toByte() else 0.toByte())
}
private fun start() {
fun start() {
console.start() // start the interactive console
}
@@ -950,12 +951,15 @@ class MonTanaMiniComputer {
}
fun rewind() {
val latestRewindStep = rewindSteps.removeFirst()
latestRewindStep.rewind()
if (rewindIndex >= 0) {
val latestRewindStep = rewindSteps[rewindIndex]
rewindIndex--
latestRewindStep?.rewind()
}
}
val isBackAvailable: Boolean
get() = !rewindSteps!!.isEmpty()
get() = rewindIndex >= 0
enum class ComputerStatus {
READY,
@@ -1011,16 +1015,8 @@ class MonTanaMiniComputer {
return true
}
val isMcp = getBits(16, 8, instruction) == 5.toShort()
if (isMcp) {
return true
}
return false
}
fun main(args: Array<String>) {
val computer = MonTanaMiniComputer()
computer.speed = 1 // default to 1hz
computer.start()
return isMcp
}
}
}

View File

@@ -3,13 +3,16 @@ package mtmc.emulator
import mtmc.util.Runnable
class RewindStep {
var subSteps: MutableList<Runnable?> = ArrayList<Runnable?>()
var subSteps: Array<Runnable> = Array(10) { {} }
var index = 0
fun rewind() {
subSteps.reversed().forEach({ obj: Runnable? -> obj!!.invoke() })
}
fun addSubStep(subStep: Runnable?) {
subSteps.add(subStep)
if (subStep != null && index < subSteps.size) {
subSteps[index++] = subStep
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ import kotlin.math.min
import kotlin.random.Random
class MTOS(private val computer: MonTanaMiniComputer) {
private var timer: Long = 0
private var timer: Double = 0.0
var random: Random = Random.Default
// Editor support
@@ -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()
@@ -240,7 +240,38 @@ class MTOS(private val computer: MonTanaMiniComputer) {
try {
// special handling for game-of-life cell files
if ("cells" == fileType) {
val str = Files.readString(file.toPath())
//val str = Files.readString(file.toPath())
val str = """
! galaxy
! Kok's galaxy - a whirling oscillator
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
...............OOOOOO.OO.....
...............OOOOOO.OO
......................OO
...............OO.....OO
...............OO.....OO
...............OO.....OO
...............OO.......
...............OO.OOOOOO
...............OO.OOOOOO.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
""".trimIndent()
val lines: List<String> =
str.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
.filter { s: String? -> !s!!.startsWith("!") }
@@ -309,7 +340,7 @@ class MTOS(private val computer: MonTanaMiniComputer) {
computer.setRegisterValue(
Register.RV,
max(0, this.timer - currentTimeMillis()).toInt()
max(0.0, this.timer - currentTimeMillis()).toInt()
)
} else if (syscallNumber == getValue("drawimg").toShort()) {
val image = computer.getRegisterValue(Register.A0)

View File

@@ -16,21 +16,17 @@ class File(
val name: String
) {
var directory: Boolean = TODO("initialize me")
var directory: Boolean = false
constructor(name: String) : this(null, name)
fun getParent(): File = parent ?: error("No parent")
fun getPath(): String = if (parent == null) {
name
} else {
"${parent.getPath()}/$name"
}
fun exists(): Boolean {
TODO("Not yet implemented")
}
fun exists(): Boolean = true
fun getAbsolutePath(): String {
TODO("Not yet implemented")

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

@@ -77,6 +77,7 @@ object Shell {
}
fun execCommand(command: String, computer: MonTanaMiniComputer) {
println("Exec: $command")
val tokens = MTMCTokenizer(command, "#")
try {
val identifier = tokens.matchAndConsume(MTMCToken.TokenType.IDENTIFIER)
@@ -96,17 +97,18 @@ object Shell {
return
}
} else {
cmd = identifier.stringValue()
cmd = identifier.stringValue
}
if (isCommand(cmd)) {
COMMANDS[cmd.lowercase()]!!.exec(tokens, computer)
} else {
println("Executing: $command")
tokens.reset()
val asm = mutableListOf<MTMCToken>()
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

@@ -11,13 +11,9 @@ object BinaryUtils {
return 0
}
val returnValue = (value.toInt() and 0xffff) ushr (start - totalBits)
var mask = 0
var toShift = totalBits
while (toShift > 0) {
toShift--
mask = mask shl 1
mask = mask + 1
}
var mask = 1
mask = mask shl totalBits
mask--
return (returnValue and mask).toShort()
}

View File

@@ -1,3 +1,11 @@
package mtmc.util
expect fun currentTimeMillis(): Long
expect fun currentTimeMillis(): Double
expect fun time(): Double
expect fun requestAnimationFrame(action: (Double) -> Unit)
expect fun setTimeout(action: () -> Unit)
expect fun immediateTimeout(action: (Double) -> Unit): Int

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,43 @@
package mtmc
import kotlinx.browser.document
import kotlinx.html.div
import nl.astraeus.komp.HtmlBuilder
import kotlinx.browser.window
import mtmc.emulator.MonTanaMiniComputer
import mtmc.util.currentTimeMillis
import mtmc.view.DisplayView
import mtmc.view.MTMCView
import nl.astraeus.komp.Komponent
class HelloKomponent : Komponent() {
override fun HtmlBuilder.render() {
div {
+ "Hello, world!"
}
}
}
val computer = MonTanaMiniComputer()
val mainView = MTMCView(computer)
val display = DisplayView(computer)
fun main() {
Komponent.create(document.body!!, HelloKomponent())
computer.speed = 1000000
computer.load(lifeCode, lifeData)
//computer.load(snakeCode, snakeData)
Komponent.create(document.body!!, mainView)
computer.start()
mainView.requestUpdate()
display.requestUpdate()
window.requestAnimationFrame { updateJsDisplay() }
}
var lastMemoryUpdate = currentTimeMillis()
var updateState = true
fun updateJsDisplay() {
display.requestUpdate()
if (currentTimeMillis() - lastMemoryUpdate > 125) {
if (updateState) {
mainView.registerView.requestUpdate()
mainView.memoryView.requestUpdate()
}
lastMemoryUpdate = currentTimeMillis()
}
window.requestAnimationFrame { updateJsDisplay() }
}

View File

@@ -0,0 +1,116 @@
package mtmc.emulator
import kotlinx.browser.document
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get
import org.khronos.webgl.set
import org.w3c.dom.CanvasRenderingContext2D
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.ImageData
actual fun createBufferedImage(
width: Int,
height: Int
): BufferedImage = BufferedImageImpl(width, height, 1)
class BufferedImageImpl(
override val width: Int,
override val height: Int,
val type: Int
) : BufferedImage {
val display = ByteArray(width * height * 4)
override fun getRGB(x: Int, y: Int): Int {
check(x in 0 until width && y in 0 until height)
val offset = (x + y * width) * 4
return display[offset + 0].toInt() +
display[offset + 1].toInt() shl 8 +
display[offset + 2].toInt() shl 16
}
override fun setRGB(x: Int, y: Int, intVal: Int) {
check(x in 0 until width && y in 0 until height)
val offset = (x + y * width) * 4
display[offset + 0] = (intVal shr 16).toByte()
display[offset + 1] = (intVal shr 8).toByte()
display[offset + 2] = intVal.toByte()
display[offset + 3] = 255.toByte()
}
}
class BufferedImageData(
val imageData: ImageData,
) : BufferedImage {
override val width: Int = imageData.width
override val height: Int = imageData.height
override fun getRGB(x: Int, y: Int): Int {
check(x in 0 until width && y in 0 until height)
val offset = (x * 4 + y * width * 4)
val display = imageData.data
return (display[offset + 0].toInt() and 0xff) shl 16 +
(display[offset + 1].toInt() and 0xff) shl 8 +
(display[offset + 2].toInt() and 0xff)
}
override fun setRGB(x: Int, y: Int, intVal: Int) {
check(x in 0 until width && y in 0 until height)
val offset = (x * 4 + y * width * 4)
val display = imageData.data
display[offset + 0] = ((intVal shr 16) and 0xff).toShort().asDynamic()
display[offset + 1] = ((intVal shr 8) and 0xff).toShort().asDynamic()
display[offset + 2] = (intVal and 0xff).toShort().asDynamic()
display[offset + 3] = 255.toShort().asDynamic()
}
}
class BufferedImageDataWebGl(
override val width: Int,
override val height: Int,
val data: Uint8Array = Uint8Array(width * height * 4)
) : BufferedImage {
init {
for (x in 0 until width) {
for (y in 0 until height) {
val offset = (x * 4 + y * width * 4)
data[offset + 3] = 255.toShort().asDynamic()
}
}
}
override fun getRGB(x: Int, y: Int): Int {
check(x in 0 until width && y in 0 until height)
val offset = (x * 4 + y * width * 4)
val display = data
return (display[offset + 0].toInt() and 0xff) shl 16 +
(display[offset + 1].toInt() and 0xff) shl 8 +
(display[offset + 2].toInt() and 0xff) shl 0 + 255
}
override fun setRGB(x: Int, y: Int, intVal: Int) {
check(x in 0 until width && y in 0 until height)
val offset = (x * 4 + y * width * 4)
data[offset + 0] = ((intVal shr 16) and 0xff).toShort().asDynamic()
data[offset + 1] = ((intVal shr 8) and 0xff).toShort().asDynamic()
data[offset + 2] = ((intVal shr 0) and 0xff).toShort().asDynamic()
//data[offset + 3] = 255.toShort().asDynamic()
}
}
val canvas = document.createElement("canvas") as HTMLCanvasElement
val ctx = canvas.getContext("2d") as CanvasRenderingContext2D
actual fun createCanvasImage(width: Int, height: Int): BufferedImage {
return BufferedImageData(ctx.createImageData(width.toDouble(), height.toDouble()));
}
actual fun createGLImage(width: Int, height: Int): BufferedImage {
return BufferedImageDataWebGl(width, height)
}

View File

@@ -1,5 +1,19 @@
package mtmc.util
import kotlinx.browser.window
import kotlin.js.Date
actual fun currentTimeMillis(): Long = Date().getTime().toLong()
actual fun currentTimeMillis(): Double = Date().getTime()
actual fun requestAnimationFrame(action: (Double) -> Unit) {
window.requestAnimationFrame {
action(it)
}
}
actual fun immediateTimeout(action: (Double) -> Unit): Int = window.setTimeout(action, 0)
actual fun time(): Double = window.performance.now()
actual fun setTimeout(action: () -> Unit) {
window.setTimeout(action)
}

View File

@@ -0,0 +1,86 @@
package mtmc.view
import kotlinx.browser.window
import kotlinx.html.div
import kotlinx.html.input
import kotlinx.html.js.onClickFunction
import kotlinx.html.js.onKeyUpFunction
import kotlinx.html.span
import mtmc.display
import mtmc.emulator.MonTanaMiniComputer
import mtmc.mainView
import mtmc.os.shell.Shell
import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent
import nl.astraeus.komp.currentElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.KeyboardEvent
class ConsoleView(
val computer: MonTanaMiniComputer
) : Komponent() {
var input: String = ""
var output = StringBuilder()
private var inputElement: HTMLInputElement? = null
init {
output.append(computer.console.consumeLines())
}
override fun HtmlBuilder.render() {
div("console") {
onClickFunction = {
inputElement?.focus()
}
div("console-history") {
div {
+output.toString()
}
}
div("console-input") {
span("console-prompt") {
+"mtmc$"
}
input(classes = "console-input") {
value = input
autoFocus = true
inputElement = currentElement() as? HTMLInputElement
currentElement().scrollIntoView()
window.setTimeout({
inputElement?.focus()
}, 0)
onKeyUpFunction = { it ->
(it as? KeyboardEvent)?.let { ke ->
(ke.target as? HTMLInputElement)?.let { inp ->
if (ke.key == "Enter") {
handleCommand()
} else {
input = inp.value
}
}
}
}
}
}
}
}
private fun handleCommand() {
computer.console.print("mtmc$ ")
computer.console.println(input)
Shell.execCommand(input, computer)
input = ""
output.append(computer.console.consumeLines())
if (output.length > 1000 && output.contains("\n")) {
output = output.deleteRange(0, output.indexOf("\n"))
}
mainView.registerView.requestUpdate()
mainView.memoryView.requestUpdate()
display.requestUpdate()
requestUpdate()
}
}

View File

@@ -0,0 +1,157 @@
package mtmc.view
import kotlinx.html.InputType
import kotlinx.html.button
import kotlinx.html.div
import kotlinx.html.i
import kotlinx.html.input
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction
import kotlinx.html.label
import kotlinx.html.option
import kotlinx.html.select
import kotlinx.html.span
import mtmc.display
import mtmc.emulator.MonTanaMiniComputer
import mtmc.mainView
import mtmc.updateState
import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent
import org.w3c.dom.HTMLSelectElement
import kotlin.text.Typography.nbsp
class ControlView(
val computer: MonTanaMiniComputer
) : Komponent() {
override fun HtmlBuilder.render() {
div("control-panel") {
div("control-header") {
span {
+"m"
}
span {
+"s"
}
span {
+"u"
}
nbsp
i {
+"MTMC-16"
}
}
div("control-secondary") {
+"MonTana Mini-Computer"
}
div("control-buttons") {
label {
+"Update:"
htmlFor = "update-state"
input {
type = InputType.checkBox
checked = updateState
onClickFunction = {
updateState = !updateState
requestUpdate()
}
}
}
label {
select {
name = "speed"
option {
value = "1"
+"1 Hz"
}
option {
value = "10"
+"10 Hz"
}
option {
value = "100"
+"100 Hz"
}
option {
value = "1000"
+"1 Khz"
}
option {
value = "1000000"
selected = true
+"1 Mhz"
}
option {
value = "2000000"
+"2 Mhz"
}
option {
value = "5000000"
+"5 Mhz"
}
option {
value = "10000000"
+"10 Mhz"
}
onChangeFunction = {
val target = it.target as? HTMLSelectElement
target?.value?.toIntOrNull()?.let { speed ->
computer.speed = speed
}
}
}
}
// Buttons with fx-* attributes
button {
if (computer.getStatus() != MonTanaMiniComputer.ComputerStatus.EXECUTING) {
+"run"
onClickFunction = {
computer.run()
requestUpdate()
}
} else {
+"pause"
onClickFunction = {
computer.pause()
requestUpdate()
}
}
}
button {
disabled =
computer.getStatus() == MonTanaMiniComputer.ComputerStatus.WAITING && computer.rewindIndex >= 0
+"back"
onClickFunction = {
computer.back()
mainView.requestUpdate()
display.requestUpdate()
}
}
button {
+"step"
onClickFunction = {
computer.step()
mainView.requestUpdate()
display.requestUpdate()
}
}
button {
+"reset"
onClickFunction = {
computer.initMemory()
mainView.requestUpdate()
display.requestUpdate()
}
}
}
}
}
}

View File

@@ -0,0 +1,173 @@
package mtmc.view
import kotlinx.html.canvas
import kotlinx.html.div
import mtmc.display
import mtmc.emulator.BufferedImageData
import mtmc.emulator.BufferedImageDataWebGl
import mtmc.emulator.MonTanaMiniComputer
import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent
import nl.astraeus.komp.currentElement
import org.khronos.webgl.Float32Array
import org.khronos.webgl.WebGLBuffer
import org.khronos.webgl.WebGLProgram
import org.khronos.webgl.WebGLRenderingContext
import org.khronos.webgl.WebGLShader
import org.khronos.webgl.WebGLTexture
import org.w3c.dom.HTMLCanvasElement
// language=GLSL
val vertexShader = """
attribute vec2 a_pos;
attribute vec2 a_uv;
varying vec2 v_uv;
void main() {
v_uv = a_uv;
gl_Position = vec4(a_pos, 0.0, 1.0);
}
"""
// language=GLSL
val fragmentShader = """
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_tex;
void main() {
gl_FragColor = texture2D(u_tex, v_uv);
}
""".trimIndent()
typealias GL = WebGLRenderingContext
class DiplayControlView(
val computer: MonTanaMiniComputer
) : Komponent() {
override fun HtmlBuilder.render() {
div("display") {
include(display)
}
}
}
class DisplayView(
val computer: MonTanaMiniComputer
) : Komponent() {
var ctx: WebGLRenderingContext? = null
var program: WebGLProgram? = null
var texture: WebGLTexture? = null
var buffer: WebGLBuffer? = null
override fun HtmlBuilder.render() {
canvas("display-canvas") {
width = "160px"
height = "144px"
val cv = currentElement() as? HTMLCanvasElement
ctx = cv?.getContext("webgl")?.unsafeCast<WebGLRenderingContext>()
if (program == null) {
createProgram()
createBuffer()
createTexture()
}
}
}
private fun createTexture() {
ctx?.let { gl: WebGLRenderingContext ->
texture = gl.createTexture()
gl.bindTexture(GL.TEXTURE_2D, texture)
// Set texture parameters for pixel-perfect rendering
gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE)
gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE)
gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.NEAREST)
gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MAG_FILTER, GL.NEAREST)
}
}
private fun createBuffer() {
ctx?.let { gl ->
// Create quad vertices
val vertices = Float32Array(
arrayOf(
-1f, -1f, 0f, 1f, // position, texCoord
1f, -1f, 1f, 1f,
-1f, 1f, 0f, 0f,
1f, 1f, 1f, 0f
)
)
buffer = gl.createBuffer();
gl.bindBuffer(GL.ARRAY_BUFFER, buffer);
gl.bufferData(GL.ARRAY_BUFFER, vertices, GL.STATIC_DRAW);
}
}
private fun createProgram() {
val vs = createShader(WebGLRenderingContext.VERTEX_SHADER, vertexShader)
val fs = createShader(WebGLRenderingContext.FRAGMENT_SHADER, fragmentShader)
val prog = ctx?.createProgram()
if (vs != null && fs != null && prog != null) {
ctx?.attachShader(prog, vs)
ctx?.attachShader(prog, fs)
ctx?.linkProgram(prog)
}
program = prog
}
private fun createShader(type: Int, source: String): WebGLShader? {
var result: WebGLShader? = null
ctx?.let { gl ->
result = gl.createShader(type)
result?.let { shader ->
gl.shaderSource(shader, source)
gl.compileShader(shader)
}
}
return result
}
override fun renderUpdate() {
// move data to canvas
val buffer = computer.display.buffer
if (buffer is BufferedImageData) {
//ctx?.putImageData(buffer.imageData, 0.0, 0.0)
} else if (buffer is BufferedImageDataWebGl) {
ctx?.let { gl ->
gl.clear(GL.COLOR_BUFFER_BIT)
gl.useProgram(program)
val positionLocation = gl.getAttribLocation(program, "a_pos")
val texCoordLocation = gl.getAttribLocation(program, "a_uv")
gl.bindBuffer(GL.ARRAY_BUFFER, this.buffer)
gl.enableVertexAttribArray(positionLocation)
gl.vertexAttribPointer(positionLocation, 2, GL.FLOAT, false, 16, 0)
gl.enableVertexAttribArray(texCoordLocation)
gl.vertexAttribPointer(texCoordLocation, 2, GL.FLOAT, false, 16, 8)
gl.bindTexture(GL.TEXTURE_2D, texture);
gl.texImage2D(
GL.TEXTURE_2D,
0, // level
GL.RGBA, // internal format
buffer.width,
buffer.height,
0, // border
GL.RGBA, // format
GL.UNSIGNED_BYTE, // type
buffer.data // data
)
// Draw quad
gl.drawArrays(GL.TRIANGLE_STRIP, 0, 4);
}
}
}
}

View File

@@ -0,0 +1,34 @@
package mtmc.view
import kotlinx.html.div
import mtmc.emulator.MonTanaMiniComputer
import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent
class MTMCView(
val computer: MonTanaMiniComputer
) : Komponent() {
val controlView = ControlView(computer)
val registerView = RegisterView(computer)
val memoryView = MemoryView(computer)
val displayView = DiplayControlView(computer)
val consoleView = ConsoleView(computer)
override fun HtmlBuilder.render() {
div("container") {
div("left-column") {
include(controlView)
include(registerView)
include(memoryView)
}
div("middle-column") {
include(displayView)
include(consoleView)
}
div("right-column") {
+"Files"
}
}
}
}

View File

@@ -0,0 +1,147 @@
package mtmc.view
import kotlinx.html.classes
import kotlinx.html.div
import kotlinx.html.table
import kotlinx.html.td
import kotlinx.html.title
import kotlinx.html.tr
import mtmc.asm.instructions.Instruction
import mtmc.emulator.MonTanaMiniComputer
import mtmc.emulator.Register
import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent
fun ByteArray.asHex(address: Int): String {
val value = getShort(address)
return value.toHexString()
}
fun ByteArray.getShort(address: Int): Short {
val value = (this[address].toInt() and 0xff) * 256 + (this[address + 1].toInt() and 0xff)
return value.toShort()
}
enum class DisplayFormat {
DYN,
HEX,
DEC,
INS,
STR
}
private val ASCII_CODES = arrayOf<String>(
"NUL", "SOH", "STX", "ETX",
"EOT", "ENT", "ACK", "BEL",
"BS", "\\t", "\\n", "VT",
"\\f", "\\r", "SO", "SI",
"DLE", "DC1", "DC2", "DC3",
"DC4", "NAK", "SYN", "ETB",
"CAN", "EM", "SUB", "ESC",
"FS", "GS", "RS", "US",
)
class MemoryView(
val computer: MonTanaMiniComputer
) : Komponent() {
var displayFormat: DisplayFormat = DisplayFormat.DYN
override fun HtmlBuilder.render() {
div("memory-panel") {
div("memory-header") {
+"Memory"
}
div("memory") {
table {
for (baseAddress in 0..<computer.memory.size step 16) {
tr {
var previousInstruction = 0x0fff.toShort()
for (offset in 0..<8) {
val address = baseAddress + offset * 2
val memoryClass = classFor(address)
val df = computeMemoryFormat(memoryClass)
val value = computer.memory.getShort(address)
td {
when (memoryClass) {
"curr" -> classes += "current-address"
"code" -> classes += "code"
"data" -> classes += "data"
"heap" -> classes += "heap"
"sta" -> classes += "stack"
}
title = address.toString() + " - " + value.toHexString()
when (df) {
DisplayFormat.DEC -> +value.toInt().toString()
DisplayFormat.INS -> {
+Instruction.disassemble(
value,
previousInstruction
)
previousInstruction = value
}
DisplayFormat.HEX -> +value.toHexString()
DisplayFormat.STR -> +get1252String(value)
else -> +"?"
}
}
}
}
}
}
}
}
}
fun classFor(address: Int): String = if (address >= computer.getRegisterValue(Register.SP)) {
"sta"
} else if (address == computer.getRegisterValue(Register.PC).toInt()) {
"curr"
} else if (address <= computer.getRegisterValue(Register.CB)) {
"code"
} else if (address <= computer.getRegisterValue(Register.DB)) {
"data"
} else if (address <= computer.getRegisterValue(Register.BP)) {
"heap"
} else {
""
}
private fun computeMemoryFormat(memoryClass: String?): DisplayFormat {
return if (displayFormat == DisplayFormat.DYN) {
when (memoryClass) {
"sta" -> DisplayFormat.DEC
"code", "curr" -> DisplayFormat.INS
"data", "heap" -> DisplayFormat.STR
else -> DisplayFormat.HEX
}
} else {
displayFormat
}
}
fun get1252String(value: Short): String {
val topByte = (value.toInt() ushr 8).toByte()
val bottomByte = value.toByte()
return if (topByte.toInt() != 0) {
get1252String(topByte) + " " + get1252String(
bottomByte
)
} else {
get1252String(bottomByte)
}
}
private fun get1252String(value: Byte): String = if (value in 0..<32) {
ASCII_CODES[value.toInt()]
} else {
"" + (value.toInt() and 0xff).toChar()
}
}

View File

@@ -0,0 +1,117 @@
package mtmc.view
import kotlinx.browser.document
import kotlinx.dom.addClass
import kotlinx.dom.removeClass
import kotlinx.html.TABLE
import kotlinx.html.classes
import kotlinx.html.div
import kotlinx.html.hr
import kotlinx.html.id
import kotlinx.html.span
import kotlinx.html.table
import kotlinx.html.td
import kotlinx.html.tr
import mtmc.emulator.MonTanaMiniComputer
import mtmc.emulator.Register
import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent
import org.w3c.dom.HTMLElement
class RegisterView(
val computer: MonTanaMiniComputer
) : Komponent() {
override fun HtmlBuilder.render() {
div {
table("register-table") {
for (index in 0..<16) {
showRegister(index)
}
tr {
td {
colSpan = "3"
hr {}
}
}
showRegister(16)
showRegister(17)
tr {
td("flags") {
colSpan = "3"
span {
+"flags t:"
div("blinken") {
id = "flags-t"
if (!computer.isFlagTestBitSet) {
classes += "off"
}
}
}
span {
+"o:"
div("blinken") {
id = "flags-o"
classes += "off"
}
}
span {
+"e:"
div("blinken") {
id = "flags-e"
if (computer.getStatus() != MonTanaMiniComputer.ComputerStatus.PERMANENT_ERROR) {
classes += "off"
}
}
}
}
}
}
}
}
override fun renderUpdate() {
for (register in 0..<18) {
val value = computer.registerFile[register]
for (bit in 15 downTo 0) {
val element = document.getElementById("bit-$bit-$register") as? HTMLElement ?: continue
if (value.toInt() and (1 shl bit) == 0) {
element.addClass("off")
} else {
element.removeClass("off")
}
}
val element = document.getElementById("register-$register") as? HTMLElement ?: continue
element.innerText = "${value.toInt() and 0xffff}"
}
}
private fun TABLE.showRegister(register: Int) {
val name = Register.fromInteger(register)
val value = computer.registerFile[register]
tr {
td {
+name
}
td("register-lights") {
for (bit in 15 downTo 0) {
div("blinken") {
id = "bit-$bit-$register"
if (value.toInt() and (1 shl bit) == 0) {
classes += "off"
}
if (bit % 4 == 0) {
classes += "space"
}
}
}
}
td("align-right") {
id = "register-$register"
+"${value.toInt() and 0xffff}"
}
}
}
}

View File

@@ -0,0 +1,256 @@
:root {
--pdp-blue: #243571;
--pdp-light-blue: #3286ce;
--pdp-beige: #fdfddc;
--pdp-white: #f1f1f6;
--pdp-off-white: #F0EBCD;
--filetree-gray: #666;
}
html, body {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
overscroll-behavior: none;
}
.container {
display: grid;
grid-template-columns: 450px 450px 1fr;
grid-template-rows: 1fr;
overflow-y: auto;
gap: 5px;
background-color: var(--pdp-white);
}
.left-column {
grid-template-rows: auto auto minmax(0, 1fr);
display: grid;
grid-gap: 4px;
height: 100dvh;
max-height: 100dvh;
min-width: 450px;
max-width: 450px;
}
.middle-column {
background-color: #eee;
grid-template-rows: auto minmax(0, 1fr);
display: grid;
grid-gap: 4px;
height: 100dvh;
max-height: 100dvh;
min-width: 450px;
max-width: 450px;
}
.right-column {
background-color: #eee;
grid-template-rows: auto;
display: grid;
grid-gap: 4px;
height: 100dvh;
max-height: 100dvh;
min-width: 450px;
}
.align-right {
text-align: right;
}
/* control */
.control-panel > * {
margin: 4px;
}
.control-header {
background-color: var(--pdp-blue);
color: var(--pdp-white);
font-family: monospace;
font-size: 32px;
padding: 4px 24px;
margin-left: 0;
margin-right: 0;
}
.control-header span {
display: inline-block;
margin-top: -6px;
margin-bottom: -6px;
padding: 4px 8px;
font-weight: bold;
font-size: 38px;
border-right: 4px solid white;
border-left: 4px solid white;
}
.control-header span:not(:first-child) {
border-left: none;
}
.control-secondary {
background-color: var(--pdp-light-blue);
color: var(--pdp-white);
font-family: monospace;
font-size: 18px;
font-style: italic;
text-align: center;
padding: 4px 4px;
margin-left: 0;
margin-right: 0;
}
.control-buttons {
text-align: right;
}
/* registers */
table.register-table {
font-family: monospace;
width: 100%;
padding: 5px;
background-color: black;
color: white;
border-radius: 8px;
}
table.register-table tr td.align-right {
text-align: right;
min-width: 100px;
}
table.register-table tr td.register-lights {
padding-left: 20px;
}
table.register-table tr td.flags {
text-align: center;
}
table.register-table tr td.flags span {
margin-left: 8px;
}
table.register-table tr td.flags span .blinken {
margin-left: 4px;
top: 2px;
position: relative;
}
.blinken {
display: inline-block;
margin-right: 2px;
height: 6px;
width: 6px;
border-radius: 6px;
padding: 3px;
background-color: red;
}
.blinken.off {
background-color: #5c0119;
}
.blinken.space {
margin-right: 8px;
}
/* memory */
.memory-panel {
background-color: #eee;
display: flex;
flex-direction: column;
}
.memory-header {
font-family: monospace;
font-weight: bold;
font-size: 16px;
/* overflow: auto; */
padding: 4px;
}
.memory {
overflow: auto;
}
.memory table tr td {
font-family: monospace;
font-size: 8px;
}
.current-address {
background-color: #78e878;
}
.code {
background-color: darkseagreen;
}
.data {
background-color: lightgoldenrodyellow;
}
.heap {
background-color: lightcoral;
}
.stack {
background-color: lightsalmon;
}
/* display */
.display {
min-height: 480px;
}
.display-canvas {
padding: 24px 64px;
width: 320px;
height: 288px;
image-rendering: pixelated; /* Keeps sharp pixels, no smoothing */
image-rendering: -moz-crisp-edges; /* Firefox */
image-rendering: crisp-edges; /* Standard */
}
/* console */
.console {
color: #ffcc00;
background-color: #35291c;
white-space: pre-wrap;
font-family: monospace;
font-weight: bold;
padding: 5px;
overflow: auto;
}
.console-prompt {
margin-right: 6px;
}
input.console-input {
color: #ffcc00;
background-color: #35291c;
border: none;
outline: none;
font-weight: bold;
font-family: monospace;
}
.small-button {
transform: rotate(-25deg) translateY(20px) translateX(-20px);
margin: 2px;
width: 50px;
background-color: #7e777b;
border: 1px solid dimgray;
border-radius: 8px;
color: whitesmoke;
box-shadow: rgb(0, 0, 0, .2) 2px 2px 1px 1px;
}

View File

@@ -7,9 +7,9 @@ import io.undertow.predicate.Predicates
import io.undertow.server.handlers.encoding.ContentEncodingRepository
import io.undertow.server.handlers.encoding.EncodingHandler
import io.undertow.server.handlers.encoding.GzipEncodingProvider
import nl.astraeus.logger.Logger
import mtmc.db.Database
import mtmc.web.RequestHandler
import nl.astraeus.logger.Logger
val log = Logger()
@@ -58,7 +58,6 @@ fun main() {
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000)
.build()
println("Starting undertow server at port 6007...")
println("Starting undertow server at port $SERVER_PORT...")
server?.start()
}

View File

@@ -0,0 +1,13 @@
package mtmc.emulator
actual fun createBufferedImage(width: Int, height: Int): BufferedImage {
TODO("Not yet implemented")
}
actual fun createCanvasImage(width: Int, height: Int): BufferedImage {
TODO("Not yet implemented")
}
actual fun createGLImage(width: Int, height: Int): BufferedImage {
TODO("Not yet implemented")
}

View File

@@ -1,3 +1,12 @@
package mtmc.util
actual fun currentTimeMillis(): Long = System.currentTimeMillis()
actual fun currentTimeMillis(): Double = System.currentTimeMillis().toDouble()
actual fun requestAnimationFrame(action: (Double) -> Unit) {
error("requestAnimationFrame is not supported on JVM")
}
actual fun immediateTimeout(action: (Double) -> Unit): Int = 0
actual fun time(): Double = System.nanoTime() * 1000.0
actual fun setTimeout(action: () -> Unit) {}

View File

@@ -3,6 +3,7 @@ package mtmc.web
import kotlinx.html.body
import kotlinx.html.head
import kotlinx.html.html
import kotlinx.html.link
import kotlinx.html.meta
import kotlinx.html.script
import kotlinx.html.stream.appendHTML
@@ -18,7 +19,7 @@ fun generateIndex(patch: String?): String {
result.appendHTML(true).html {
head {
title(pageTitle)
//link("/css/all.min.css", "stylesheet")
link("/mtmc.css", "stylesheet")
}
body {
script(src = "/$repoName.js") {}

View File

@@ -5,12 +5,13 @@ import io.undertow.server.HttpServerExchange
import io.undertow.server.handlers.PathHandler
import io.undertow.server.handlers.resource.PathResourceManager
import io.undertow.server.handlers.resource.ResourceHandler
import io.undertow.util.Headers
import mtmc.itemUrl
import java.nio.file.Paths
import kotlin.text.startsWith
object IndexHandler : HttpHandler {
override fun handleRequest(exchange: HttpServerExchange) {
exchange.responseHeaders.put(Headers.CONTENT_TYPE, "text/html")
if (exchange.requestPath.startsWith("/$itemUrl/")) {
exchange.responseSender.send(generateIndex(null))
} else {