Add a WebGPU backend alongside WebGL2 - #31
Draft
DanKane2029 wants to merge 2 commits into
Draft
DanKane2029 wants to merge 2 commits into
DanKane2029 wants to merge 2 commits into
Conversation
Firefly's GPU handles used to live on the asset objects themselves: VertexBuffer held a WebGLBuffer and a VAO, Shader a WebGLShader, Texture a WebGLTexture, each with its own created/loaded flags. That married every mesh to exactly one WebGL context - Renderer.ts's own comments cited it as the reason a second context was rejected - and left no room for a second graphics API at all, since there is no field a WebGPU handle could share. The handles now live in a backend-owned cache (webgl/GLResourceCache.ts), keyed by the asset. Assets are pure CPU-side data, and the new RenderBackend interface is the seam a WebGPU implementation can plug into. Also here, because the interface has to accommodate both APIs honestly: - Attribute locations are pinned in attributeLocations.ts and bound before link, rather than queried per-program. A mesh's VAO no longer depends on a shader program, and WGSL's numeric @location slots can agree with GLSL's names. - pick() and readTargetPixels() are async on both backends. WebGPU can only map memory back asynchronously; WebGL resolves immediately. GizmoController awaits the pick and guards against a stale answer with a request id and a button-still-down check. - Picker.ts folds into WebGLBackend (it needed the private framebuffer), and App exposes App.pick / App.backend instead of App.picker / App.renderer. No behavior change: tsc clean, 151/151 tests pass.
Firefly can now draw the same world through either graphics API. The Renderer menu switches between them; "Automatic" prefers WebGPU and falls back to WebGL2, so a browser without WebGPU is unaffected. The new backend (src/Renderer/webgpu/) implements the same RenderBackend interface as the WebGL one, with full parity: lit and unlit shading, textures, the selection outline, GPU picking, and the Render panel's offscreen final render. Reading the two backends side by side is half the point - the same scene, expressed through a state machine and through immutable pipelines. Shaders are hand-written WGSL twins of the GLSL pair (src/Shaders/wgsl/), and a ShaderProgram now carries both languages. No transpiler: its output would be unreadable in a repo that exists to be read. Four WebGPU-specific things worth knowing, each commented where it lives: - Clip space. gl-matrix builds an OpenGL projection (z from -1 to 1); WebGPU wants 0 to 1. Unchanged, it silently clips away the near half of the view volume. CLIP_SPACE_CORRECTION fixes it and a test pins it. - Uniforms are raw bytes, so UniformLayout.ts has to agree with the WGSL structs exactly - including a vec3 taking 16 bytes and a mat3x3 taking 48. Tested directly, since a wrong offset renders wrong rather than throwing. - Cull mode is baked into a pipeline, so the outline pass is a second pipeline rather than WebGL's mid-frame cullFace flip. - A canvas's context type is permanent, so switching backends remounts the Scene panel's canvas rather than reconfiguring it. One change reaches outside the backend: every mesh now carries the same three attributes (standardVertexLayout), with zeroed UVs where there are none. WebGL tolerated a missing attribute by feeding the shader zeros; WebGPU rejects a pipeline that doesn't supply every location its shader declares. This just makes explicit what WebGL was already substituting. tsc clean, 166/166 tests pass (15 new), production build clean. Not verified in a browser - there is no headless GPU in this environment.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a WebGPU backend alongside the existing WebGL2 one — not replacing it. The same ECS world, camera, editor, and shaders' semantics; two interchangeable GPU drivers behind one interface. The Renderer menu in the nav bar switches between them, and "Automatic" prefers WebGPU with a silent fallback to WebGL2.
Targeted at
maindirectly (see CLAUDE.md's note on this repo's stacked-PR history).Two commits, reviewable in order
1.
2fbee78— Lift GPU handles off the assets, define theRenderBackendseam. No behavior change; this is the enabling refactor and the bulk of the risk.GPU handles used to live on the asset objects:
VertexBufferheld aWebGLBufferand a VAO,ShaderaWebGLShader,TextureaWebGLTexture, each withcreated/loadedflags. That married every mesh to one context —Renderer.ts's own comments cited it as why a second context was rejected — and left no field a WebGPU handle could occupy. Handles now live in a backend-owned cache keyed by the asset, so assets are pure CPU-side data.Also here, because the interface has to fit both APIs honestly:
attributeLocations.tsand bound before link, instead of queried per-program. A mesh's VAO no longer depends on any shader program, and WGSL's numeric@locationslots can agree with GLSL's names.pick()andreadTargetPixels()are async on both backends. WebGPU can only map memory back asynchronously.GizmoController.onMouseDownawaits the pick and guards a stale answer with a request id plus a button-still-down check.Picker.tsfolds intoWebGLBackend(it needed the private framebuffer).App.picker/App.renderer→App.pick/App.backend.2.
75628da— The WebGPU backend. Full parity: lit + unlit shading, textures, selection outline, GPU picking, and the Render panel's offscreen final render.The four things that bite
mat4.perspectivebuilds an OpenGL projection (z ∈ [-1,1]); WebGPU wants [0,1]. Handed over unchanged it silently clips away the near half of the view volume — reads like a broken near plane, not a coordinate convention.CLIP_SPACE_CORRECTIONfixes it, and a test pins near→0 / far→1.UniformLayout.tsmust match the WGSL structs exactly, including avec3occupying 16 bytes and amat3x3occupying 48, not 36. A wrong offset renders a subtly wrong scene rather than throwing, so this is tested directly.gl.cullFace(FRONT)mid-frame for the outline; WebGPU needs a second pipeline bound partway through the same pass.<canvas>(keyed on the backend choice) rather than reconfiguring it.One change that reaches outside the backend
Every mesh now carries the same three attributes (
standardVertexLayout), with zeroed UVs where a mesh has none. WebGL tolerated a missing attribute by feeding the shader(0,0,0,1); WebGPU rejects a pipeline that doesn't supply every location its shader declares. This makes explicit what WebGL was already substituting, and avoids compiling a shader variant per layout. It grows OBJ-loaded vertex buffers by 2 floats/vertex.Shaders are hand-written WGSL twins (
src/Shaders/wgsl/) rather than transpiled — a transpiler is a heavy build dependency whose output nobody could read, in a repo that exists to be read. A test pins the parts that can be checked without a GPU (MAX_LIGHTSacross all four declarations, attribute locations).Verification
tsc --noEmitclean (stricton)UniformLayout.test.tsandBackends.test.tsVector.test.tserror is untouched)Not verified in a browser — this environment has no GPU or headless browser, so no frame has actually been rendered through either backend on this branch. Before merging, please run
npm run start:devand check, for each backend: scene draws lit and textured; orbit; click-select; the outline reads as a rim not a solid halo; gizmo drags in all three modes; the Render panel matches the viewport minus gizmo/camera icons; resize the panel then re-pick a corner object (catches id-target resize bugs); close and reopen the Scene panel; switch backends and repeat.