From 79f8a25f419dd030fd9caa07aca62a41543e00f8 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Mon, 8 Sep 2025 17:33:25 +0900 Subject: [PATCH] video resize --- assets/disk0/tvdos/bin/playtev.js | 2 +- .../torvald/tsvm/GraphicsJSR223Delegate.kt | 229 +++++++++++++----- tsvm_executable/src/net/torvald/tsvm/VMGUI.kt | 11 +- 3 files changed, 183 insertions(+), 59 deletions(-) diff --git a/assets/disk0/tvdos/bin/playtev.js b/assets/disk0/tvdos/bin/playtev.js index 7561cdc..0572548 100644 --- a/assets/disk0/tvdos/bin/playtev.js +++ b/assets/disk0/tvdos/bin/playtev.js @@ -670,7 +670,7 @@ try { // Upload RGB buffer to display framebuffer with dithering let uploadStart = sys.nanoTime() - graphics.uploadRGBToFramebuffer(CURRENT_RGB_ADDR, width, height, frameCount) + graphics.uploadRGBToFramebuffer(CURRENT_RGB_ADDR, width, height, frameCount, true) uploadTime = (sys.nanoTime() - uploadStart) / 1000000.0 // Convert to milliseconds } else { diff --git a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt index 73d5ad1..6d622d7 100644 --- a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt @@ -1312,8 +1312,21 @@ class GraphicsJSR223Delegate(private val vm: VM) { * @param rgbAddr Source RGB buffer (24-bit: R,G,B bytes) * @param width Frame width * @param height Frame height + * @param frameCounter Frame counter for dithering */ fun uploadRGBToFramebuffer(rgbAddr: Long, width: Int, height: Int, frameCounter: Int) { + uploadRGBToFramebuffer(rgbAddr, width, height, frameCounter, false) + } + + /** + * Upload RGB frame buffer to graphics framebuffer with dithering and optional resize + * @param rgbAddr Source RGB buffer (24-bit: R,G,B bytes) + * @param width Frame width + * @param height Frame height + * @param frameCounter Frame counter for dithering + * @param resizeToFull If true, resize video to fill entire screen; if false, center video + */ + fun uploadRGBToFramebuffer(rgbAddr: Long, width: Int, height: Int, frameCounter: Int, resizeToFull: Boolean) { val gpu = (vm.peripheralTable[1].peripheral as GraphicsAdapter) val rgbAddrIncVec = if (rgbAddr >= 0) 1 else -1 @@ -1322,68 +1335,121 @@ class GraphicsJSR223Delegate(private val vm: VM) { val nativeWidth = gpu.config.width val nativeHeight = gpu.config.height - // Calculate centering offset - val offsetX = (nativeWidth - width) / 2 - val offsetY = (nativeHeight - height) / 2 - val totalNativePixels = (nativeWidth * nativeHeight).toLong() - // Process video pixels in 8KB chunks to balance memory usage and performance - val totalVideoPixels = width * height - val chunkSize = 8192 - val rgChunk = ByteArray(chunkSize) - val baChunk = ByteArray(chunkSize) - val positionChunk = IntArray(chunkSize) // Store framebuffer positions - - var pixelsProcessed = 0 - - while (pixelsProcessed < totalVideoPixels) { - val pixelsInChunk = kotlin.math.min(chunkSize, totalVideoPixels - pixelsProcessed) + if (resizeToFull && (width / 2 != nativeWidth / 2 || height / 2 != nativeHeight / 2)) { + // Calculate scaling factors for resize-to-full (source to native mapping) + val scaleX = width.toFloat() / nativeWidth.toFloat() + val scaleY = height.toFloat() / nativeHeight.toFloat() - // Batch process chunk of pixels - for (i in 0 until pixelsInChunk) { - val pixelIndex = pixelsProcessed + i - val videoY = pixelIndex / width - val videoX = pixelIndex % width - - // Calculate position in native framebuffer (centered) - val nativeX = videoX + offsetX - val nativeY = videoY + offsetY - val nativePos = nativeY * nativeWidth + nativeX - positionChunk[i] = nativePos - - val rgbOffset = (pixelIndex.toLong() * 3) * rgbAddrIncVec - - // Read RGB values (3 peek operations per pixel - still the bottleneck) - val r = vm.peek(rgbAddr + rgbOffset)!!.toUint() - val g = vm.peek(rgbAddr + rgbOffset + rgbAddrIncVec)!!.toUint() - val b = vm.peek(rgbAddr + rgbOffset + rgbAddrIncVec * 2)!!.toUint() - - // Apply Bayer dithering and convert to 4-bit - val r4 = ditherValue(r, videoX, videoY, frameCounter) - val g4 = ditherValue(g, videoX, videoY, frameCounter) - val b4 = ditherValue(b, videoX, videoY, frameCounter) - - // Pack and store in chunk buffers - rgChunk[i] = ((r4 shl 4) or g4).toByte() - baChunk[i] = ((b4 shl 4) or 15).toByte() - } + // Process native pixels in 8KB chunks + val chunkSize = 8192 + val rgChunk = ByteArray(chunkSize) + val baChunk = ByteArray(chunkSize) - // Write pixels to their calculated positions in framebuffer - for (i in 0 until pixelsInChunk) { - val pos = positionChunk[i].toLong() - // Bounds check to ensure we don't write outside framebuffer - if (pos in 0 until totalNativePixels) { - UnsafeHelper.memcpyRaw( - rgChunk, UnsafeHelper.getArrayOffset(rgChunk) + i, - null, gpu.framebuffer.ptr + pos, 1L) - UnsafeHelper.memcpyRaw( - baChunk, UnsafeHelper.getArrayOffset(baChunk) + i, - null, gpu.framebuffer2!!.ptr + pos, 1L) + var pixelsProcessed = 0 + + while (pixelsProcessed < totalNativePixels) { + val pixelsInChunk = kotlin.math.min(chunkSize, (totalNativePixels - pixelsProcessed).toInt()) + + // Batch process chunk of pixels + for (i in 0 until pixelsInChunk) { + val nativePixelIndex = pixelsProcessed + i + val nativeY = nativePixelIndex / nativeWidth + val nativeX = nativePixelIndex % nativeWidth + + // Map native pixel to source video coordinates for bilinear sampling + val videoX = nativeX * scaleX + val videoY = nativeY * scaleY + + // Sample RGB values using bilinear interpolation + val rgb = sampleBilinear(rgbAddr, width, height, videoX, videoY, rgbAddrIncVec) + val r = rgb[0] + val g = rgb[1] + val b = rgb[2] + + // Apply Bayer dithering and convert to 4-bit using native coordinates + val r4 = ditherValue(r, nativeX, nativeY, frameCounter) + val g4 = ditherValue(g, nativeX, nativeY, frameCounter) + val b4 = ditherValue(b, nativeX, nativeY, frameCounter) + + // Pack and store in chunk buffers + rgChunk[i] = ((r4 shl 4) or g4).toByte() + baChunk[i] = ((b4 shl 4) or 15).toByte() } - } + + // Write pixels to their sequential positions in framebuffer + UnsafeHelper.memcpyRaw( + rgChunk, UnsafeHelper.getArrayOffset(rgChunk), + null, gpu.framebuffer.ptr + pixelsProcessed, pixelsInChunk.toLong()) + UnsafeHelper.memcpyRaw( + baChunk, UnsafeHelper.getArrayOffset(baChunk), + null, gpu.framebuffer2!!.ptr + pixelsProcessed, pixelsInChunk.toLong()) - pixelsProcessed += pixelsInChunk + pixelsProcessed += pixelsInChunk + } + } else { + // Original centering logic + val offsetX = (nativeWidth - width) / 2 + val offsetY = (nativeHeight - height) / 2 + + // Process video pixels in 8KB chunks to balance memory usage and performance + val totalVideoPixels = width * height + val chunkSize = 65536 + val rgChunk = ByteArray(chunkSize) + val baChunk = ByteArray(chunkSize) + val positionChunk = IntArray(chunkSize) // Store framebuffer positions + + var pixelsProcessed = 0 + + while (pixelsProcessed < totalVideoPixels) { + val pixelsInChunk = kotlin.math.min(chunkSize, totalVideoPixels - pixelsProcessed) + + // Batch process chunk of pixels + for (i in 0 until pixelsInChunk) { + val pixelIndex = pixelsProcessed + i + val videoY = pixelIndex / width + val videoX = pixelIndex % width + + // Calculate position in native framebuffer (centered) + val nativeX = videoX + offsetX + val nativeY = videoY + offsetY + val nativePos = nativeY * nativeWidth + nativeX + positionChunk[i] = nativePos + + val rgbOffset = (pixelIndex.toLong() * 3) * rgbAddrIncVec + + // Read RGB values (3 peek operations per pixel - still the bottleneck) + val r = vm.peek(rgbAddr + rgbOffset)!!.toUint() + val g = vm.peek(rgbAddr + rgbOffset + rgbAddrIncVec)!!.toUint() + val b = vm.peek(rgbAddr + rgbOffset + rgbAddrIncVec * 2)!!.toUint() + + // Apply Bayer dithering and convert to 4-bit + val r4 = ditherValue(r, videoX, videoY, frameCounter) + val g4 = ditherValue(g, videoX, videoY, frameCounter) + val b4 = ditherValue(b, videoX, videoY, frameCounter) + + // Pack and store in chunk buffers + rgChunk[i] = ((r4 shl 4) or g4).toByte() + baChunk[i] = ((b4 shl 4) or 15).toByte() + } + + // Write pixels to their calculated positions in framebuffer + for (i in 0 until pixelsInChunk) { + val pos = positionChunk[i].toLong() + // Bounds check to ensure we don't write outside framebuffer + if (pos in 0 until totalNativePixels) { + UnsafeHelper.memcpyRaw( + rgChunk, UnsafeHelper.getArrayOffset(rgChunk) + i, + null, gpu.framebuffer.ptr + pos, 1L) + UnsafeHelper.memcpyRaw( + baChunk, UnsafeHelper.getArrayOffset(baChunk) + i, + null, gpu.framebuffer2!!.ptr + pos, 1L) + } + } + + pixelsProcessed += pixelsInChunk + } } } @@ -1400,6 +1466,57 @@ class GraphicsJSR223Delegate(private val vm: VM) { return round(15f * q) } + /** + * Sample RGB values using bilinear interpolation + * @param rgbAddr Source RGB buffer address + * @param width Source image width + * @param height Source image height + * @param x Floating-point x coordinate in source image + * @param y Floating-point y coordinate in source image + * @param rgbAddrIncVec Address increment vector + * @return IntArray containing interpolated [R, G, B] values + */ + private fun sampleBilinear(rgbAddr: Long, width: Int, height: Int, x: Float, y: Float, rgbAddrIncVec: Int): IntArray { + // Clamp coordinates to valid range + val clampedX = x.coerceIn(0f, (width - 1).toFloat()) + val clampedY = y.coerceIn(0f, (height - 1).toFloat()) + + // Get integer coordinates and fractional parts + val x0 = clampedX.toInt() + val y0 = clampedY.toInt() + val x1 = kotlin.math.min(x0 + 1, width - 1) + val y1 = kotlin.math.min(y0 + 1, height - 1) + + val fx = clampedX - x0 + val fy = clampedY - y0 + + // Sample the four corner pixels + fun samplePixel(px: Int, py: Int): IntArray { + val pixelIndex = py * width + px + val rgbOffset = (pixelIndex.toLong() * 3) * rgbAddrIncVec + return intArrayOf( + vm.peek(rgbAddr + rgbOffset)!!.toUint(), + vm.peek(rgbAddr + rgbOffset + rgbAddrIncVec)!!.toUint(), + vm.peek(rgbAddr + rgbOffset + rgbAddrIncVec * 2)!!.toUint() + ) + } + + val c00 = samplePixel(x0, y0) // top-left + val c10 = samplePixel(x1, y0) // top-right + val c01 = samplePixel(x0, y1) // bottom-left + val c11 = samplePixel(x1, y1) // bottom-right + + // Bilinear interpolation + val result = IntArray(3) + for (i in 0..2) { + val top = c00[i] * (1f - fx) + c10[i] * fx + val bottom = c01[i] * (1f - fx) + c11[i] * fx + result[i] = (top * (1f - fy) + bottom * fy).toInt().coerceIn(0, 255) + } + + return result + } + val dctBasis8 = Array(8) { u -> FloatArray(8) { x -> diff --git a/tsvm_executable/src/net/torvald/tsvm/VMGUI.kt b/tsvm_executable/src/net/torvald/tsvm/VMGUI.kt index 9fbf549..471ad89 100644 --- a/tsvm_executable/src/net/torvald/tsvm/VMGUI.kt +++ b/tsvm_executable/src/net/torvald/tsvm/VMGUI.kt @@ -256,7 +256,11 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe private val defaultGuiBackgroundColour = Color(0x444444ff) + private var framecount = 0L + private fun renderGame(delta: Float) { + framecount++ + camera.setToOrtho(false, viewportWidth.toFloat(), viewportHeight.toFloat()) batch.projectionMatrix = camera.combined gpuFBO.begin() @@ -286,6 +290,7 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe // draw GPU and border batch.shader = crtShader batch.shader.setUniformf("resolution", viewportWidth.toFloat(), viewportHeight.toFloat()) + batch.shader.setUniformf("interlacer", (framecount % 2).toFloat()) batch.setBlendFunctionSeparate(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA, GL20.GL_SRC_ALPHA, GL20.GL_ONE) batch.draw(gpuFBO.colorBufferTexture, 0f, 0f) } @@ -415,7 +420,7 @@ uniform sampler2D u_texture; uniform vec2 resolution = vec2(640.0, 480.0); out vec4 fragColor; -const vec4 scanline = vec4(0.9, 0.9, 0.9, 1.0); +const vec4 scanline = vec4(0.8, 0.8, 0.8, 1.0); const vec4 one = vec4(1.0); const vec4 pointfive = vec4(0.5); @@ -457,6 +462,8 @@ uniform float gcount = 96.0; uniform float bcount = 96.0; uniform float acount = 1.0; +uniform float interlacer = 0.0; + vec4 toYUV(vec4 rgb) { return rgb_to_yuv * rgb; } vec4 toRGB(vec4 ycc) { return yuv_to_rgb * ycc; } @@ -538,7 +545,7 @@ void main() { vec4 wgtavr = avr(LRavr, colourIn, gamma); vec4 outCol = wgtavr; - vec4 out2 = clamp(grading(outCol, gradingarg) * ((mod(gl_FragCoord.y, 2.0) >= 1.0) ? scanline : one), 0.0, 1.0); + vec4 out2 = clamp(grading(outCol, gradingarg) * ((mod(gl_FragCoord.y + interlacer, 2.0) >= 1.0) ? scanline : one), 0.0, 1.0); // mix in CRT glass overlay float spread = 1.0 / (0.299 * (rcount - 1.0) + 0.587 * (gcount - 1.0) + 0.114 * (bcount - 1.0)); // this spread value is optimised one -- try your own values for various effects!