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 createGLImage(width: Int, height: Int): BufferedImage
interface BufferedImage {
val width: Int
val height: Int

View File

@@ -41,7 +41,7 @@ class MTMCClock(
val delta = time - lastFrame
lastFrame = time
val actualTime = min(delta / 1000.0, 0.05)
val actualTime = min(delta / 1000.0, 0.0166)
// assume 1Hz = 1 instruction/second
if (computer.getStatus() == ComputerStatus.EXECUTING) {
@@ -58,7 +58,8 @@ class MTMCClock(
val ir = computer.pulse(pulse)
instructions += ir
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

View File

@@ -3,7 +3,7 @@ package mtmc.emulator
import kotlin.math.min
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 graphics: Array<BufferedImage> = arrayOf()
private var byteArray: ByteArray = ByteArray(0)

View File

@@ -12,7 +12,7 @@ import kotlin.math.min
import kotlin.random.Random
class MTOS(private val computer: MonTanaMiniComputer) {
private var timer: Long = 0
private var timer: Double = 0.0
var random: Random = Random.Default
// Editor support
@@ -340,7 +340,7 @@ class MTOS(private val computer: MonTanaMiniComputer) {
computer.setRegisterValue(
Register.RV,
max(0, this.timer - currentTimeMillis()).toInt()
max(0.0, this.timer - currentTimeMillis()).toInt()
)
} else if (syscallNumber == getValue("drawimg").toShort()) {
val image = computer.getRegisterValue(Register.A0)

View File

@@ -1,6 +1,6 @@
package mtmc.util
expect fun currentTimeMillis(): Long
expect fun currentTimeMillis(): Double
expect fun requestAnimationFrame(action: (Double) -> Unit)

View File

@@ -1,6 +1,7 @@
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
@@ -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 ctx = canvas.getContext("2d") as CanvasRenderingContext2D
actual fun createCanvasImage(width: Int, height: Int): BufferedImage {
return BufferedImageData(ctx.createImageData(width.toDouble(), height.toDouble()));
}
actual fun createGLImage(width: Int, height: Int): BufferedImage {
return BufferedImageDataWebGl(width, height)
}

View File

@@ -6,16 +6,19 @@ import mtmc.mainView
import kotlin.js.Date
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) {
window.requestAnimationFrame {
action(it)
display.requestUpdate()
if (currentTimeMillis() - lastMemoryUpdate > 125) {
mainView.registerView.requestUpdate()
mainView.memoryView.requestUpdate()
if (updateState) {
mainView.registerView.requestUpdate()
mainView.memoryView.requestUpdate()
}
lastMemoryUpdate = currentTimeMillis()
}
}

View File

@@ -1,8 +1,10 @@
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
@@ -12,6 +14,7 @@ import kotlinx.html.span
import mtmc.display
import mtmc.emulator.MonTanaMiniComputer
import mtmc.mainView
import mtmc.util.updateState
import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent
import org.w3c.dom.HTMLSelectElement
@@ -42,6 +45,18 @@ class ControlView(
+"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"
@@ -75,6 +90,10 @@ class ControlView(
value = "5000000"
+"5 Mhz"
}
option {
value = "10000000"
+"10 Mhz"
}
onChangeFunction = {
val target = it.target as? HTMLSelectElement

View File

@@ -4,13 +4,43 @@ 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.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
// 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() {
@@ -25,7 +55,10 @@ class DiplayControlView(
class DisplayView(
val computer: MonTanaMiniComputer
) : 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() {
canvas("display-canvas") {
@@ -34,18 +67,107 @@ class DisplayView(
val cv = currentElement() as? HTMLCanvasElement
ctx = cv?.getContext("2d")?.unsafeCast<CanvasRenderingContext2D>()
ctx = cv?.getContext("webgl")?.unsafeCast<WebGLRenderingContext>()
ctx?.fillStyle = "#400040"
ctx?.fillRect(0.0, 0.0, 160.0, 144.0)
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)
//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 -> +"?"
}
}
}
}
}

View File

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

View File

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