From 3020229932b3296f6ccd46bb0e3ea44f1389b1ca Mon Sep 17 00:00:00 2001 From: RDW Date: Tue, 6 Feb 2024 16:22:11 +0100 Subject: [PATCH] Perf: Replace CPU texture pre-processing with shader logic This reduces the loading times, at the cost of having to perform more checks on the GPU. I highly doubt GPU usage will be an issue however, whereas loading times are very painful in general. The drawback here is that if a single texture was to be reused many times, it would incur a runtime cost for each instance, whereas pre-processing has a startup cost shared between all instances. But I doubt it's relevant here, and there's no mechanism to reuse resources currently, anyway. Update Renderer.spec.lua --- Core/NativeClient/Renderer.lua | 47 --------- .../WebGPU/Shaders/TerrainGeometryShader.wgsl | 11 ++- Tests/NativeClient/Renderer.spec.lua | 95 ------------------- 3 files changed, 10 insertions(+), 143 deletions(-) diff --git a/Core/NativeClient/Renderer.lua b/Core/NativeClient/Renderer.lua index 6aa4f32b..4d9a076c 100644 --- a/Core/NativeClient/Renderer.lua +++ b/Core/NativeClient/Renderer.lua @@ -820,54 +820,7 @@ function Renderer:CreateDummyTexture() self.dummyTextureMaterial = dummyTextureMaterial end --- Should probably move this to the runtime for efficiency (needs benchmarking) --- Also... should use string buffers everywhere, but currently the API still uses strings -local function discardTransparentPixels(rgbaImageBytes, width, height, discardRanges) - local OFFSET_RED = 0 - local OFFSET_GREEN = 1 - local OFFSET_BLUE = 2 - local OFFSET_ALPHA = 3 - - local DISCARD_MIN_RED = discardRanges.red.from - local DISCARD_MAX_RED = discardRanges.red.to - local DISCARD_MIN_GREEN = discardRanges.green.from - local DISCARD_MAX_GREEN = discardRanges.green.to - local DISCARD_MIN_BLUE = discardRanges.blue.from - local DISCARD_MAX_BLUE = discardRanges.blue.to - - local rgbaImageBuffer = buffer.new(width * height * 4):put(rgbaImageBytes) - local pixelArray, bufferSize = rgbaImageBuffer:ref() - assert(bufferSize == width * height * 4) - - for pixelStartOffset = 0, width * height * 4, 4 do - local red = pixelArray[pixelStartOffset + OFFSET_RED] - local green = pixelArray[pixelStartOffset + OFFSET_GREEN] - local blue = pixelArray[pixelStartOffset + OFFSET_BLUE] - - local isRedWithinDiscardedRange = (red >= DISCARD_MIN_RED and red <= DISCARD_MAX_RED) - local isGreenWithinDiscardedRange = (green >= DISCARD_MIN_GREEN and green <= DISCARD_MAX_GREEN) - local isBlueWithinDiscardedRange = (blue >= DISCARD_MIN_BLUE and blue <= DISCARD_MAX_BLUE) - local shouldDiscardPixel = isRedWithinDiscardedRange - and isGreenWithinDiscardedRange - and isBlueWithinDiscardedRange - - if shouldDiscardPixel then - pixelArray[pixelStartOffset + OFFSET_ALPHA] = 0 - end - end - - return rgbaImageBuffer:tostring() -end - function Renderer:CreateTextureFromImage(rgbaImageBytes, width, height) - local inclusiveTransparentPixelRanges = { - red = { from = 254, to = 255 }, - green = { from = 0, to = 3 }, - blue = { from = 254, to = 255 }, - } - -- This is currently NOT in-place and so incurs unnecessary copy overhead (optimize later) - rgbaImageBytes = discardTransparentPixels(rgbaImageBytes, width, height, inclusiveTransparentPixelRanges) - local texture = Texture(self.wgpuDevice, rgbaImageBytes, width, height) Renderer:UploadTextureImage(texture) diff --git a/Core/NativeClient/WebGPU/Shaders/TerrainGeometryShader.wgsl b/Core/NativeClient/WebGPU/Shaders/TerrainGeometryShader.wgsl index 7f9f14e9..20f289b8 100644 --- a/Core/NativeClient/WebGPU/Shaders/TerrainGeometryShader.wgsl +++ b/Core/NativeClient/WebGPU/Shaders/TerrainGeometryShader.wgsl @@ -135,14 +135,23 @@ fn vs_main(in: VertexInput) -> VertexOutput { return out; } +// Magenta background pixels should be discarded (but pre-processing on the CPU is expensive) +fn isTransparentBackgroundPixel(diffuseTextureColor : vec4f) -> bool { + return (diffuseTextureColor.r >= 254/255 && diffuseTextureColor.g <= 3/255 && diffuseTextureColor.b >= 254/255); +} + @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let textureCoords = in.diffuseTextureCoords; - let diffuseTextureColor = textureSample(diffuseTexture, diffuseTextureSampler, textureCoords); + var diffuseTextureColor = textureSample(diffuseTexture, diffuseTextureSampler, textureCoords); let normal = normalize(in.surfaceNormal); let sunlightColor = uPerSceneData.directionalLightColor.rgb; let ambientColor = uPerSceneData.ambientLight.rgb; + if (isTransparentBackgroundPixel(diffuseTextureColor)) { + diffuseTextureColor.a = 0.0; + } + let lightmapTexCoords = in.lightmapTextureCoords; var lightmapTextureColor = textureSample(lightmapTexture, lightmapTextureSampler, lightmapTexCoords); diff --git a/Tests/NativeClient/Renderer.spec.lua b/Tests/NativeClient/Renderer.spec.lua index 195db7b4..be7453bb 100644 --- a/Tests/NativeClient/Renderer.spec.lua +++ b/Tests/NativeClient/Renderer.spec.lua @@ -1,6 +1,5 @@ local etrace = require("etrace") local ffi = require("ffi") -local miniz = require("miniz") local Plane = require("Core.NativeClient.DebugDraw.Plane") local Mesh = require("Core.NativeClient.WebGPU.Mesh") @@ -57,100 +56,6 @@ describe("Renderer", function() assertEquals(tonumber(events[1].payload.dataLayout.bytesPerRow), width * 4) assertEquals(tonumber(events[1].payload.dataLayout.rowsPerImage), height) end) - - it("should discard transparent background pixels in the final image", function() - local IMAGE_WIDTH, IMAGE_HEIGHT = 256, 256 - local transparentColors = { - "\254\000\254\255", - "\254\000\255\255", - "\254\001\254\255", - "\254\001\255\255", - "\254\002\254\255", - "\254\002\255\255", - "\254\003\254\255", - "\254\003\255\255", - "\255\000\254\255", - "\255\000\255\255", - "\255\001\254\255", - "\255\001\255\255", - "\255\002\254\255", - "\255\002\255\255", - "\255\003\254\255", - "\255\003\255\255", - } - - local function createImageBytes(pixel) - return string.rep(pixel, IMAGE_WIDTH * IMAGE_HEIGHT) - end - - local transparentImages = {} - for index = 1, #transparentColors, 1 do - table.insert(transparentImages, createImageBytes(transparentColors[index])) - end - - -- Only discard alpha to save on unnecessary writes - local expectedPixelValues = { - "\254\000\254\000", - "\254\000\255\000", - "\254\001\254\000", - "\254\001\255\000", - "\254\002\254\000", - "\254\002\255\000", - "\254\003\254\000", - "\254\003\255\000", - "\255\000\254\000", - "\255\000\255\000", - "\255\001\254\000", - "\255\001\255\000", - "\255\002\254\000", - "\255\002\255\000", - "\255\003\254\000", - "\255\003\255\000", - } - - local expectedImages = {} - for index = 1, #expectedPixelValues, 1 do - table.insert(expectedImages, createImageBytes(expectedPixelValues[index])) - end - - local function assertRendererDiscardsTransparentPixelsOnUpload(rgbaImageBytes, expectedResult) - etrace.clear() - - Renderer:CreateTextureFromImage(rgbaImageBytes, IMAGE_WIDTH, IMAGE_HEIGHT) - local events = etrace.filter("GPU_TEXTURE_WRITE") - local payload = events[1].payload - - assertEquals(events[1].name, "GPU_TEXTURE_WRITE") - assertEquals(payload.dataSize, IMAGE_WIDTH * IMAGE_HEIGHT * 4) - assertEquals(payload.writeSize.width, IMAGE_WIDTH) - assertEquals(payload.writeSize.height, IMAGE_HEIGHT) - assertEquals(payload.writeSize.depthOrArrayLayers, 1) - - assertEquals(#payload.data, #rgbaImageBytes) -- More readable errors in case of failure - assertEquals(miniz.crc32(payload.data), miniz.crc32(expectedResult)) - - assertEquals(tonumber(events[1].payload.dataLayout.offset), 0) - assertEquals(tonumber(events[1].payload.dataLayout.bytesPerRow), IMAGE_WIDTH * 4) - assertEquals(tonumber(events[1].payload.dataLayout.rowsPerImage), IMAGE_HEIGHT) - end - - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[1], expectedImages[1]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[2], expectedImages[2]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[3], expectedImages[3]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[4], expectedImages[4]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[5], expectedImages[5]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[6], expectedImages[6]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[7], expectedImages[7]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[8], expectedImages[8]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[9], expectedImages[9]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[10], expectedImages[10]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[11], expectedImages[11]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[12], expectedImages[12]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[13], expectedImages[13]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[14], expectedImages[14]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[15], expectedImages[15]) - assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[16], expectedImages[16]) - end) end) describe("UploadMeshGeometry", function()