generated from rnentjes/kotlin-server-web-undertow
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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ class MemoryView(
|
|||||||
else -> +"?"
|
else -> +"?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user