mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-03-07 11:51:49 +09:00
video resize
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
@@ -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!
|
||||||
|
|||||||
Reference in New Issue
Block a user