fix: Emit empty fragment targets instead of [null] - #2920
Conversation
|
pkg.pr.new packages benchmark commit |
Resolution Time Benchmark---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Random Branching (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.95, 1.84, 4.37, 5.92, 7.00, 13.03, 24.67, 26.04]
line [0.98, 1.89, 4.47, 6.26, 6.96, 11.42, 23.35, 26.11]
line [0.93, 1.87, 3.98, 6.54, 7.00, 10.65, 22.93, 24.72]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Linear Recursion (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.26, 0.53, 0.71, 0.86, 1.13, 1.19, 1.47, 1.53]
line [0.31, 0.52, 0.69, 0.79, 1.06, 1.17, 1.42, 1.55]
line [0.27, 0.47, 0.67, 0.82, 1.14, 1.17, 1.37, 1.55]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Full Tree (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.79, 2.10, 3.27, 6.55, 12.18, 25.45, 54.53, 111.06]
line [0.78, 2.23, 3.26, 6.76, 11.92, 24.85, 55.30, 110.50]
line [0.87, 1.98, 3.27, 6.55, 12.67, 25.58, 54.27, 110.52]
|
Bundle size comparison (
|
| 🟢 Decreased (max -0.00%) | ➖ Unchanged | 🔴 Increased | ❔ Unknown |
|---|---|---|---|
| 21 | 304 | 0 | 0 |
import { ... } in PR vs import * as ... in PR (is the library tree-Shakeable?):
| Test | tsdown |
|---|---|
| tgpu_init.ts | 263.09 kB ( |
| tgpu_initFromDevice.ts | 262.55 kB ( |
| tgpu_resolve.ts | 161.75 kB ( |
| tgpu_resolveWithContext.ts | 161.68 kB ( |
| tgpu_bindGroupLayout.ts | 62.32 kB ( |
| tgpu_mutableAccessor.ts | 57.04 kB ( |
| tgpu_accessor.ts | 57.04 kB ( |
| tgpu_privateVar.ts | 55.73 kB ( |
| tgpu_workgroupVar.ts | 55.73 kB ( |
| tgpu_const.ts | 55.15 kB ( |
| tgpu_lazy.ts | 54.95 kB ( |
| tgpu_fragmentFn.ts | 39.68 kB ( |
| tgpu_fn.ts | 39.62 kB ( |
| tgpu_vertexFn.ts | 39.50 kB ( |
| tgpu_computeFn.ts | 39.20 kB ( |
| tgpu_vertexLayout.ts | 28.33 kB ( |
| tgpu_comptime.ts | 15.93 kB ( |
| tgpu_unroll.ts | 1.75 kB ( |
| tgpu_slot.ts | 1.70 kB ( |
If you wish to run a comparison for other, slower bundlers, run the 'Tree-shake test' from the GitHub Actions menu.
There was a problem hiding this comment.
ℹ️ The fix is correct and spec-legal — one suggestion inline.
Reviewed changes — Single commit (9f0882b) turning [null] fragment target emission into [] for shaders that write only a builtin or nothing, which Firefox rejects with Missing required 'format' member of GPUColorTargetState.
connectTargetsToShader.ts— void/builtin fragment outputs now producetargets: []instead of[null].renderPipeline.ts—connectedTargetsfalls back to[](was[null]) when the fragment output isn't resolvable.point-light-shadowexample — clear-color used to make the example work correctly.
The change is narrowly scoped and matches the underlying model: a void or builtin-only fragment (e.g. @builtin(frag_depth)) declares no color outputs, so [] is not merely spec-legal but the semantically accurate descriptor. The struct branch that mixes builtins with color outputs is untouched, so pipelines writing both depth and color keep their real targets.
Technical details
# Suggestion: add a regression test
The two changed pipelines reach the `typeof navigator === 'undefined'`-safe
branches before any `navigator.gpu` access, so `connectTargetsToShader(void|builtin)`
can be unit-tested without a device — it returns before the `getPreferredCanvasFormat`
call. Consider pinning the actual regression: a builtin-only fragment (or void) should
yield `fragment.targets: []`, and a struct mixing color + `@builtin(frag_depth)` should
still yield the color targets. Without a test, this subtle spec-compat behavior can
silently regress back to `[null]`.DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
|
|
||
| if (isVoid(fragmentOut) || isBuiltin(fragmentOut)) { | ||
| return [null]; | ||
| return []; |
There was a problem hiding this comment.
Minor: worth a regression test pinning targets: [] here? This branch returns before any navigator.gpu access, so connectTargetsToShader(voidOrBuiltin, ...) is unit-testable without a device. The exactly regression — a builtin-only fragment (e.g. @builtin(frag_depth)) yielding targets: [] instead of [null] — would otherwise be easy to silently revert, since the unmanifested difference only shows up on Firefox (null rejects format).

Fragment shaders that return only a builtin e.g (
out: d.builtin.fragDepth) now create pipelines withfragment.targets: []instead of[null].Both ways are spec-legal, but Firefox rejects
[null]withMissing required 'format' member of GPUColorTargetState, so the pipeline never ran.Due to this bug
Point Light Shadowexample now clears the color attachment to[0, 0, 0, 1].