Skip to content

v2.22.0 — PixelWater authoring and runtime

Choose a tag to compare

@apotema apotema released this 18 Sep 00:30
· 32 commits to main since this release

The engine half of COND-07 pixel water — labelle-bgfx#100, RFC at #879. One feature since v2.21.0, so a minor bump.

What landed (#880)

A typed built-in PixelWater component through the existing JSONC and .zon authoring paths — no parallel parser. Both formats call the same game.addPixelWater, so they cannot drift. Game-facing setWaterSettings / setWaterLevel / addWaterRipple, with simulation time advanced from the engine update step so pause and time scale apply (never a wall clock — deterministic tests pin it). PixelWater and its settings type are exported from src/root.zig.

Two contracts worth knowing

Bounded strength. Runtime strength is a dimensionless [0,1] scale on the authored ripple_strength_pixels (itself a nonnegative pixel amplitude). One place retunes the visual scale; a drop only reports how hard it landed; peak displacement is bounded by construction whatever a caller passes. Enforced at both boundaries — a bound on only one leaves the other as the way around it.

Stage before commit. Loader, editor and hot-reload paths validate and stage the candidate before committing either the component or the gfx instance. Committing the component first and synchronizing after leaves a torn state on rejection — the component holds the new value while gfx holds the old, and nothing can detect it because the component already looks updated.

Colour. There was no sRGB→linear helper anywhere in engine/core/gfx, so this adds the canonical one (IEC 61966-2-1). The EOTF is applied exactly once, at WaterConfig assembly. Alpha is coverage and is never gamma-decoded.

Ripple expiry, the eight-impact cap and oldest-replacement live in labelle-gfx; the engine delegates rather than duplicating the invariants.

Fixed beyond water

  • A loader crash. A wild float in any JSONC integer field — logical_size: [1e100, 18] — reached @intFromFloat before its checked cast and trapped in safety builds, terminating the load instead of reporting a malformed payload. That is shared deserializer infrastructure, so the fix benefits every component. Verified: on the pre-fix code three of the new tests abort outright.
  • An asset leak — mask/reflection references were acquired on every addPixelWater and never released.
  • Cross-world aliasing — the asset table was keyed by Entity alone, so ids colliding across independent world ECS instances shared one record, and the reaper freed shelved worlds' live references. Now scoped to World, following the existing direct_textures precedent.
  • Live prefab refresh never reached built-in PixelWater, because dispatch runs over Components.names() and built-ins are not in that registry.

Verified

1423/1423. Pixel water confirmed rendering end-to-end on real hardware — Adreno 610 / GLES 3.2 at 62 fps — and under WebGL2 on ANGLE/Metal.

Requires

labelle-gfx >= v1.36.0, labelle-core >= v1.32.0.

Known, tracked

  • #881 — comptime .zon scenes and live prefab refresh silently drop built-in Camera/Image/Emitter. The same registry-only dispatch, now seen at two independent sites.

https://claude.ai/code/session_01XBGRLojiysEr5zy1U5mbP6