From 701570373f65cbc22b3c2b33db765e00bfc41e2b Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 15 Jan 2024 21:24:35 +0100 Subject: [PATCH] Move the conversion from color index to color into the shader. Like this, only the index of the color is set on the CPU. The conversion to color is set on the GPU and the engine doesn't need to pass over the full image again. --- .../minigdx/tiny/graphic/ColorPalette.kt | 2 +- .../minigdx/tiny/graphic/FrameBuffer.kt | 37 +++------- .../github/minigdx/tiny/graphic/PixelArray.kt | 28 ++++--- .../com/github/minigdx/tiny/lua/SprLib.kt | 2 +- .../com/github/minigdx/tiny/lua/StdLib.kt | 6 +- .../github/minigdx/tiny/render/GLRender.kt | 74 ++++++++++++++++--- .../minigdx/tiny/render/GLRenderContext.kt | 1 + .../tiny/platform/glfw/GlfwPlatform.kt | 19 ++++- 8 files changed, 110 insertions(+), 59 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/ColorPalette.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/ColorPalette.kt index e069d817..d2998533 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/ColorPalette.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/ColorPalette.kt @@ -8,7 +8,7 @@ import kotlin.math.abs /** * Color palette used by the game. * - * Every colors from every resources will be aligned to use this color palette. + * Every color from every resource will be aligned to use this color palette. * It means that if a color from a resource is not part of this color palette, * this color will be replaced will the closed color from the palette. * diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBuffer.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBuffer.kt index d2861df9..ec8dd144 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBuffer.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBuffer.kt @@ -30,7 +30,7 @@ class Blender(private val gamePalette: ColorPalette) { switch[gamePalette.check(source)] = gamePalette.check(target) } - fun mix(colors: Array, x: Pixel, y: Pixel, transparency: Array?): Array? { + fun mix(colors: ByteArray, x: Pixel, y: Pixel, transparency: Array?): ByteArray? { fun dither(pattern: Int): Boolean { val a = x % 4 val b = (y % 4) * 4 @@ -38,10 +38,10 @@ class Blender(private val gamePalette: ColorPalette) { return (pattern shr (15 - (a + b))) and 0x01 == 0x01 } - val color = gamePalette.check(colors[0]) - colors[0] = switch[gamePalette.check(color)] + val color = gamePalette.check(colors[0].toInt()) + colors[0] = switch[color].toByte() // Return null if transparent - if (transparency == null && gamePalette.isTransparent(colors[0])) return null + if (transparency == null && gamePalette.isTransparent(colors[0].toInt())) return null return if (!dither(dithering)) { null } else { @@ -88,7 +88,7 @@ class FrameBuffer( internal val camera = Camera() - private var tmp = Array(1) { 0 } + private var tmp = ByteArray(1) { 0 } private val transparency = arrayOf(0) @@ -103,9 +103,9 @@ class FrameBuffer( val cy = camera.cy(y) if (!clipper.isIn(cx, cy)) return - tmp[0] = gamePalette.check(colorIndex) + tmp[0] = gamePalette.check(colorIndex).toByte() val index = blender.mix(tmp, cx, cy, transparency) ?: return - colorIndexBuffer.set(cx, cy, index[0]) + colorIndexBuffer.set(cx, cy, index[0].toInt()) } fun fill(startX: Pixel, endX: Pixel, y: Pixel, colorIndex: ColorIndex) { @@ -129,7 +129,7 @@ class FrameBuffer( pixel(x, y, colorIndex) } } else { - tmp[0] = color + tmp[0] = color.toByte() val targetColor = blender.mix(tmp, 0, 0, transparency) ?: return colorIndexBuffer.fill(left, right, cy, targetColor[0]) } @@ -180,7 +180,7 @@ class FrameBuffer( /** * Blend function */ - blender: (Array, Pixel, Pixel) -> Array = { colors, _, _ -> colors }, + blender: (ByteArray, Pixel, Pixel) -> ByteArray = { colors, _, _ -> colors }, ) { val cx = camera.cx(dstX) val cy = camera.cy(dstY) @@ -209,23 +209,6 @@ class FrameBuffer( * Create a buffer using the colorIndexBuffer as reference. */ fun generateBuffer(): ByteArray { - // Reset the old buffer - gifBuffer = IntArray(height * width) - - var pos = 0 - for (x in 0 until width) { - for (y in 0 until height) { - val index = colorIndexBuffer.getOne(x, y) - val color = gamePalette.getRGBA(index) - - buffer[pos++] = color[0] - buffer[pos++] = color[1] - buffer[pos++] = color[2] - buffer[pos++] = color[3] - - gifBuffer[x + y * width] = gamePalette.getRGAasInt(index) - } - } - return buffer + return this.colorIndexBuffer.pixels } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/PixelArray.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/PixelArray.kt index 8e6271b8..eee435ae 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/PixelArray.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/PixelArray.kt @@ -7,11 +7,11 @@ import kotlin.math.min class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = PixelFormat.INDEX) { - internal var pixels = Array(width * height * pixelFormat) { 0 } + internal var pixels = ByteArray(width * height * pixelFormat) { 0 } val size = width * height * pixelFormat - private val tmp = Array(pixelFormat) { 0 } + private val tmp = ByteArray(pixelFormat) { 0 } fun copyFrom(array: PixelArray) { pixels = array.pixels.copyOf() @@ -24,13 +24,13 @@ class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = Pix val ctop = 0 - top val cbottom = bottom + 2 * ctop - pixels = Array(width * height * pixelFormat) { index -> + pixels = ByteArray(width * height * pixelFormat) { index -> val p = index / pixelFormat val x = p % width val y = p / width if (x in cleft..cright && y in ctop..cbottom) { - pixel + pixel.toByte() } else { 0 } @@ -47,13 +47,13 @@ class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = Pix val position = (correctedX + correctedY * width) * pixelFormat pixel.forEachIndexed { index, value -> - pixels[position + index] = value + pixels[position + index] = value.toByte() } } - fun get(x: Pixel, y: Pixel): Array { + fun get(x: Pixel, y: Pixel): ByteArray { assert(x >= 0 && x < width) { "x ($x) has to be between 0 and $width (excluded)" } - assert(y >= 0 && x < height) { "y ($y) has to be between 0 and $height (excluded)" } + assert(y >= 0 && y < height) { "y ($y) has to be between 0 and $height (excluded)" } val position = (x + y * width) * pixelFormat when (pixelFormat) { PixelFormat.RGBA -> { @@ -81,7 +81,7 @@ class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = Pix * The pixel format should be equals to 1 otherwise * it will returns only the first component of the color. */ - fun getOne(x: Pixel, y: Pixel): Int = get(x, y)[0] + fun getOne(x: Pixel, y: Pixel): Int = get(x, y)[0].toInt() fun copyFrom( source: PixelArray, @@ -93,7 +93,7 @@ class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = Pix height: Pixel = this.height, reverseX: Boolean = false, reverseY: Boolean = false, - blender: (Array, Pixel, Pixel) -> Array?, + blender: (ByteArray, Pixel, Pixel) -> ByteArray?, ) { assert(source.pixelFormat == pixelFormat) { "Can't copy PixelArray because the pixel format is different between the two PixelArray" @@ -103,14 +103,14 @@ class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = Pix val minHeight = min(height, min(height - (dstY + height - this.height), height - (sourceY + height - source.height))) - (0 until minHeight).forEach { h -> + for (h in 0 until minHeight) { val offsetY = if (reverseY) { minHeight - h - 1 } else { h } - (0 until minWidth).forEach { w -> + for (w in 0 until minWidth) { val dstPosition = (w + dstX + (h + dstY) * this.width) * pixelFormat val offsetX = if (reverseX) { @@ -136,9 +136,13 @@ class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = Pix } } - operator fun iterator(): Iterator = pixels.iterator() + // operator fun iterator(): Iterator = pixels.iterator() fun fill(startX: Int, endX: Int, y: Int, value: Int) { + fill(startX, endX, y, value.toByte()) + } + + fun fill(startX: Int, endX: Int, y: Int, value: Byte) { val yy = (y * width * pixelFormat) pixels.fill(value, yy + startX * pixelFormat, yy + endX * pixelFormat) } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SprLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SprLib.kt index a847b982..71e78846 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SprLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SprLib.kt @@ -49,7 +49,7 @@ class SprLib(val gameOptions: GameOptions, val resourceAccess: GameResourceAcces if (isInPixelArray(pixelArray, x, y)) { val index = pixelArray.get(x, y) val colorIndex = index.get(0) - return valueOf(colorIndex) + return valueOf(colorIndex.toInt()) } else { return NIL } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt index 7fe19168..1ca05882 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt @@ -246,11 +246,11 @@ class StdLib( indexY * 4, 4, 4, - ) { pixel: Array, _, _ -> - if (pixel[0] == 0) { + ) { pixel: ByteArray, _, _ -> + if (pixel[0].toInt() == 0) { pixel } else { - pixel[0] = color + pixel[0] = color.toByte() pixel } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRender.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRender.kt index c09cfe92..9358338f 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRender.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRender.kt @@ -11,9 +11,13 @@ import com.danielgergely.kgl.GL_FLOAT import com.danielgergely.kgl.GL_FRAGMENT_SHADER import com.danielgergely.kgl.GL_LINK_STATUS import com.danielgergely.kgl.GL_NEAREST +import com.danielgergely.kgl.GL_R8 +import com.danielgergely.kgl.GL_RED import com.danielgergely.kgl.GL_REPEAT import com.danielgergely.kgl.GL_RGBA import com.danielgergely.kgl.GL_STATIC_DRAW +import com.danielgergely.kgl.GL_TEXTURE0 +import com.danielgergely.kgl.GL_TEXTURE1 import com.danielgergely.kgl.GL_TEXTURE_2D import com.danielgergely.kgl.GL_TEXTURE_MAG_FILTER import com.danielgergely.kgl.GL_TEXTURE_MIN_FILTER @@ -26,6 +30,7 @@ import com.danielgergely.kgl.Kgl import com.danielgergely.kgl.Shader import com.github.minigdx.tiny.Pixel import com.github.minigdx.tiny.engine.GameOptions +import com.github.minigdx.tiny.graphic.PixelFormat import com.github.minigdx.tiny.log.Logger import com.github.minigdx.tiny.platform.RenderContext import com.github.minigdx.tiny.platform.WindowManager @@ -36,14 +41,16 @@ class GLRender( private val gameOptions: GameOptions, ) : Render { + private var buffer = ByteArray(0) + private val uvsData = FloatBuffer( floatArrayOf( 2f, - 0f, - 0f, 2f, 0f, 0f, + 0f, + 2f, ), ) @@ -73,9 +80,13 @@ class GLRender( varying vec2 texture; uniform sampler2D image; + uniform sampler2D colors; void main() { - gl_FragColor = texture2D(image, texture); + vec4 point = texture2D(image, texture); + vec4 color = texture2D(colors, vec2(point.r, 1.0)); + + gl_FragColor = color; } """.trimIndent() @@ -110,12 +121,12 @@ class GLRender( gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) val vertexData = floatArrayOf( + 3f, + -1f, -1f, - -3f, 3f, - 1f, -1f, - 1f, + -1f, ) val positionBuffer = gl.createBuffer() @@ -154,10 +165,47 @@ class GLRender( ) gl.enableVertexAttribArray(uvs) + val colors = gameOptions.colors() + // texture of one pixel height and 256 pixel width. + // one pixel of the texture = one index. + buffer = ByteArray(256 * 256 * PixelFormat.RGBA) + var pos = 0 + for (y in 0 until 256) { + for (index in 0 until 256) { + val color = colors.getRGBA(index) + + buffer[pos++] = color[0] + buffer[pos++] = color[1] + buffer[pos++] = color[2] + buffer[pos++] = color[3] + } + } + val index = gl.createTexture() + gl.bindTexture(GL_TEXTURE_2D, index) + + gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT) + gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT) + + gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) + gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) + gl.texImage2D( + GL_TEXTURE_2D, + 0, + GL_RGBA, + 256, + 256, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + ByteBuffer(buffer), + ) + gl.uniform1i(gl.getUniformLocation(shaderProgram, "colors")!!, 1) + return GLRenderContext( windowManager = windowManager, program = shaderProgram, texture = gameTexture, + colors = index, ) } @@ -188,24 +236,26 @@ class GLRender( gameOptions.height * gameOptions.zoom * context.windowManager.ratioHeight, ) + // -- game screen -- // + gl.activeTexture(GL_TEXTURE0) gl.bindTexture(GL_TEXTURE_2D, context.texture) gl.texImage2D( GL_TEXTURE_2D, 0, - GL_RGBA, - // I think that the texture format is not in the format OpenGL expect it (column first or line first) - // So I swap the height and the width so it's working even with non square game resolution. - height, + GL_R8, width, + height, 0, - GL_RGBA, + GL_RED, GL_UNSIGNED_BYTE, ByteBuffer(image), ) - gl.uniform1i(gl.getUniformLocation(context.program, "image")!!, 0) + gl.activeTexture(GL_TEXTURE1) + gl.bindTexture(GL_TEXTURE_2D, context.colors) + gl.clear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) gl.clearColor(0f, 0f, 0f, 1.0f) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRenderContext.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRenderContext.kt index 7395d3b2..b0bc4626 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRenderContext.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRenderContext.kt @@ -8,5 +8,6 @@ import com.github.minigdx.tiny.platform.WindowManager class GLRenderContext( val program: Program, val texture: Texture, + val colors: Texture, val windowManager: WindowManager, ) : RenderContext diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt index 578ea1b2..80bae2d8 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt @@ -56,7 +56,8 @@ class GlfwPlatform( private var lastFrame: Long = getTime() // Keep 30 seconds at 60 frames per seconds - private val gifBufferCache: MutableFixedSizeList = MutableFixedSizeList(gameOptions.record.toInt() * FPS) + private val gifFrameCache: MutableFixedSizeList = MutableFixedSizeList(gameOptions.record.toInt() * FPS) + private var lastBuffer: FrameBuffer? = null private val lwjglInputHandler = LwjglInput(gameOptions) @@ -187,10 +188,22 @@ class GlfwPlatform( GLFW.glfwTerminate() } + private fun convert(data: ByteArray): IntArray { + val result = IntArray(data.size) + val colorPalette = gameOptions.colors() + data.forEachIndexed { index, byte -> + result[index] = colorPalette.getRGAasInt(byte.toInt()) + } + return result + } + override fun draw(context: RenderContext, frameBuffer: FrameBuffer) { val image = frameBuffer.generateBuffer() render.draw(context, image, frameBuffer.width, frameBuffer.height) - gifBufferCache.add(frameBuffer.gifBuffer) + val imageCopy = image.copyOf() + recordScope.launch { + gifFrameCache.add(convert(imageCopy)) + } lastBuffer = frameBuffer } @@ -201,7 +214,7 @@ class GlfwPlatform( logger.info("GLWF") { "Starting to generate GIF in '${origin.absolutePath}' (Wait for it...)" } val buffer = mutableListOf().apply { - addAll(gifBufferCache) + addAll(gifFrameCache) } recordScope.launch {