generated from rnentjes/kotlin-server-web-undertow
Compare commits
13 Commits
c7552c2a95
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4dcfe3a6ff | |||
| f14f316e38 | |||
| 11b069ddc5 | |||
| d7e331728f | |||
| 8457d3a854 | |||
| 37691dc7fa | |||
| f169dce339 | |||
| 6acf781324 | |||
| 40baff5cb0 | |||
| 11da7fd588 | |||
| 4b17ce3cf5 | |||
| d5314ce046 | |||
| 9f295b2fb9 |
7
.idea/runConfigurations/MTMC_debug.xml
generated
Normal file
7
.idea/runConfigurations/MTMC_debug.xml
generated
Normal 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
10
.idea/runConfigurations/MainKt.xml
generated
Normal 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
13
LICENSE.txt
Normal 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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -34,4 +34,4 @@ class Data(labels: MutableList<MTMCToken>, lineNumber: Int) : ASMElement(labels,
|
||||
override fun addError(err: String) {
|
||||
addError(labels.last(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
20
src/commonMain/kotlin/mtmc/emulator/BufferedImage.kt
Normal file
20
src/commonMain/kotlin/mtmc/emulator/BufferedImage.kt
Normal 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
|
||||
)
|
||||
8
src/commonMain/kotlin/mtmc/emulator/Color.kt
Normal file
8
src/commonMain/kotlin/mtmc/emulator/Color.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package mtmc.emulator
|
||||
|
||||
class Color(
|
||||
val r: Int,
|
||||
val g: Int,
|
||||
val b: Int,
|
||||
) {
|
||||
}
|
||||
@@ -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 + ")")
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1727
src/commonMain/kotlin/mtmc/emulator/SplashData.kt
Normal file
1727
src/commonMain/kotlin/mtmc/emulator/SplashData.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@ data class MTMCToken(
|
||||
return stringValue
|
||||
}
|
||||
|
||||
fun stringValue(): String {
|
||||
return stringValue
|
||||
}
|
||||
|
||||
fun charValue(): Char {
|
||||
return stringValue.get(0)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
4817
src/jsMain/kotlin/mtmc/Code.kt
Normal file
4817
src/jsMain/kotlin/mtmc/Code.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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() }
|
||||
}
|
||||
|
||||
116
src/jsMain/kotlin/mtmc/emulator/BufferedImage.js.kt
Normal file
116
src/jsMain/kotlin/mtmc/emulator/BufferedImage.js.kt
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
86
src/jsMain/kotlin/mtmc/view/ConsoleView.kt
Normal file
86
src/jsMain/kotlin/mtmc/view/ConsoleView.kt
Normal 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()
|
||||
}
|
||||
|
||||
}
|
||||
157
src/jsMain/kotlin/mtmc/view/ControlView.kt
Normal file
157
src/jsMain/kotlin/mtmc/view/ControlView.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
173
src/jsMain/kotlin/mtmc/view/DisplayView.kt
Normal file
173
src/jsMain/kotlin/mtmc/view/DisplayView.kt
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/jsMain/kotlin/mtmc/view/MTMCView.kt
Normal file
34
src/jsMain/kotlin/mtmc/view/MTMCView.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
147
src/jsMain/kotlin/mtmc/view/MemoryView.kt
Normal file
147
src/jsMain/kotlin/mtmc/view/MemoryView.kt
Normal 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()
|
||||
}
|
||||
|
||||
}
|
||||
117
src/jsMain/kotlin/mtmc/view/RegisterView.kt
Normal file
117
src/jsMain/kotlin/mtmc/view/RegisterView.kt
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
256
src/jsMain/resources/mtmc.css
Normal file
256
src/jsMain/resources/mtmc.css
Normal 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;
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
|
||||
13
src/jvmMain/kotlin/mtmc/emulator/BufferedImage.jvm.kt
Normal file
13
src/jvmMain/kotlin/mtmc/emulator/BufferedImage.jvm.kt
Normal 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")
|
||||
}
|
||||
@@ -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) {}
|
||||
@@ -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") {}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user