Skip to content

Commit

Permalink
Perf: Replace CPU texture pre-processing with shader logic
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rdw-software committed Feb 6, 2024
1 parent 8ff8e22 commit 5ad965e
Show file tree
Hide file tree
Showing 3 changed files with 10 additions and 142 deletions.
47 changes: 0 additions & 47 deletions Core/NativeClient/Renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
11 changes: 10 additions & 1 deletion Core/NativeClient/WebGPU/Shaders/TerrainGeometryShader.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
94 changes: 0 additions & 94 deletions Tests/NativeClient/Renderer.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -57,100 +57,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()
Expand Down

0 comments on commit 5ad965e

Please sign in to comment.