Compare commits

...

5 Commits

24 changed files with 2286 additions and 84 deletions

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
@@ -72,4 +78,4 @@ kotlin {
} }
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

@@ -4,6 +4,8 @@ expect fun createBufferedImage(width: Int, height: Int): BufferedImage
expect fun createCanvasImage(width: Int, height: Int): BufferedImage expect fun createCanvasImage(width: Int, height: Int): BufferedImage
expect fun createGLImage(width: Int, height: Int): BufferedImage
interface BufferedImage { interface BufferedImage {
val width: Int val width: Int
val height: Int val height: Int

View File

@@ -3,6 +3,8 @@ 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.requestAnimationFrame
import mtmc.util.setTimeout
import mtmc.util.time
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -27,21 +29,22 @@ class MTMCClock(
var frame = 0 var frame = 0
fun run() { fun run() {
requestAnimationFrame { handleFrame(it) } requestAnimationFrame { handleFrame() }
} }
fun handleFrame(time: Double) { fun handleFrame() {
// figure out how many instructions to execute this 'time' duration // figure out how many instructions to execute this 'time' duration
// maximize time so we don't hang if the emulator is too slow // maximize time so we don't hang if the emulator is too slow
val time = time()
if (lastFrame == 0.0) { if (lastFrame == 0.0) {
lastFrame = time lastFrame = time
requestAnimationFrame { handleFrame(it) } requestAnimationFrame { handleFrame() }
return return
} }
val delta = time - lastFrame val delta = time - lastFrame
lastFrame = time lastFrame = time
val actualTime = min(delta / 1000.0, 0.05) val timeToProcess = min(delta, 16.0)
// assume 1Hz = 1 instruction/second // assume 1Hz = 1 instruction/second
if (computer.getStatus() == ComputerStatus.EXECUTING) { if (computer.getStatus() == ComputerStatus.EXECUTING) {
@@ -50,22 +53,28 @@ class MTMCClock(
speed = 1L speed = 1L
} }
instructionsToRun += actualTime * speed instructionsToRun += timeToProcess * speed / 1000.0
val pulse: Long = instructionsToRun.toLong() val pulse: Long = instructionsToRun.toLong() + 1
instructionsToRun -= pulse instructionsToRun -= pulse
val time = currentTimeMillis() val start = time()
val ir = computer.pulse(pulse) val ir = computer.pulse(pulse)
instructions += ir instructions += ir
val duration = (time() - start)
val actual = ir / (delta / 1000.0)
if (frame % 100 == 0) { if (frame % 100 == 0) {
println("Instructions ran: $ir (delta = $delta, actualTime = $actualTime, speed = $speed, duration = ${currentTimeMillis() - time})") println("Instructions ran: $ir (delta = ${delta.toFloat()}, timeToProcess = ${timeToProcess.toFloat()}, speed = $speed (actual=${actual.toLong()}), duration = $duration)")
} }
virtual += instructions virtual += instructions
ips = (instructions / actualTime).toLong() ips = (instructions / timeToProcess).toLong()
frame++ frame++
requestAnimationFrame { handleFrame(it) } if (duration > timeToProcess) {
setTimeout({ handleFrame() })
} 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

@@ -10,7 +10,7 @@ class MTMCConsole(private val computer: MonTanaMiniComputer) {
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
@@ -83,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
@@ -116,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 {

View File

@@ -3,7 +3,7 @@ package mtmc.emulator
import kotlin.math.min import kotlin.math.min
class MTMCDisplay(private val computer: MonTanaMiniComputer) { class MTMCDisplay(private val computer: MonTanaMiniComputer) {
val buffer: BufferedImage = createCanvasImage(COLS, ROWS) val buffer: BufferedImage = createGLImage(COLS, ROWS)
private var currentColor: DisplayColor? = null private var currentColor: DisplayColor? = null
private var graphics: Array<BufferedImage> = arrayOf() private var graphics: Array<BufferedImage> = arrayOf()
private var byteArray: ByteArray = ByteArray(0) private var byteArray: ByteArray = ByteArray(0)
@@ -18,13 +18,8 @@ class MTMCDisplay(private val computer: MonTanaMiniComputer) {
LIGHT(87, 124, 68), LIGHT(87, 124, 68),
LIGHTEST(127, 134, 15); LIGHTEST(127, 134, 15);
val intVal: Int val intVal: Int = 0xFF shl 24 or (r shl 16) or (g shl 8) or b
val javaColor: Color val javaColor: Color = Color(r, g, b)
init {
this.intVal = 0xFF shl 24 or (r shl 16) or (g shl 8) or b
javaColor = Color(r, g, b)
}
fun distance(r: Int, g: Int, b: Int): Int { fun distance(r: Int, g: Int, b: Int): Int {
val dr = this.r - r val dr = this.r - r
@@ -56,15 +51,20 @@ class MTMCDisplay(private val computer: MonTanaMiniComputer) {
} }
private fun loadSplashScreen() { private fun loadSplashScreen() {
/* try { currentColor = DisplayColor.DARK
val bytes: ByteArray = Base64.getDecoder().decode(SPLASH_SCREEN) var currentColorCount = 0
val bais = ByteArrayInputStream(bytes) var currentColor = 0
var img: BufferedImage? = null var colorIndex = 0
img = ImageIO.read(bais) for (col in 0..<COLS) {
loadScaledImage(img) for (row in 0..<ROWS) {
} catch (e: IOException) { if (currentColorCount == 0) {
e.printStackTrace() currentColorCount = SPLASH_SCREEN_COLORS[colorIndex++]
}*/ currentColor = SPLASH_SCREEN_COLORS[colorIndex++]
}
setPixel(col, row, DisplayColor.entries[currentColor])
currentColorCount--
}
}
} }
private fun loadImage(data: ByteArray): BufferedImage? { private fun loadImage(data: ByteArray): BufferedImage? {
@@ -107,7 +107,11 @@ class MTMCDisplay(private val computer: MonTanaMiniComputer) {
} }
fun setPixel(col: Int, row: Int, color: DisplayColor) { fun setPixel(col: Int, row: Int, color: DisplayColor) {
buffer.setRGB(col, row, color.intVal) try {
buffer.setRGB(col, row, color.intVal)
} catch (e: Throwable) {
throw e
}
} }
fun getPixel(col: Int, row: Int): Short { fun getPixel(col: Int, row: Int): Short {
@@ -173,7 +177,7 @@ class MTMCDisplay(private val computer: MonTanaMiniComputer) {
//computer.notifyOfDisplayUpdate() //computer.notifyOfDisplayUpdate()
} }
fun toPng(): ByteArray? { fun toPng(): ByteArray {
return byteArray return byteArray
} }

View File

@@ -32,7 +32,7 @@ class MonTanaMiniComputer {
var clock: MTMCClock = MTMCClock(this) var clock: MTMCClock = MTMCClock(this)
var fileSystem: FileSystem = FileSystem(this) var fileSystem: FileSystem = FileSystem(this)
var rewindSteps = Array<RewindStep?>(MAX_REWIND_STEPS) { null } var rewindSteps = Array(MAX_REWIND_STEPS) { RewindStep() }
var rewindIndex = -1 var rewindIndex = -1
// listeners // listeners
@@ -48,7 +48,6 @@ 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)
rewindIndex = -1
setRegisterValue( setRegisterValue(
Register.SP, Register.SP,
MEMORY_SIZE.toShort().toInt() MEMORY_SIZE.toShort().toInt()
@@ -134,11 +133,9 @@ class MonTanaMiniComputer {
} }
fun fetchAndExecute() { fun fetchAndExecute() {
currentRewindStep = RewindStep() rewindIndex = (rewindIndex + 1) % rewindSteps.size
currentRewindStep?.let { rewindSteps[rewindIndex].index = 0
rewindIndex = (rewindIndex + 1) % rewindSteps.size
rewindSteps.set(rewindIndex, it)
}
fetchCurrentInstruction() fetchCurrentInstruction()
val instruction = getRegisterValue(Register.IR) val instruction = getRegisterValue(Register.IR)
if (isDoubleWordInstruction(instruction)) { if (isDoubleWordInstruction(instruction)) {
@@ -792,7 +789,7 @@ 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)
} }
} }
@@ -816,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)
} }
} }

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?> = mutableListOf() 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
@@ -340,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

@@ -1,7 +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 requestAnimationFrame(action: (Double) -> Unit)
expect fun setTimeout(action: () -> Unit)
expect fun immediateTimeout(action: (Double) -> Unit): Int expect fun immediateTimeout(action: (Double) -> Unit): Int

View File

@@ -1,7 +1,9 @@
package mtmc package mtmc
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.browser.window
import mtmc.emulator.MonTanaMiniComputer import mtmc.emulator.MonTanaMiniComputer
import mtmc.util.currentTimeMillis
import mtmc.view.DisplayView import mtmc.view.DisplayView
import mtmc.view.MTMCView import mtmc.view.MTMCView
import nl.astraeus.komp.Komponent import nl.astraeus.komp.Komponent
@@ -11,12 +13,31 @@ val mainView = MTMCView(computer)
val display = DisplayView(computer) val display = DisplayView(computer)
fun main() { fun main() {
computer.speed = 2000000 // default to 1hz computer.speed = 1000000
computer.load(lifeCode, lifeData) computer.load(lifeCode, lifeData)
//computer.load(snakeCode, snakeData)
Komponent.create(document.body!!, mainView) Komponent.create(document.body!!, mainView)
computer.start() computer.start()
mainView.requestUpdate() 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

@@ -1,6 +1,7 @@
package mtmc.emulator package mtmc.emulator
import kotlinx.browser.document import kotlinx.browser.document
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get import org.khronos.webgl.get
import org.khronos.webgl.set import org.khronos.webgl.set
import org.w3c.dom.CanvasRenderingContext2D import org.w3c.dom.CanvasRenderingContext2D
@@ -67,9 +68,49 @@ class BufferedImageData(
} }
} }
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 canvas = document.createElement("canvas") as HTMLCanvasElement
val ctx = canvas.getContext("2d") as CanvasRenderingContext2D val ctx = canvas.getContext("2d") as CanvasRenderingContext2D
actual fun createCanvasImage(width: Int, height: Int): BufferedImage { actual fun createCanvasImage(width: Int, height: Int): BufferedImage {
return BufferedImageData(ctx.createImageData(width.toDouble(), height.toDouble())); return BufferedImageData(ctx.createImageData(width.toDouble(), height.toDouble()));
} }
actual fun createGLImage(width: Int, height: Int): BufferedImage {
return BufferedImageDataWebGl(width, height)
}

View File

@@ -1,24 +1,19 @@
package mtmc.util package mtmc.util
import kotlinx.browser.window import kotlinx.browser.window
import mtmc.display
import mtmc.mainView
import kotlin.js.Date import kotlin.js.Date
var lastMemoryUpdate = currentTimeMillis() actual fun currentTimeMillis(): Double = Date().getTime()
actual fun currentTimeMillis(): Long = Date().getTime().toLong()
actual fun requestAnimationFrame(action: (Double) -> Unit) { actual fun requestAnimationFrame(action: (Double) -> Unit) {
window.requestAnimationFrame { window.requestAnimationFrame {
action(it) action(it)
display.requestUpdate()
if (currentTimeMillis() - lastMemoryUpdate > 100) {
mainView.registerView.requestUpdate()
mainView.memoryView.requestUpdate()
lastMemoryUpdate = currentTimeMillis()
}
} }
} }
actual fun immediateTimeout(action: (Double) -> Unit): Int = window.setTimeout(action, 0) 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

@@ -19,10 +19,14 @@ import org.w3c.dom.events.KeyboardEvent
class ConsoleView( class ConsoleView(
val computer: MonTanaMiniComputer val computer: MonTanaMiniComputer
) : Komponent() { ) : Komponent() {
val history: MutableList<String> = mutableListOf()
var input: String = "" var input: String = ""
var output = StringBuilder()
private var inputElement: HTMLInputElement? = null private var inputElement: HTMLInputElement? = null
init {
output.append(computer.console.consumeLines())
}
override fun HtmlBuilder.render() { override fun HtmlBuilder.render() {
div("console") { div("console") {
onClickFunction = { onClickFunction = {
@@ -30,7 +34,7 @@ class ConsoleView(
} }
div("console-history") { div("console-history") {
div { div {
+computer.console.getOutput() +output.toString()
} }
} }
div("console-input") { div("console-input") {
@@ -41,6 +45,7 @@ class ConsoleView(
value = input value = input
autoFocus = true autoFocus = true
inputElement = currentElement() as? HTMLInputElement inputElement = currentElement() as? HTMLInputElement
currentElement().scrollIntoView()
window.setTimeout({ window.setTimeout({
inputElement?.focus() inputElement?.focus()
}, 0) }, 0)
@@ -61,11 +66,17 @@ class ConsoleView(
} }
private fun handleCommand() { private fun handleCommand() {
//history.add(input) computer.console.print("mtmc$ ")
computer.console.println(input)
Shell.execCommand(input, computer) Shell.execCommand(input, computer)
input = "" input = ""
output.append(computer.console.consumeLines())
if (output.length > 1000 && output.contains("\n")) {
output = output.deleteRange(0, output.indexOf("\n"))
}
mainView.registerView.requestUpdate() mainView.registerView.requestUpdate()
mainView.memoryView.requestUpdate() mainView.memoryView.requestUpdate()
display.requestUpdate() display.requestUpdate()

View File

@@ -1,18 +1,156 @@
package mtmc.view package mtmc.view
import kotlinx.html.InputType
import kotlinx.html.button
import kotlinx.html.div 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.emulator.MonTanaMiniComputer
import mtmc.mainView
import mtmc.updateState
import nl.astraeus.komp.HtmlBuilder import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent import nl.astraeus.komp.Komponent
import org.w3c.dom.HTMLSelectElement
import kotlin.text.Typography.nbsp
class ControlView( class ControlView(
val computer: MonTanaMiniComputer val computer: MonTanaMiniComputer
) : Komponent() { ) : Komponent() {
override fun HtmlBuilder.render() { override fun HtmlBuilder.render() {
div { div("control-panel") {
+"Controls view" 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

@@ -4,13 +4,43 @@ import kotlinx.html.canvas
import kotlinx.html.div import kotlinx.html.div
import mtmc.display import mtmc.display
import mtmc.emulator.BufferedImageData import mtmc.emulator.BufferedImageData
import mtmc.emulator.BufferedImageDataWebGl
import mtmc.emulator.MonTanaMiniComputer import mtmc.emulator.MonTanaMiniComputer
import nl.astraeus.komp.HtmlBuilder import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent import nl.astraeus.komp.Komponent
import nl.astraeus.komp.currentElement import nl.astraeus.komp.currentElement
import org.w3c.dom.CanvasRenderingContext2D 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 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( class DiplayControlView(
val computer: MonTanaMiniComputer val computer: MonTanaMiniComputer
) : Komponent() { ) : Komponent() {
@@ -25,7 +55,10 @@ class DiplayControlView(
class DisplayView( class DisplayView(
val computer: MonTanaMiniComputer val computer: MonTanaMiniComputer
) : Komponent() { ) : Komponent() {
var ctx: CanvasRenderingContext2D? = null var ctx: WebGLRenderingContext? = null
var program: WebGLProgram? = null
var texture: WebGLTexture? = null
var buffer: WebGLBuffer? = null
override fun HtmlBuilder.render() { override fun HtmlBuilder.render() {
canvas("display-canvas") { canvas("display-canvas") {
@@ -34,18 +67,107 @@ class DisplayView(
val cv = currentElement() as? HTMLCanvasElement val cv = currentElement() as? HTMLCanvasElement
ctx = cv?.getContext("2d")?.unsafeCast<CanvasRenderingContext2D>() ctx = cv?.getContext("webgl")?.unsafeCast<WebGLRenderingContext>()
ctx?.fillStyle = "#400040" if (program == null) {
ctx?.fillRect(0.0, 0.0, 160.0, 144.0) 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() { override fun renderUpdate() {
// move data to canvas // move data to canvas
val buffer = computer.display.buffer val buffer = computer.display.buffer
if (buffer is BufferedImageData) { if (buffer is BufferedImageData) {
ctx?.putImageData(buffer.imageData, 0.0, 0.0) //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

@@ -89,7 +89,6 @@ class MemoryView(
else -> +"?" else -> +"?"
} }
} }
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import kotlinx.html.classes
import kotlinx.html.div import kotlinx.html.div
import kotlinx.html.hr import kotlinx.html.hr
import kotlinx.html.id import kotlinx.html.id
import kotlinx.html.span
import kotlinx.html.table import kotlinx.html.table
import kotlinx.html.td import kotlinx.html.td
import kotlinx.html.tr import kotlinx.html.tr
@@ -36,9 +37,34 @@ class RegisterView(
showRegister(16) showRegister(16)
showRegister(17) showRegister(17)
tr { tr {
td { td("flags") {
colSpan = "3" colSpan = "3"
+"Flags" 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"
}
}
}
} }
} }
} }

View File

@@ -1,3 +1,12 @@
:root {
--pdp-blue: #243571;
--pdp-light-blue: #3286ce;
--pdp-beige: #fdfddc;
--pdp-white: #f1f1f6;
--pdp-off-white: #F0EBCD;
--filetree-gray: #666;
}
html, body { html, body {
padding: 0; padding: 0;
margin: 0; margin: 0;
@@ -14,7 +23,7 @@ body {
grid-template-rows: 1fr; grid-template-rows: 1fr;
overflow-y: auto; overflow-y: auto;
gap: 5px; gap: 5px;
background-color: #ccc; background-color: var(--pdp-white);
} }
.left-column { .left-column {
@@ -52,6 +61,53 @@ body {
text-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 */ /* registers */
table.register-table { table.register-table {
@@ -72,6 +128,20 @@ table.register-table tr td.register-lights {
padding-left: 20px; 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 { .blinken {
display: inline-block; display: inline-block;
margin-right: 2px; margin-right: 2px;
@@ -158,10 +228,11 @@ table.register-table tr td.register-lights {
font-family: monospace; font-family: monospace;
font-weight: bold; font-weight: bold;
padding: 5px; padding: 5px;
overflow: auto;
} }
.console-prompt { .console-prompt {
margin-right: 10px; margin-right: 6px;
} }
input.console-input { input.console-input {
@@ -169,6 +240,8 @@ input.console-input {
background-color: #35291c; background-color: #35291c;
border: none; border: none;
outline: none; outline: none;
font-weight: bold;
font-family: monospace;
} }
.small-button { .small-button {

View File

@@ -6,4 +6,8 @@ actual fun createBufferedImage(width: Int, height: Int): BufferedImage {
actual fun createCanvasImage(width: Int, height: Int): BufferedImage { actual fun createCanvasImage(width: Int, height: Int): BufferedImage {
TODO("Not yet implemented") TODO("Not yet implemented")
}
actual fun createGLImage(width: Int, height: Int): BufferedImage {
TODO("Not yet implemented")
} }

View File

@@ -1,8 +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) { actual fun requestAnimationFrame(action: (Double) -> Unit) {
error("requestAnimationFrame is not supported on JVM") error("requestAnimationFrame is not supported on JVM")
} }
actual fun immediateTimeout(action: (Double) -> Unit): Int = 0 actual fun immediateTimeout(action: (Double) -> Unit): Int = 0
actual fun time(): Double = System.nanoTime() * 1000.0
actual fun setTimeout(action: () -> Unit) {}

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 {