Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/debt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,4 @@ stay in place when retired; the log is the history).
| [output-channel-led-cap-silent-truncation](output-channel-led-cap-silent-truncation.md) | retired | 2026-05-18 | lp-fw/fw-esp32-common + fw-esp32c6 output | `MAX_LEDS = 256` per-channel cap is silent (no log, no error) and duplicated in two crates; a long strip renders truncated with no diagnostic |
| [s3-frame-cost-scales-per-fixture](s3-frame-cost-scales-per-fixture.md) | carried | 2026-07-31 | lpc-engine resolver + lpc-hardware registry | Frame cost is flat ~8.4 ms/fixture: per-frame dataflow re-resolution + per-frame endpoint-status recomputation; the shader JIT is ~1%, sends 11% |
| [c6-on-legacy-ws281x-driver](c6-on-legacy-ws281x-driver.md) | carried | 2026-07-31 | lp-fw/fw-esp32c6/src/output | C6 still runs its own single-channel WS281x driver, not `lp-ws281x`; the two now diverge in features and both hardcode `MAX_LEDS` |
| [example-shaders-not-compile-gated](example-shaders-not-compile-gated.md) | carried | 2026-07-29 | examples GLSL + CI + lps-filetests | an example shader can compile on the host yet fail on 4 of 5 targets; the break surfaces only when a human opens it in Studio |
53 changes: 53 additions & 0 deletions docs/debt/example-shaders-not-compile-gated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
status: carried
since: 2026-07-29
logged: 2026-07-29
area: examples/**/*.glsl, CI (just check / build-ci), lps-filetests
related:
- docs/defects/2026-07-29-uniform-struct-array-runtime-index.md
---
# Shipped example shaders are not compile-gated on non-host targets

**Shape** — nothing in CI compiles the GLSL under `examples/` for the
device- and browser-canonical shader targets. `examples_valid.rs` loads
every example as a project (which exercises the HOST backend only), and the
filetest suite covers `lp-shader/lps-filetests/filetests/**`, never
`examples/`. An example can therefore ship a construct that compiles on the
host and fails on `rv32n` / `rv32c` / `wasm` / `interp` — the targets that
actually run on device and in the browser sim.

The natural-looking guard does not work, which is what makes this
structural rather than one missing assertion. An engine-level render test
(load the example, tick, render, assert nonzero pixels) runs the host
backend by construction, so it cannot observe another target's lowering.
Asserting node runtime status does not help either: on the host there is no
error to report. The gate has to compile the example sources against the
other targets explicitly.

**Carrying cost** — the failure surfaces only when a human opens that
example in Studio. It presents as a runtime shader-compile error on a node
that otherwise mounted and "runs", which is easy to read as a preview
glitch rather than broken shipped content.

**Workarounds**
- When authoring example GLSL, copy the shape of an existing example that
is already known-good on device rather than inventing one; the uniform
struct-array idiom in `examples/events/shader.glsl` is the reference.
- After adding or editing an example shader, open it in the browser sim
once and read the node's status — that is currently the only end-to-end
check.

**Incident log**
- 2026-07-29 — first occurrence:
[uniform-struct-array-runtime-index](../defects/2026-07-29-uniform-struct-array-runtime-index.md).
The meteor example's render shader indexed a uniform struct array with a
runtime value; the construct failed on 4 of 5 targets while every
automated gate stayed green.

**Exit criteria** — extend the filetest runner (or add a small harness) to
compile every `examples/**/*.glsl` for `ALL_TARGETS`, run-free
(compile-only), and fail on any target that rejects a shipped example.
Compile-only keeps it cheap and needs no uniform values or expected
outputs. Note the filetest harness currently treats `compile-fail` as an
expected-failure category, so this gate must assert on it rather than
reuse that path.
62 changes: 62 additions & 0 deletions docs/defects/2026-07-29-uniform-struct-array-runtime-index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
status: fixed
found: 2026-07-29 # how: live-debugging (Yona opened the meteor sim)
area: examples/effects/meteor (render.glsl), lps-frontend lowering, example shader coverage
class: missing-coverage
related:
- docs/debt/example-shaders-not-compile-gated.md
---
# Meteor's render shader indexed a uniform struct array with a runtime value

**Symptom** — opening the imported meteor effect in the browser sim:

```
shader compile: lower: in function 'drawMeteor': unsupported expression:
AccessIndex: struct value behind Load: Access base has no uniform element addr
path /studio.show/meteor.project/render.shader
```

The effect mounted and ran; only its visual failed to compile.

**Root cause** — `drawMeteor(vec3 accum, int slot, vec2 uv)` read
`meteors[slot].…` where `slot` is a runtime parameter. A uniform array of
structs has no element address resolvable at lower time, so members must be
read through **constant** indices at the call site and passed in as
scalars/vectors. `examples/events/shader.glsl` already carries exactly that
shape (`drawEvent(color, 0, events[0].id, events[0].seq, uv)`) — the
existing example was the idiom, and the new one departed from it.

Measured breadth (filetest probe, 2026-07-29): the runtime-indexed form
**compile-fails on 4 of 5 targets** — `rv32n.q32`, `rv32c.q32`, `wasm.q32`,
`interp.f32` — and compiles only on `rv32lpn.q32`. It is broadly
unsupported, not a single-target quirk.

**Fix** — `examples/effects/meteor/meteor/render.glsl` now takes the
members as parameters (`uint id, vec2 head, vec3 color, float intensity`)
and indexes with constants at the call site. (The meteor example itself
lands with the composite-effects work, PR #218; the filetest and this
entry were harvested ahead of it.)

**Regression coverage** — the miss is the interesting part, and it was two
independent holes:

1. **The engine render test could not see it.** The new
`effect_examples_render_through_their_mirrors` (lpc-engine) renders each
effect and asserts nonzero RGB. It runs the HOST backend, whose lowering
accepts the construct — verified empirically by restoring the broken
shader, which still passed. Adding a per-node
`NodeRuntimeStatus::Error` assertion did not catch it either: on the
host path there is genuinely no error to report. An engine-level render
test is structurally incapable of gating shader-lowering support.

2. **Example shaders are not compile-gated on the other targets.** Nothing
in CI compiles `examples/**/*.glsl` for the device/browser-canonical
targets, so any example can ship a construct that fails everywhere but
the host. That condition is broader than this defect — see the debt
entry.

Added `lp-shader/lps-filetests/filetests/uniform/array-of-struct.glsl`,
which pins the SUPPORTED idiom (constant-index member reads, including
through a helper) green on all five targets. Note a negative test would not
gate anything: the filetest harness records `compile-fail` as an
*expected-failure*, not a failure.
1 change: 1 addition & 0 deletions docs/defects/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ sentence (arguments in, returns out; registers and stack).
| silent-drop | 2026-07-31 | [loader-silently-drops-unparseable-nodes](2026-07-31-loader-silently-drops-unparseable-nodes.md) | fixed | lpc-engine loader + flush + virtual ws281x |
| unbounded-restatement | 2026-07-28 | [tick-error-restated-every-frame](2026-07-28-tick-error-restated-every-frame.md) | fixed | lpa-server (advance_frame) |
| unsynchronized-shared-artifact | 2026-07-29 | [builtins-elf-uplift-race](2026-07-29-builtins-elf-uplift-race.md) | fixed | justfile `test` + lpvm-cranelift/build.rs |
| missing-coverage | 2026-07-29 | [uniform-struct-array-runtime-index](2026-07-29-uniform-struct-array-runtime-index.md) | fixed | examples/effects/meteor + lps-frontend lowering |

## Predecessor: `docs/bugs/`

Expand Down
51 changes: 51 additions & 0 deletions lp-shader/lps-filetests/filetests/uniform/array-of-struct.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// test run

// Uniform ARRAY OF STRUCTS. Distinct from `uniform/array.glsl` (uniform
// arrays of scalars/vectors, dynamically indexable) and from
// `array/of-struct/` (function-local struct arrays): a uniform struct
// array's element address must be resolvable at lower time, so members are
// read through CONSTANT indices — the idiom `examples/events/shader.glsl`
// and `examples/effects/meteor` both carry.
//
// Regression origin (2026-07-29): the meteor effect shipped a helper that
// indexed this array with a runtime parameter. It compiled on every
// Naga-frontend target and failed only on `Frontend::Lp` lowering
// ("AccessIndex: struct value behind Load: Access base has no uniform
// element addr"), so both the engine render test and CI passed while the
// browser sim failed.

struct Particle {
uint id;
vec2 pos;
vec3 color;
float intensity;
};

layout(binding = 0) uniform Particle particles[4];

// Members pass into the helper as scalars/vectors; the helper never
// indexes the uniform array itself.
vec3 accumulate(vec3 accum, uint id, vec2 pos, vec3 color, float intensity) {
if (id == 0u) {
return accum;
}
return accum + color * intensity + vec3(pos.x, pos.y, 0.0);
}

vec3 test_uniform_aos_constant_index_through_helper() {
vec3 accum = vec3(0.0, 0.0, 0.0);
accum = accumulate(accum, particles[0].id, particles[0].pos, particles[0].color, particles[0].intensity);
accum = accumulate(accum, particles[1].id, particles[1].pos, particles[1].color, particles[1].intensity);
accum = accumulate(accum, particles[2].id, particles[2].pos, particles[2].color, particles[2].intensity);
accum = accumulate(accum, particles[3].id, particles[3].pos, particles[3].color, particles[3].intensity);
return accum;
}

// run: test_uniform_aos_constant_index_through_helper() ~= vec3(0.0, 0.0, 0.0)

// Direct constant-index member reads at the top level.
float test_uniform_aos_direct_member_read() {
return particles[0].intensity + particles[3].pos.x + float(particles[2].id);
}

// run: test_uniform_aos_direct_member_read() ~= 0.0
Loading