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) @file:OptIn(ExperimentalDistributionDsl::class)
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalDistributionDsl import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalDistributionDsl
plugins { plugins {
@@ -21,7 +22,15 @@ repositories {
kotlin { kotlin {
jvmToolchain(21) 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 { js {
binaries.executable() binaries.executable()
browser { browser {
@@ -40,9 +49,6 @@ kotlin {
val commonMain by getting { val commonMain by getting {
dependencies { dependencies {
api("nl.astraeus:kotlin-simple-logging:1.1.1") 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 val commonTest by getting
@@ -67,9 +73,9 @@ kotlin {
} }
val jsMain by getting { val jsMain by getting {
dependencies { dependencies {
implementation("nl.astraeus:kotlin-komponent:1.2.5") implementation("nl.astraeus:kotlin-komponent:1.2.8")
} }
} }
val jsTest by getting val jsTest by getting
} }
} }

View File

@@ -1,6 +1,6 @@
#Sun Apr 28 09:54:33 CEST 2024 #Sun Apr 28 09:54:33 CEST 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

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

View File

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

View File

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

View File

@@ -26,10 +26,16 @@ class JumpRegisterInstruction(
companion object { companion object {
fun disassemble(instruction: Short): String? { 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 reg = BinaryUtils.getBits(4, 4, instruction)
val sb = StringBuilder("jr") if (reg.toInt() == 11) {
sb.append(fromInteger(reg.toInt())) return "ret"
} else {
val sb = StringBuilder("jr")
sb.append(" ")
sb.append(fromInteger(reg.toInt()))
return sb.toString()
}
} }
return null 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.emulator.MonTanaMiniComputer.ComputerStatus
import mtmc.util.currentTimeMillis import mtmc.util.currentTimeMillis
import mtmc.util.requestAnimationFrame
import mtmc.util.setTimeout
import mtmc.util.time
import kotlin.math.max import kotlin.math.max
import kotlin.math.min
/** /**
* *
@@ -11,49 +15,66 @@ import kotlin.math.max
class MTMCClock( class MTMCClock(
private val computer: MonTanaMiniComputer 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() { fun run() {
var instructions: Long = 0 requestAnimationFrame { handleFrame() }
var ips: Long = 0 }
var expected: Long = 0
var virtual: Long = 0
var startTime = currentTimeMillis() fun handleFrame() {
var deltaStart: Long // figure out how many instructions to execute this 'time' duration
var delta: Long // 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 val timeToProcess = min(delta, 16.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() speed = max(computer.speed, 0).toLong()
pulse = (if (speed <= 0) 1000000 else max(speed / 100, 1)) if (speed == 0L) {
ms = (if (pulse < 10) 1000 / speed else 10) speed = 1L
deltaStart = currentTimeMillis()
delta = ms - (currentTimeMillis() - deltaStart)
/* We've lost more than a second. Recalibrate. */
if ((expected - virtual) > pulse * 100) {
startTime = deltaStart
virtual = 0
} }
instructionsToRun += timeToProcess * speed / 1000.0
val pulse: Long = instructionsToRun.toLong() + 1
instructionsToRun -= pulse
/* Throttles to every 10ms, but "catches up" if we're behind */ val start = time()
if (delta > 0 && (expected - virtual) < pulse && speed != 0L) { val ir = computer.pulse(pulse)
try { instructions += ir
Thread.sleep(delta) val duration = (time() - start)
} catch (e: InterruptedException) { 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 if (duration > timeToProcess) {
ips = (virtual * 1000) / max(1, currentTimeMillis() - startTime) setTimeout({ handleFrame() })
expected = (currentTimeMillis() - startTime) * speed / 1000 } else {
requestAnimationFrame { handleFrame() }
}
} }
//println("Executed " + instructions + " instructions at a rate of " + ips + " ips (speed = " + speed + ")") //println("Executed " + instructions + " instructions at a rate of " + ips + " ips (speed = " + speed + ")")

View File

@@ -1,17 +1,16 @@
package mtmc.emulator package mtmc.emulator
import mtmc.os.fs.Console import mtmc.os.fs.Console
import mtmc.os.fs.System
import mtmc.os.shell.Shell import mtmc.os.shell.Shell
import mtmc.tokenizer.MTMCScanner import mtmc.tokenizer.MTMCScanner
import mtmc.tokenizer.MTMCToken import mtmc.tokenizer.MTMCToken
class MTMCConsole(private val computer: MonTanaMiniComputer) { class MTMCConsole(private val computer: MonTanaMiniComputer) {
var mode: Mode = Mode.NON_INTERACTIVE var mode: Mode = Mode.INTERACTIVE
var sysConsole: Console? = null var sysConsole: Console? = null
// non-interactive data // non-interactive data
private val output = StringBuilder() private var output = StringBuilder()
private var shortValueSet = false private var shortValueSet = false
private var shortValue: Short = 0 private var shortValue: Short = 0
private var stringValue: String? = null 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 // TODO invert so shell is driving and console is just IO
fun start() { fun start() {
mode = Mode.INTERACTIVE mode = Mode.INTERACTIVE
sysConsole = System.console
Shell.printShellWelcome(computer) Shell.printShellWelcome(computer)
while (true) {
val cmd = sysConsole!!.readLine("mtmc > ")
computer.oS.processCommand(cmd)
}
} }
fun println(x: String) { 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 "" val text = if (index >= 0) output.substring(0, index + 1) else ""
if (index >= 0) { if (index >= 0) {
output.removeRange(0, index + 1) val updated = StringBuilder()
updated.append(output.removeRange(0, index + 1))
output = updated
} }
return text return text
@@ -122,7 +118,7 @@ class MTMCConsole(private val computer: MonTanaMiniComputer) {
} }
fun resetOutput() { fun resetOutput() {
output.removeRange(0, output.length) output.clear()
} }
enum class Mode { 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 memory: ByteArray = ByteArray(MEMORY_SIZE)
var breakpoints: ByteArray = ByteArray(MEMORY_SIZE) var breakpoints: ByteArray = ByteArray(MEMORY_SIZE)
private var status: ComputerStatus? = ComputerStatus.READY private var status: ComputerStatus? = ComputerStatus.READY
var speed: Int = 1000000 var speed: Int = 1
set(speed) { set(speed) {
field = speed field = speed
this.notifyOfExecutionUpdate() this.notifyOfExecutionUpdate()
@@ -31,7 +31,9 @@ class MonTanaMiniComputer {
var display: MTMCDisplay = MTMCDisplay(this) var display: MTMCDisplay = MTMCDisplay(this)
var clock: MTMCClock = MTMCClock(this) var clock: MTMCClock = MTMCClock(this)
var fileSystem: FileSystem = FileSystem(this) var fileSystem: FileSystem = FileSystem(this)
var rewindSteps = mutableListOf<RewindStep>()
var rewindSteps = Array(MAX_REWIND_STEPS) { RewindStep() }
var rewindIndex = -1
// listeners // listeners
private val observers = mutableListOf<MTMCObserver>() private val observers = mutableListOf<MTMCObserver>()
@@ -46,34 +48,33 @@ class MonTanaMiniComputer {
registerFile = ShortArray(Register.entries.size) registerFile = ShortArray(Register.entries.size)
memory = ByteArray(MEMORY_SIZE) memory = ByteArray(MEMORY_SIZE)
breakpoints = ByteArray(MEMORY_SIZE) breakpoints = ByteArray(MEMORY_SIZE)
rewindSteps.clear()
setRegisterValue( setRegisterValue(
Register.SP, Register.SP,
MEMORY_SIZE.toShort().toInt() MEMORY_SIZE.toShort().toInt()
) // default the stack pointer to the top of memory ) // default the stack pointer to the top of memory
rewindSteps.clear() rewindIndex = -1
observers.forEach { obj -> observers.forEach { obj ->
obj.computerReset() obj.computerReset()
} }
} }
fun load(code: ByteArray, data: ByteArray, debugInfo: DebugInfo?) { fun load(
load(code, data, Array(0) { ByteArray(0) }, debugInfo) code: ByteArray,
} data: ByteArray,
graphics: Array<ByteArray> = arrayOf(),
fun load(code: ByteArray, data: ByteArray, graphics: Array<ByteArray>, debugInfo: DebugInfo?) { debugInfo: DebugInfo? = null
) {
this.debugInfo = debugInfo this.debugInfo = debugInfo
// reset memory // reset memory
initMemory() initMemory()
val codeBoundary = code.size val codeBoundary = code.size
code.copyInto(memory, 0, 0, codeBoundary) code.copyInto(memory, 0, 0, codeBoundary)
setRegisterValue(Register.CB, codeBoundary - 1) setRegisterValue(Register.CB, codeBoundary - 1)
val dataBoundary = codeBoundary + data.size val dataBoundary = codeBoundary + data.size
code.copyInto(memory, codeBoundary, 0, data.size) data.copyInto(memory, codeBoundary, 0, data.size)
setRegisterValue(Register.DB, dataBoundary - 1) setRegisterValue(Register.DB, dataBoundary - 1)
@@ -132,10 +133,9 @@ class MonTanaMiniComputer {
} }
fun fetchAndExecute() { fun fetchAndExecute() {
currentRewindStep = RewindStep() rewindIndex = (rewindIndex + 1) % rewindSteps.size
currentRewindStep?.let { rewindSteps[rewindIndex].index = 0
rewindSteps.add(0, it)
}
fetchCurrentInstruction() fetchCurrentInstruction()
val instruction = getRegisterValue(Register.IR) val instruction = getRegisterValue(Register.IR)
if (isDoubleWordInstruction(instruction)) { if (isDoubleWordInstruction(instruction)) {
@@ -727,7 +727,11 @@ class MonTanaMiniComputer {
private fun badInstruction(instruction: Short) { private fun badInstruction(instruction: Short) {
setStatus(ComputerStatus.PERMANENT_ERROR) setStatus(ComputerStatus.PERMANENT_ERROR)
// TODO implement flags // 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() { fun fetchCurrentInstruction() {
@@ -749,7 +753,7 @@ class MonTanaMiniComputer {
val upperByte = fetchByteFromMemory(address).toShort() val upperByte = fetchByteFromMemory(address).toShort()
val lowerByte = fetchByteFromMemory(address + 1) val lowerByte = fetchByteFromMemory(address + 1)
var value = (upperByte.toInt() shl 8).toShort() 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() value = i.toShort()
return value return value
} }
@@ -785,17 +789,14 @@ class MonTanaMiniComputer {
val currentValue = memory[address] val currentValue = memory[address]
addRewindStep { memory[address] = currentValue } addRewindStep { memory[address] = currentValue }
memory[address] = value memory[address] = value
observers!!.forEach { o: MTMCObserver? -> observers.forEach { o: MTMCObserver? ->
o!!.memoryUpdated(address, value) o!!.memoryUpdated(address, value)
} }
} }
private fun addRewindStep(runnable: Runnable?) { private fun addRewindStep(runnable: Runnable?) {
if (currentRewindStep != null && rewindSteps != null) { if (currentRewindStep != null) {
currentRewindStep!!.addSubStep(runnable) currentRewindStep!!.addSubStep(runnable)
if (rewindSteps.size > MAX_REWIND_STEPS) {
rewindSteps.removeLast()
}
} }
} }
@@ -812,7 +813,7 @@ class MonTanaMiniComputer {
registerFile[register] = currentValue registerFile[register] = currentValue
} }
registerFile[register] = value.toShort() registerFile[register] = value.toShort()
observers!!.forEach { o: MTMCObserver? -> observers.forEach { o: MTMCObserver? ->
o!!.registerUpdated(register, value) o!!.registerUpdated(register, value)
} }
} }
@@ -843,7 +844,7 @@ class MonTanaMiniComputer {
breakpoints[address] = (if (active) 1.toByte() else 0.toByte()) breakpoints[address] = (if (active) 1.toByte() else 0.toByte())
} }
private fun start() { fun start() {
console.start() // start the interactive console console.start() // start the interactive console
} }
@@ -950,12 +951,15 @@ class MonTanaMiniComputer {
} }
fun rewind() { fun rewind() {
val latestRewindStep = rewindSteps.removeFirst() if (rewindIndex >= 0) {
latestRewindStep.rewind() val latestRewindStep = rewindSteps[rewindIndex]
rewindIndex--
latestRewindStep?.rewind()
}
} }
val isBackAvailable: Boolean val isBackAvailable: Boolean
get() = !rewindSteps!!.isEmpty() get() = rewindIndex >= 0
enum class ComputerStatus { enum class ComputerStatus {
READY, READY,
@@ -1011,16 +1015,8 @@ class MonTanaMiniComputer {
return true return true
} }
val isMcp = getBits(16, 8, instruction) == 5.toShort() val isMcp = getBits(16, 8, instruction) == 5.toShort()
if (isMcp) {
return true
}
return false
}
fun main(args: Array<String>) { return isMcp
val computer = MonTanaMiniComputer()
computer.speed = 1 // default to 1hz
computer.start()
} }
} }
} }

View File

@@ -3,13 +3,16 @@ package mtmc.emulator
import mtmc.util.Runnable import mtmc.util.Runnable
class RewindStep { class RewindStep {
var subSteps: MutableList<Runnable?> = ArrayList<Runnable?>() var subSteps: Array<Runnable> = Array(10) { {} }
var index = 0
fun rewind() { fun rewind() {
subSteps.reversed().forEach({ obj: Runnable? -> obj!!.invoke() }) subSteps.reversed().forEach({ obj: Runnable? -> obj!!.invoke() })
} }
fun addSubStep(subStep: Runnable?) { 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 import kotlin.random.Random
class MTOS(private val computer: MonTanaMiniComputer) { class MTOS(private val computer: MonTanaMiniComputer) {
private var timer: Long = 0 private var timer: Double = 0.0
var random: Random = Random.Default var random: Random = Random.Default
// Editor support // Editor support
@@ -51,59 +51,59 @@ class MTOS(private val computer: MonTanaMiniComputer) {
fun handleSysCall(syscallNumber: Short) { fun handleSysCall(syscallNumber: Short) {
if (syscallNumber == getValue("exit").toShort()) { if (syscallNumber == getValue("exit").toShort()) {
computer.setStatus(MonTanaMiniComputer.ComputerStatus.FINISHED) computer.setStatus(MonTanaMiniComputer.ComputerStatus.FINISHED)
} else if (syscallNumber == getValue("rint").toShort()) { // } else if (syscallNumber == getValue("rint").toShort()) {
// rint // // rint
if (!computer.console.hasShortValue()) { // if (!computer.console.hasShortValue()) {
computer.notifyOfRequestInteger() // computer.notifyOfRequestInteger()
} // }
while (!computer.console.hasShortValue() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) { // while (!computer.console.hasShortValue() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) {
try { // try {
Thread.sleep(10) // Thread.sleep(10)
} catch (e: InterruptedException) { // } catch (e: InterruptedException) {
} // }
} // }
val `val` = computer.console.readInt() // val `val` = computer.console.readInt()
computer.setRegisterValue(Register.RV, `val`.toInt()) // computer.setRegisterValue(Register.RV, `val`.toInt())
} else if (syscallNumber == getValue("wint").toShort()) { } else if (syscallNumber == getValue("wint").toShort()) {
// wint // wint
val value = computer.getRegisterValue(Register.A0) val value = computer.getRegisterValue(Register.A0)
computer.console.writeInt(value) computer.console.writeInt(value)
} else if (syscallNumber == getValue("rchr").toShort()) { // } else if (syscallNumber == getValue("rchr").toShort()) {
if (!computer.console.hasShortValue()) { // if (!computer.console.hasShortValue()) {
computer.notifyOfRequestCharacter() // computer.notifyOfRequestCharacter()
} // }
while (!computer.console.hasShortValue() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) { // while (!computer.console.hasShortValue() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) {
try { // try {
Thread.sleep(10) // Thread.sleep(10)
} catch (e: InterruptedException) { // } catch (e: InterruptedException) {
} // }
} // }
val `val` = computer.console.readChar() // val `val` = computer.console.readChar()
computer.setRegisterValue(Register.RV, `val`.code) // computer.setRegisterValue(Register.RV, `val`.code)
} else if (syscallNumber == getValue("wchr").toShort()) { } else if (syscallNumber == getValue("wchr").toShort()) {
val value = computer.getRegisterValue(Register.A0) val value = computer.getRegisterValue(Register.A0)
computer.console.print("" + Char(value.toUShort())) computer.console.print("" + Char(value.toUShort()))
} else if (syscallNumber == getValue("rstr").toShort()) { // } else if (syscallNumber == getValue("rstr").toShort()) {
// rstr // // rstr
val pointer = computer.getRegisterValue(Register.A0) // val pointer = computer.getRegisterValue(Register.A0)
val maxLen = computer.getRegisterValue(Register.A1) // val maxLen = computer.getRegisterValue(Register.A1)
if (!computer.console.hasReadString()) { // if (!computer.console.hasReadString()) {
computer.notifyOfRequestString() // computer.notifyOfRequestString()
} // }
while (!computer.console.hasReadString() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) { // while (!computer.console.hasReadString() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) {
try { // try {
Thread.sleep(10) // Thread.sleep(10)
} catch (e: InterruptedException) { // } catch (e: InterruptedException) {
} // }
} // }
val string = computer.console.readString() // val string = computer.console.readString()
val bytes = string!!.encodeToByteArray() // val bytes = string!!.encodeToByteArray()
val bytesToRead = min(bytes.size, maxLen.toInt()) // val bytesToRead = min(bytes.size, maxLen.toInt())
for (i in 0..<bytesToRead) { // for (i in 0..<bytesToRead) {
val aByte = bytes[i] // val aByte = bytes[i]
computer.writeByteToMemory(pointer + i, aByte) // computer.writeByteToMemory(pointer + i, aByte)
} // }
computer.setRegisterValue(Register.RV, bytesToRead) // computer.setRegisterValue(Register.RV, bytesToRead)
} else if (syscallNumber == getValue("wstr").toShort()) { } else if (syscallNumber == getValue("wstr").toShort()) {
// wstr // wstr
val pointer = computer.getRegisterValue(Register.A0) 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)) computer.setRegisterValue(Register.RV, random.nextInt(low.toInt(), high + 1))
} else if (syscallNumber == getValue("sleep").toShort()) { // } else if (syscallNumber == getValue("sleep").toShort()) {
// sleep // // sleep
val millis = computer.getRegisterValue(Register.A0) // val millis = computer.getRegisterValue(Register.A0)
try { // try {
if (millis > 0) Thread.sleep(millis.toLong()) // if (millis > 0) Thread.sleep(millis.toLong())
} catch (e: InterruptedException) { // } catch (e: InterruptedException) {
throw RuntimeException(e) // throw RuntimeException(e)
} // }
} else if (syscallNumber == getValue("fbreset").toShort()) { } else if (syscallNumber == getValue("fbreset").toShort()) {
// fbreset // fbreset
computer.display.reset() computer.display.reset()
@@ -240,7 +240,38 @@ class MTOS(private val computer: MonTanaMiniComputer) {
try { try {
// special handling for game-of-life cell files // special handling for game-of-life cell files
if ("cells" == fileType) { 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> = val lines: List<String> =
str.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() str.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
.filter { s: String? -> !s!!.startsWith("!") } .filter { s: String? -> !s!!.startsWith("!") }
@@ -309,7 +340,7 @@ class MTOS(private val computer: MonTanaMiniComputer) {
computer.setRegisterValue( computer.setRegisterValue(
Register.RV, Register.RV,
max(0, this.timer - currentTimeMillis()).toInt() max(0.0, this.timer - currentTimeMillis()).toInt()
) )
} else if (syscallNumber == getValue("drawimg").toShort()) { } else if (syscallNumber == getValue("drawimg").toShort()) {
val image = computer.getRegisterValue(Register.A0) val image = computer.getRegisterValue(Register.A0)

View File

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

View File

@@ -12,4 +12,14 @@ class Path {
fun resolve(filename: String): Path { fun resolve(filename: String): Path {
TODO("Not yet implemented") 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) { fun execCommand(command: String, computer: MonTanaMiniComputer) {
println("Exec: $command")
val tokens = MTMCTokenizer(command, "#") val tokens = MTMCTokenizer(command, "#")
try { try {
val identifier = tokens.matchAndConsume(MTMCToken.TokenType.IDENTIFIER) val identifier = tokens.matchAndConsume(MTMCToken.TokenType.IDENTIFIER)
@@ -96,17 +97,18 @@ object Shell {
return return
} }
} else { } else {
cmd = identifier.stringValue() cmd = identifier.stringValue
} }
if (isCommand(cmd)) { if (isCommand(cmd)) {
COMMANDS[cmd.lowercase()]!!.exec(tokens, computer) COMMANDS[cmd.lowercase()]!!.exec(tokens, computer)
} else { } else {
println("Executing: $command")
tokens.reset() tokens.reset()
val asm = mutableListOf<MTMCToken>() val asm = mutableListOf<MTMCToken>()
asm.addAll(tokens.tokens) asm.addAll(tokens.tokens)
val updatedAsm = Assembler.transformSyntheticInstructions(asm) val updatedAsm = Assembler.transformSyntheticInstructions(asm)
val firstToken = updatedAsm.peekFirst() 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)) { if (!updatedAsm.isEmpty() && isInstruction(firstTokenStr)) {
val assembler = Assembler() val assembler = Assembler()
val result = assembler.assemble(command) val result = assembler.assemble(command)

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,11 @@
package mtmc.util 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 package mtmc
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.html.div import kotlinx.browser.window
import nl.astraeus.komp.HtmlBuilder import mtmc.emulator.MonTanaMiniComputer
import mtmc.util.currentTimeMillis
import mtmc.view.DisplayView
import mtmc.view.MTMCView
import nl.astraeus.komp.Komponent import nl.astraeus.komp.Komponent
class HelloKomponent : Komponent() { val computer = MonTanaMiniComputer()
override fun HtmlBuilder.render() { val mainView = MTMCView(computer)
div { val display = DisplayView(computer)
+ "Hello, world!"
}
}
}
fun main() { 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 package mtmc.util
import kotlinx.browser.window
import kotlin.js.Date 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.ContentEncodingRepository
import io.undertow.server.handlers.encoding.EncodingHandler import io.undertow.server.handlers.encoding.EncodingHandler
import io.undertow.server.handlers.encoding.GzipEncodingProvider import io.undertow.server.handlers.encoding.GzipEncodingProvider
import nl.astraeus.logger.Logger
import mtmc.db.Database import mtmc.db.Database
import mtmc.web.RequestHandler import mtmc.web.RequestHandler
import nl.astraeus.logger.Logger
val log = Logger() val log = Logger()
@@ -58,7 +58,6 @@ fun main() {
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000) .setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000)
.build() .build()
println("Starting undertow server at port 6007...") println("Starting undertow server at port $SERVER_PORT...")
server?.start() 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 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.body
import kotlinx.html.head import kotlinx.html.head
import kotlinx.html.html import kotlinx.html.html
import kotlinx.html.link
import kotlinx.html.meta import kotlinx.html.meta
import kotlinx.html.script import kotlinx.html.script
import kotlinx.html.stream.appendHTML import kotlinx.html.stream.appendHTML
@@ -18,7 +19,7 @@ fun generateIndex(patch: String?): String {
result.appendHTML(true).html { result.appendHTML(true).html {
head { head {
title(pageTitle) title(pageTitle)
//link("/css/all.min.css", "stylesheet") link("/mtmc.css", "stylesheet")
} }
body { body {
script(src = "/$repoName.js") {} 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.PathHandler
import io.undertow.server.handlers.resource.PathResourceManager import io.undertow.server.handlers.resource.PathResourceManager
import io.undertow.server.handlers.resource.ResourceHandler import io.undertow.server.handlers.resource.ResourceHandler
import io.undertow.util.Headers
import mtmc.itemUrl import mtmc.itemUrl
import java.nio.file.Paths import java.nio.file.Paths
import kotlin.text.startsWith
object IndexHandler : HttpHandler { object IndexHandler : HttpHandler {
override fun handleRequest(exchange: HttpServerExchange) { override fun handleRequest(exchange: HttpServerExchange) {
exchange.responseHeaders.put(Headers.CONTENT_TYPE, "text/html")
if (exchange.requestPath.startsWith("/$itemUrl/")) { if (exchange.requestPath.startsWith("/$itemUrl/")) {
exchange.responseSender.send(generateIndex(null)) exchange.responseSender.send(generateIndex(null))
} else { } else {