video resize

This commit is contained in:
minjaesong
2025-09-08 17:33:25 +09:00
parent 0c26d12d65
commit 79f8a25f41
3 changed files with 183 additions and 59 deletions

View File

@@ -670,7 +670,7 @@ try {
// Upload RGB buffer to display framebuffer with dithering // Upload RGB buffer to display framebuffer with dithering
let uploadStart = sys.nanoTime() 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 uploadTime = (sys.nanoTime() - uploadStart) / 1000000.0 // Convert to milliseconds
} }
else { else {

View File

@@ -1312,8 +1312,21 @@ class GraphicsJSR223Delegate(private val vm: VM) {
* @param rgbAddr Source RGB buffer (24-bit: R,G,B bytes) * @param rgbAddr Source RGB buffer (24-bit: R,G,B bytes)
* @param width Frame width * @param width Frame width
* @param height Frame height * @param height Frame height
* @param frameCounter Frame counter for dithering
*/ */
fun uploadRGBToFramebuffer(rgbAddr: Long, width: Int, height: Int, frameCounter: Int) { 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 gpu = (vm.peripheralTable[1].peripheral as GraphicsAdapter)
val rgbAddrIncVec = if (rgbAddr >= 0) 1 else -1 val rgbAddrIncVec = if (rgbAddr >= 0) 1 else -1
@@ -1322,68 +1335,121 @@ class GraphicsJSR223Delegate(private val vm: VM) {
val nativeWidth = gpu.config.width val nativeWidth = gpu.config.width
val nativeHeight = gpu.config.height val nativeHeight = gpu.config.height
// Calculate centering offset
val offsetX = (nativeWidth - width) / 2
val offsetY = (nativeHeight - height) / 2
val totalNativePixels = (nativeWidth * nativeHeight).toLong() val totalNativePixels = (nativeWidth * nativeHeight).toLong()
// Process video pixels in 8KB chunks to balance memory usage and performance if (resizeToFull && (width / 2 != nativeWidth / 2 || height / 2 != nativeHeight / 2)) {
val totalVideoPixels = width * height // Calculate scaling factors for resize-to-full (source to native mapping)
val chunkSize = 8192 val scaleX = width.toFloat() / nativeWidth.toFloat()
val rgChunk = ByteArray(chunkSize) val scaleY = height.toFloat() / nativeHeight.toFloat()
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 // Process native pixels in 8KB chunks
for (i in 0 until pixelsInChunk) { val chunkSize = 8192
val pixelIndex = pixelsProcessed + i val rgChunk = ByteArray(chunkSize)
val videoY = pixelIndex / width val baChunk = ByteArray(chunkSize)
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 var pixelsProcessed = 0
for (i in 0 until pixelsInChunk) {
val pos = positionChunk[i].toLong() while (pixelsProcessed < totalNativePixels) {
// Bounds check to ensure we don't write outside framebuffer val pixelsInChunk = kotlin.math.min(chunkSize, (totalNativePixels - pixelsProcessed).toInt())
if (pos in 0 until totalNativePixels) {
UnsafeHelper.memcpyRaw( // Batch process chunk of pixels
rgChunk, UnsafeHelper.getArrayOffset(rgChunk) + i, for (i in 0 until pixelsInChunk) {
null, gpu.framebuffer.ptr + pos, 1L) val nativePixelIndex = pixelsProcessed + i
UnsafeHelper.memcpyRaw( val nativeY = nativePixelIndex / nativeWidth
baChunk, UnsafeHelper.getArrayOffset(baChunk) + i, val nativeX = nativePixelIndex % nativeWidth
null, gpu.framebuffer2!!.ptr + pos, 1L)
// 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) 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 -> val dctBasis8 = Array(8) { u ->
FloatArray(8) { x -> FloatArray(8) { x ->

View File

@@ -256,7 +256,11 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
private val defaultGuiBackgroundColour = Color(0x444444ff) private val defaultGuiBackgroundColour = Color(0x444444ff)
private var framecount = 0L
private fun renderGame(delta: Float) { private fun renderGame(delta: Float) {
framecount++
camera.setToOrtho(false, viewportWidth.toFloat(), viewportHeight.toFloat()) camera.setToOrtho(false, viewportWidth.toFloat(), viewportHeight.toFloat())
batch.projectionMatrix = camera.combined batch.projectionMatrix = camera.combined
gpuFBO.begin() gpuFBO.begin()
@@ -286,6 +290,7 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
// draw GPU and border // draw GPU and border
batch.shader = crtShader batch.shader = crtShader
batch.shader.setUniformf("resolution", viewportWidth.toFloat(), viewportHeight.toFloat()) 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.setBlendFunctionSeparate(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA, GL20.GL_SRC_ALPHA, GL20.GL_ONE)
batch.draw(gpuFBO.colorBufferTexture, 0f, 0f) batch.draw(gpuFBO.colorBufferTexture, 0f, 0f)
} }
@@ -415,7 +420,7 @@ uniform sampler2D u_texture;
uniform vec2 resolution = vec2(640.0, 480.0); uniform vec2 resolution = vec2(640.0, 480.0);
out vec4 fragColor; 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 one = vec4(1.0);
const vec4 pointfive = vec4(0.5); const vec4 pointfive = vec4(0.5);
@@ -457,6 +462,8 @@ uniform float gcount = 96.0;
uniform float bcount = 96.0; uniform float bcount = 96.0;
uniform float acount = 1.0; uniform float acount = 1.0;
uniform float interlacer = 0.0;
vec4 toYUV(vec4 rgb) { return rgb_to_yuv * rgb; } vec4 toYUV(vec4 rgb) { return rgb_to_yuv * rgb; }
vec4 toRGB(vec4 ycc) { return yuv_to_rgb * ycc; } vec4 toRGB(vec4 ycc) { return yuv_to_rgb * ycc; }
@@ -538,7 +545,7 @@ void main() {
vec4 wgtavr = avr(LRavr, colourIn, gamma); vec4 wgtavr = avr(LRavr, colourIn, gamma);
vec4 outCol = wgtavr; 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 // 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! 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!