Refactor BufferedImage logic with WebGL support, update DisplayView for GPU-based rendering, transition timing functions to Double, optimize MTMCClock frame logic, and add dynamic control for update state in ControlView.

This commit is contained in:
2025-08-23 15:51:47 +02:00
parent 11b069ddc5
commit f14f316e38
12 changed files with 208 additions and 17 deletions

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

@@ -41,7 +41,7 @@ class MTMCClock(
val delta = time - lastFrame val delta = time - lastFrame
lastFrame = time lastFrame = time
val actualTime = min(delta / 1000.0, 0.05) val actualTime = min(delta / 1000.0, 0.0166)
// assume 1Hz = 1 instruction/second // assume 1Hz = 1 instruction/second
if (computer.getStatus() == ComputerStatus.EXECUTING) { if (computer.getStatus() == ComputerStatus.EXECUTING) {
@@ -58,7 +58,8 @@ class MTMCClock(
val ir = computer.pulse(pulse) val ir = computer.pulse(pulse)
instructions += ir instructions += ir
if (frame % 100 == 0) { if (frame % 100 == 0) {
println("Instructions ran: $ir (delta = $delta, actualTime = $actualTime, speed = $speed, duration = ${currentTimeMillis() - time})") val duration = currentTimeMillis() - time
println("Instructions ran: $ir (delta = ${delta.toFloat()}, actualTime = ${actualTime.toFloat()}, speed = $speed (actual=${(ir / delta * 1000).toLong()}), duration = $duration)")
} }
virtual += instructions virtual += instructions

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)

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,6 +1,6 @@
package mtmc.util package mtmc.util
expect fun currentTimeMillis(): Long expect fun currentTimeMillis(): Double
expect fun requestAnimationFrame(action: (Double) -> Unit) expect fun requestAnimationFrame(action: (Double) -> Unit)

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

@@ -6,16 +6,19 @@ import mtmc.mainView
import kotlin.js.Date import kotlin.js.Date
var lastMemoryUpdate = currentTimeMillis() var lastMemoryUpdate = currentTimeMillis()
var updateState = true
actual fun currentTimeMillis(): Long = Date().getTime().toLong() actual fun currentTimeMillis(): Double = Date().getTime()
actual fun requestAnimationFrame(action: (Double) -> Unit) { actual fun requestAnimationFrame(action: (Double) -> Unit) {
window.requestAnimationFrame { window.requestAnimationFrame {
action(it) action(it)
display.requestUpdate() display.requestUpdate()
if (currentTimeMillis() - lastMemoryUpdate > 125) { if (currentTimeMillis() - lastMemoryUpdate > 125) {
mainView.registerView.requestUpdate() if (updateState) {
mainView.memoryView.requestUpdate() mainView.registerView.requestUpdate()
mainView.memoryView.requestUpdate()
}
lastMemoryUpdate = currentTimeMillis() lastMemoryUpdate = currentTimeMillis()
} }
} }

View File

@@ -1,8 +1,10 @@
package mtmc.view package mtmc.view
import kotlinx.html.InputType
import kotlinx.html.button import kotlinx.html.button
import kotlinx.html.div import kotlinx.html.div
import kotlinx.html.i import kotlinx.html.i
import kotlinx.html.input
import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import kotlinx.html.label import kotlinx.html.label
@@ -12,6 +14,7 @@ import kotlinx.html.span
import mtmc.display import mtmc.display
import mtmc.emulator.MonTanaMiniComputer import mtmc.emulator.MonTanaMiniComputer
import mtmc.mainView import mtmc.mainView
import mtmc.util.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 org.w3c.dom.HTMLSelectElement
@@ -42,6 +45,18 @@ class ControlView(
+"MonTana Mini-Computer" +"MonTana Mini-Computer"
} }
div("control-buttons") { div("control-buttons") {
label {
+"Update:"
htmlFor = "update-state"
input {
type = InputType.checkBox
checked = updateState
onClickFunction = {
updateState = !updateState
requestUpdate()
}
}
}
label { label {
select { select {
name = "speed" name = "speed"
@@ -75,6 +90,10 @@ class ControlView(
value = "5000000" value = "5000000"
+"5 Mhz" +"5 Mhz"
} }
option {
value = "10000000"
+"10 Mhz"
}
onChangeFunction = { onChangeFunction = {
val target = it.target as? HTMLSelectElement val target = it.target as? HTMLSelectElement

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

@@ -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,6 +1,6 @@
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")
} }