# Crash BASIC 1.26.0 Release Notes

*~written by Crash-tan~*

## THE "DISSOLVE ACTUALLY DISSOLVES" RELEASE

Four connected fixes that close out the per-sprite alpha migration started in 1.25.0. If 1.25.x is when per-sprite alpha became real, 1.26.0 is when the rest of the renderer finally got the memo.

### Fixed: built-in CLS-effect shaders no longer snap straight to the target color

The renderer's shader uniform header grew from 4 f32s to 8 f32s in 1.25.0 to make room for the new `balpha` slot at byte offset 16. The user-shader compiler (`eval/shader.rs`) emitted matching WGSL — `params[0]` lives at byte offset 32 in the new layout, just past the balpha vec4. But the seven hand-written WGSL shaders in `crashgfx/src/builtin_shaders.rs` (the six CLS effects FADE / MELT / DISSOLVE / BLOCK / WIPEX / WIPEY, plus the layer wrap shader) all kept the old four-f32 header. Their WGSL still declared `params: array<vec4<f32>, 16>` immediately after `screen_height`, which means at runtime they read `params[0]` from byte offset 16 — exactly where the renderer was now writing `balpha = 1.0` every frame.

The visible symptom: `CLS 4 EFFECT(DISSOLVE) SPEED(60)` flashed to red instantly. The DISSOLVE shader read `progress = params[0].x`, which was now always 1.0, so every pixel passed the dissolve threshold on frame 1. Same root cause for all six CLS effects — WIPEX/WIPEY would jump straight to the target, FADE finished in one frame, MELT showed no displacement, BLOCK revealed everything immediately. Every transition compressed to a single instantaneous frame.

Fix is mechanical: add the same `balpha: f32` + three padding f32s between the header and the params array in every built-in shader's `Uniforms` struct. WGSL's std140 layout rules pad scalar+scalar+scalar+scalar to a vec4, so the four explicit f32s line up cleanly with the renderer's eight-f32 header. The params-array offsets match again, `progress` reads from the correct slot, and dissolves animate over their actual SPEED-defined duration.

### Fixed: layer-wrap pipeline GPU buffer matches its WGSL declaration

Same family of bug, different code path. The `Segment::WrappedLayer` rendering branch in `wgpu_renderer.rs` was hand-rolling its own uniform-data Vec — four header f32s plus 64 param f32s, totalling 272 bytes — while the layer wrap shader's WGSL had been updated to expect the full 288-byte layout (8-f32 header + 64 param f32s). On startup, wgpu's pipeline-creation path noticed the mismatch and started flooding the console with `LateMinBufferBindingSizeMismatch` validation errors:

```
the buffer bound at binding index 0 is bound with size 272 where the shader expects 288
```

The buffer-too-small validation also tripped during the warmup encoder before any user code ran. Once the validation error spam started, every wrapped-layer draw failed and the user saw a black screen for any scene using a wrapped layer.

Routed the layer-wrap branch through the same `shader_uniform_header()` helper every other built-in pipeline already uses (`balpha = 1.0` because layer-wrap rendering isn't a per-draw-alpha context). Now every built-in pipeline, the user-shader compiler, and the renderer's own `SHADER_UNIFORM_BUFFER_SIZE_BYTES` constant all reference the same source of truth — the layout can't drift again without somebody editing all four places at once.

### Fixed: per-sprite balpha now survives the per-frame full-buffer rewrite

Two functions write to the per-sprite shader uniform buffer, and they were stomping each other once per frame:

- `update_sprite_balpha` does a tiny 4-byte write at byte offset 16 to push the sprite's current alpha into the balpha slot. Called whenever a BAlpha animation step fires, a FADE pattern step bites, or `SET SPRITE n ALPHA x.x` runs.
- `update_sprite_shader_params` does a full 288-byte rewrite of the entire uniform buffer once per frame from `tick_shaders()`, refreshing `time`, `tick`, and the params array.

The full rewrite was hardcoding `balpha = 1.0` in the header — the comment on the line literally said "per-sprite alpha hookup lands in a follow-up commit." Whatever balpha value `update_sprite_balpha` had just written got clobbered on the very next frame. A BAlpha-driven fade was visible only for the single frame between the two writes (or not at all, depending on which call landed last in a given tick).

Fix introduces a `current_balpha: f32` field on `SpriteShaderState` that both writers agree on. `update_sprite_balpha` updates the cache, `update_sprite_shader_params` reads it. Default is 1.0 at sprite-shader creation time so sprites that never touch alpha pay nothing — the multiply is a no-op. Sprites that animate alpha now do so smoothly, frame-to-frame, instead of strobing.

### Fixed: WGSL pre-validated through naga before wgpu touches it

1.25.0 introduced an error-capture path that snapshotted `LAST_GPU_ERROR` before and after `device.create_shader_module()`, then refused to register the broken pipeline if the slot was populated. That works on native (where wgpu's validation is synchronous), but **not** on WebGPU — the uncaptured-error callback is a JavaScript callback that fires on the next event-loop tick. Any value read at the synchronous `create_shader_module` return site is necessarily stale. The shader got registered as "OK", subsequent pipelines cascade-failed every frame, and the console flooded with identical "Invalid RenderPipeline" lines until Chrome cut off the device.

`WgpuRenderer::create_shader` now parses+validates the WGSL through `naga` (wgpu's own shader translator, already a transitive dep at the matching version) **before** handing the source to wgpu. naga's WGSL frontend is synchronous on every backend, returns `Result<Module, ParseError>`, and the validator uses `ValidationFlags::all()` to match what wgpu uses internally. Parse errors get the source-pointing diagnostic via `err.emit_to_string(wgsl)` — the exact multi-line format wgpu logged to the console — so the dialog message reads like a familiar compile error. Validation errors include the naga error chain. Either way the function returns `Err`, the broken pipeline never enters the registry, and the existing `runtime_error("SHADER", ...)` → halt → fatal-exit chain takes over.

The post-pipeline `LAST_GPU_ERROR` check is gone — same async-callback problem applied. Pipeline-creation errors past the naga gate are rare (wgpu's lowering can still flag backend-specific issues), and a future render against a registered-but-broken pipeline falls through to the existing per-frame error suppression rather than corrupting the registry. Async-await on `device.pop_error_scope` would catch those too, but it requires making `create_shader` async through the renderer trait — meaningful surgery, deferred until needed.

### What didn't change

Every existing user shader continues to compile and run identically — the naga pre-validation step accepts everything wgpu accepts. `SET SPRITE n SHADER "name"` semantics, the way user-defined shaders compile, the `balpha` uniform-field byte offset (still 16), the runtime-error / halt / kill propagation from 1.24.4, the auto-attach passthrough shader behavior from 1.25.1, the per-sprite-alpha-on-shader-sprites infrastructure from 1.25.0. Default-rendered sprites still get the auto-attach passthrough on first sub-1.0 alpha. CLS effects with custom shader names (`CLS EFFECT("MyTransition")`) work identically to the built-in names — they always went through the user-shader-compiler path and never had the layout drift.

The one new public surface is `Renderer::has_sprite_shader(sprite_id) -> bool`, used by the auto-attach decision path to avoid double-attaching the passthrough on sprites that already have one. Trait method, no breaking change.

1277 unit tests pass unchanged.
