diff --git a/src/commonMain/kotlin/mtmc/emulator/BufferedImage.kt b/src/commonMain/kotlin/mtmc/emulator/BufferedImage.kt index cc3de2e..e26c34e 100644 --- a/src/commonMain/kotlin/mtmc/emulator/BufferedImage.kt +++ b/src/commonMain/kotlin/mtmc/emulator/BufferedImage.kt @@ -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 diff --git a/src/commonMain/kotlin/mtmc/emulator/MTMCClock.kt b/src/commonMain/kotlin/mtmc/emulator/MTMCClock.kt index 7b6c005..7cf161d 100644 --- a/src/commonMain/kotlin/mtmc/emulator/MTMCClock.kt +++ b/src/commonMain/kotlin/mtmc/emulator/MTMCClock.kt @@ -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 diff --git a/src/commonMain/kotlin/mtmc/emulator/MTMCDisplay.kt b/src/commonMain/kotlin/mtmc/emulator/MTMCDisplay.kt index 713bb48..9197ca5 100644 --- a/src/commonMain/kotlin/mtmc/emulator/MTMCDisplay.kt +++ b/src/commonMain/kotlin/mtmc/emulator/MTMCDisplay.kt @@ -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 = arrayOf() private var byteArray: ByteArray = ByteArray(0) diff --git a/src/commonMain/kotlin/mtmc/os/MTOS.kt b/src/commonMain/kotlin/mtmc/os/MTOS.kt index 29ba098..282195e 100644 --- a/src/commonMain/kotlin/mtmc/os/MTOS.kt +++ b/src/commonMain/kotlin/mtmc/os/MTOS.kt @@ -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) diff --git a/src/commonMain/kotlin/mtmc/util/PlatformSpecific.kt b/src/commonMain/kotlin/mtmc/util/PlatformSpecific.kt index df7360e..734bcef 100644 --- a/src/commonMain/kotlin/mtmc/util/PlatformSpecific.kt +++ b/src/commonMain/kotlin/mtmc/util/PlatformSpecific.kt @@ -1,6 +1,6 @@ package mtmc.util -expect fun currentTimeMillis(): Long +expect fun currentTimeMillis(): Double expect fun requestAnimationFrame(action: (Double) -> Unit) diff --git a/src/jsMain/kotlin/mtmc/emulator/BufferedImage.js.kt b/src/jsMain/kotlin/mtmc/emulator/BufferedImage.js.kt index d57d0f4..7615817 100644 --- a/src/jsMain/kotlin/mtmc/emulator/BufferedImage.js.kt +++ b/src/jsMain/kotlin/mtmc/emulator/BufferedImage.js.kt @@ -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) +} diff --git a/src/jsMain/kotlin/mtmc/util/PlatformSpecific.js.kt b/src/jsMain/kotlin/mtmc/util/PlatformSpecific.js.kt index 617b597..2a74fad 100644 --- a/src/jsMain/kotlin/mtmc/util/PlatformSpecific.js.kt +++ b/src/jsMain/kotlin/mtmc/util/PlatformSpecific.js.kt @@ -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() } } diff --git a/src/jsMain/kotlin/mtmc/view/ControlView.kt b/src/jsMain/kotlin/mtmc/view/ControlView.kt index b725ac7..c7fc16b 100644 --- a/src/jsMain/kotlin/mtmc/view/ControlView.kt +++ b/src/jsMain/kotlin/mtmc/view/ControlView.kt @@ -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 diff --git a/src/jsMain/kotlin/mtmc/view/DisplayView.kt b/src/jsMain/kotlin/mtmc/view/DisplayView.kt index 0e38338..422b4bd 100644 --- a/src/jsMain/kotlin/mtmc/view/DisplayView.kt +++ b/src/jsMain/kotlin/mtmc/view/DisplayView.kt @@ -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() + ctx = cv?.getContext("webgl")?.unsafeCast() - 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); + } } } } diff --git a/src/jsMain/kotlin/mtmc/view/MemoryView.kt b/src/jsMain/kotlin/mtmc/view/MemoryView.kt index 67faec1..fe07db4 100644 --- a/src/jsMain/kotlin/mtmc/view/MemoryView.kt +++ b/src/jsMain/kotlin/mtmc/view/MemoryView.kt @@ -89,7 +89,6 @@ class MemoryView( else -> +"?" } } - } } } diff --git a/src/jvmMain/kotlin/mtmc/emulator/BufferedImage.jvm.kt b/src/jvmMain/kotlin/mtmc/emulator/BufferedImage.jvm.kt index 846cc33..707f908 100644 --- a/src/jvmMain/kotlin/mtmc/emulator/BufferedImage.jvm.kt +++ b/src/jvmMain/kotlin/mtmc/emulator/BufferedImage.jvm.kt @@ -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") } \ No newline at end of file diff --git a/src/jvmMain/kotlin/mtmc/util/PlatformSpecific.jvm.kt b/src/jvmMain/kotlin/mtmc/util/PlatformSpecific.jvm.kt index 24b9fbb..119a65b 100644 --- a/src/jvmMain/kotlin/mtmc/util/PlatformSpecific.jvm.kt +++ b/src/jvmMain/kotlin/mtmc/util/PlatformSpecific.jvm.kt @@ -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") }