@codexo/exojs 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +585 -499
  2. package/README.md +156 -156
  3. package/dist/esm/core/Application.d.ts +8 -0
  4. package/dist/esm/core/Application.js +17 -0
  5. package/dist/esm/core/Application.js.map +1 -1
  6. package/dist/esm/core/capabilities.d.ts +38 -0
  7. package/dist/esm/core/capabilities.js +198 -0
  8. package/dist/esm/core/capabilities.js.map +1 -0
  9. package/dist/esm/core/index.d.ts +1 -0
  10. package/dist/esm/index.js +1 -0
  11. package/dist/esm/index.js.map +1 -1
  12. package/dist/esm/rendering/webgl2/glsl/mask-compose.frag.js +1 -1
  13. package/dist/esm/rendering/webgl2/glsl/mask-compose.vert.js +1 -1
  14. package/dist/esm/rendering/webgl2/glsl/particle.frag.js +1 -1
  15. package/dist/esm/rendering/webgl2/glsl/primitive.frag.js +1 -1
  16. package/dist/esm/rendering/webgl2/glsl/primitive.vert.js +1 -1
  17. package/dist/esm/rendering/webgl2/glsl/sprite.frag.js +1 -1
  18. package/dist/esm/rendering/webgpu/WebGpuBackend.js +41 -41
  19. package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -1
  20. package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js +44 -44
  21. package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js.map +1 -1
  22. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js +65 -65
  23. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js.map +1 -1
  24. package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.js +25 -25
  25. package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.js.map +1 -1
  26. package/dist/esm/vendor/webgl-debug.js +1156 -1156
  27. package/dist/esm/vendor/webgl-debug.js.map +1 -1
  28. package/dist/exo.esm.js +1550 -1338
  29. package/dist/exo.esm.js.map +1 -1
  30. package/package.json +105 -105
package/CHANGELOG.md CHANGED
@@ -1,499 +1,585 @@
1
- # Changelog
2
-
3
- All notable changes to ExoJS are documented in this file.
4
-
5
- The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
-
7
- ## [0.6.2] - 2026-05-02
8
-
9
- Adds the `Mesh` primitive the first new public Drawable since the
10
- 0.6.0 cleanup. PATCH bump because the only change is additive: a new
11
- class plus its two backend renderers; nothing existing changes shape.
12
-
13
- ### Added
14
-
15
- - **`Mesh` Drawable.** Arbitrary 2D triangle-mesh primitive sitting
16
- alongside `Sprite` in the Drawable hierarchy. Construction takes a
17
- `MeshOptions` object with required `vertices` (flat (x,y) pairs) and
18
- optional `indices`, `uvs`, `colors` (packed RGBA8 u32 per vertex),
19
- and `texture`. Mesh data is immutable post-construction, but the
20
- underlying typed arrays may be mutated in place — call
21
- `mesh.recomputeLocalBounds()` afterwards to keep culling correct.
22
- Validation is enforced at construction (mismatched array lengths,
23
- out-of-range indices, non-multiple-of-3 vertex/index counts all
24
- throw).
25
- - **`WebGl2MeshRenderer`.** Single-drawcall-per-mesh path on WebGL2.
26
- Vertex layout is 20 bytes (pos f32x2 + uv f32x2 + color u8x4-norm).
27
- Texture is bound to slot 0; meshes without an explicit texture
28
- resolve to `Texture.white` so the fragment shader stays branchless.
29
- - **`WebGpuMeshRenderer`.** Deferred batched-pass path on WebGPU. CPU
30
- bakes (view × globalTransform) into vertex positions so the WGSL is
31
- uniform-free except for a per-mesh dynamic-offset tint+flags slot.
32
- Pipelines are created per (blendMode × format) and pre-warmed via
33
- `prewarmPipelines` during backend init. Texture bind groups are
34
- cached per Texture/RenderTexture instance.
35
- - **Three live examples** under `examples/public/examples/rendering/`:
36
- `mesh-triangle.js` (untextured, vertex-colored), `mesh-textured-quad.js`
37
- (textured quad equivalent to a Sprite, hand-built from a Mesh), and
38
- `mesh-deformed-grid.js` (16×16 grid whose vertex positions wave
39
- each frame — demonstrates the deformation use case Sprite can't
40
- handle).
41
-
42
- ## [0.6.1] - 2026-05-02
43
-
44
- Playground-only release. Library code is unchanged from 0.6.0; the
45
- npm tarball ships byte-for-byte the same `dist/` output. The version
46
- bump exists so the published changelog and the playground's release
47
- catalog stay in sync.
48
-
49
- ### Changed
50
-
51
- - **Playground version selector now reads GitHub Releases at runtime.**
52
- The dropdown was previously fed by a committed `versions.json` plus
53
- per-version snapshot directories under
54
- `examples/public/examples/versions/<id>/` and
55
- `examples/public/vendor/exojs/<id>/`. Both are gone. The dropdown
56
- now fetches from the GitHub Releases API
57
- (`api.github.com/repos/Exoridus/ExoJS/releases`); the special
58
- "current" entry continues to load locally-vendored sources for the
59
- build-time HEAD. Example sources for any released version load
60
- from `raw.githubusercontent.com/Exoridus/ExoJS/v<id>/...` and the
61
- library bundle loads from `cdn.jsdelivr.net/npm/@codexo/exojs@<id>`.
62
- Versions appear in the dropdown automatically once a tag is
63
- published no bookkeeping commit is needed any more.
64
-
65
- ### Removed
66
-
67
- - **Versioned-snapshot scaffolding in the playground.** The
68
- `examples/public/examples/versions/` snapshot tree, the
69
- per-version `examples/public/vendor/exojs/<id>/` mirrors, and
70
- `examples/public/examples/versions.json` are all gone, along with
71
- the `phase2-bundle.smoke.test.mjs` smoke test that policed their
72
- byte-identical layout. The `versions.json` shape test in
73
- `phase1-bundle.smoke.test.mjs` is also gone. `sync-exo-vendor.ts`
74
- no longer mirrors the flat vendor into a versioned subdirectory.
75
-
76
- ## [0.6.0] - 2026-05-02
77
-
78
- A large pre-1.0 cleanup release. Two intentional API breaks (Backend
79
- rename, Scene class-only), a full GPU-instancing pass across sprite
80
- and particle renderers on both backends, and a slimmer npm package
81
- shape. All on a single 0.x minor since the project is still pre-1.0
82
- and breaks freely between minors.
83
-
84
- ### Breaking
85
-
86
- - **`Runtime` types renamed to `Backend`; render-manager classes
87
- collapse into the same name.** `SceneRenderRuntime`
88
- `RenderBackend`. The split `WebGl2RendererRuntime` /
89
- `WebGpuRendererRuntime` interfaces are gone the concrete classes
90
- are the public type. `WebGl2RenderManager` `WebGl2Backend`,
91
- `WebGpuRenderManager` → `WebGpuBackend`. `Application.renderManager`
92
- → `Application.backend`. Internal field/parameter names follow
93
- (`runtime` `backend`, `_runtime` → `_backend`, `getRuntime()` →
94
- `getBackend()`). `WebGl2ShaderRuntime` → `WebGl2ShaderProgram` (the
95
- type stores a `WebGLProgram` plus its bound state — the new name
96
- reflects that). `WebGl2RenderBufferRuntime` and
97
- `WebGl2VertexArrayObjectRuntime` keep their names they describe
98
- per-resource lifecycle, not the render backend.
99
- - **`Scene` is class-only; the plain-object definition constructor is
100
- gone.** `new Scene({ update() { ... } })` no longer works. Subclass
101
- to define a scene `class GameScene extends Scene { override
102
- update(...) { ... } }` for named scenes, `new class extends Scene
103
- { ... }` for one-offs. The `SceneData` interface and
104
- `SceneInstance<T>` type alias are removed (they only existed to
105
- type the spread-into-`this` constructor). Internal Scene fields
106
- move from ECMAScript `#`-private to TS `protected _app/_root/
107
- _stackMode/_inputMode` subclasses can now reach internal state
108
- directly when they need to.
109
- - **npm package shape simplified.** Dropped: `dist/exo.global.js` /
110
- `dist/exo.global.min.js` (legacy IIFE for `<script>` use) and
111
- `dist/exo.esm.min.js` (consumers minify on their side). What ships
112
- now: `dist/esm/` (per-module ESM tree, the canonical entry) and
113
- `dist/exo.esm.js` (single-file ESM bundle for direct module
114
- loading). `package.json#main`, `module`, `browser`, `exports` are
115
- unchanged in semantics only the auxiliary artifacts go away.
116
-
117
- ### Performance
118
-
119
- - **WebGL2 sprite renderer is now fully GPU-instanced.** Quad
120
- corners derive from `gl_VertexID` in the vertex shader; per-instance
121
- attributes carry `localBounds`, `transformAB`/`transformCD` (the 2D
122
- affine), `uvBounds`, packed RGBA8 tint, and packed slot/flags (56
123
- bytes per instance). The CPU per-frame cost is one bounded
124
- `writeBuffer` per batch; no per-vertex stream is uploaded.
125
- `drawArraysInstanced` over `TRIANGLE_STRIP` replaces the per-vertex
126
- `drawElements` path.
127
- - **WebGPU sprite renderer matches the same instanced layout.** Uses
128
- `drawIndexed` over a static `[0,1,2,0,2,3]` index buffer with
129
- `triangle-list` topology (the index buffer keeps mock-frame
130
- bookkeeping deterministic the on-screen result is the same as a
131
- triangle-strip).
132
- - **Particle renderers fully instanced on both backends, with system
133
- data hoisted out of per-instance.** `localBounds`, `uvBounds`, and
134
- `systemTransform` are now uniforms (one upload per system per
135
- frame). Per-instance shrinks from 56 to 24 bytes (translation,
136
- scale, rotation, packed RGBA8 color). `WebGl2ParticleRenderer` no
137
- longer extends `AbstractWebGl2BatchedRenderer` particles don't
138
- share batch infrastructure with sprites anymore.
139
-
140
- ### Removed
141
-
142
- - `docs/` directory and the README's "Next Steps" link block. The
143
- prose docs were drifting out of sync with the code; the in-repo
144
- examples (`examples/README.md`) remain the supported reference.
145
- - `SceneRenderRuntime`, `WebGl2RendererRuntime`, `WebGpuRendererRuntime`
146
- interfaces (collapsed into the renamed classes — see Breaking).
147
- - `SceneData` interface, `SceneInstance<T>` type alias (no longer
148
- needed without the Scene definition-spread constructor).
149
- - `WebGl2RenderManager`, `WebGpuRenderManager` class names (renamed
150
- to `*Backend` — see Breaking).
151
- - `Sampler._premultiplyAlpha`, `Sampler._generateMipMap`,
152
- `Sampler._flipY` (write-only — texture pixel-store path consumes
153
- these directly from `SamplerOptions`, the GL sampler object only
154
- cares about scale and wrap modes).
155
- - `AudioAnalyser._audioContext` (write-only — never read after
156
- setup).
157
- - `WebGpuRenderManager._blendMode` (write-only renderers consult
158
- `sprite.blendMode` directly; `setBlendMode` keeps its
159
- not-yet-implemented blend-mode validation).
160
- - `@rollup/plugin-terser` devDependency (no minified bundle output
161
- any more).
162
-
163
- ### Migration
164
-
165
- ```ts
166
- // Before (0.5.x)
167
- class GameScene extends Scene {
168
- override draw(runtime: SceneRenderRuntime): void {
169
- this.root.render(runtime);
170
- }
171
- }
172
-
173
- const triangleRenderer = new CustomRenderer(app.renderManager);
174
-
175
- if (app.renderManager instanceof WebGpuRenderManager) { /* ... */ }
176
-
177
- // Plain-object scene
178
- app.start(new Scene({ update() { /* ... */ } }));
179
- ```
180
-
181
- ```ts
182
- // After (0.6.0)
183
- class GameScene extends Scene {
184
- override draw(backend: RenderBackend): void {
185
- this.root.render(backend);
186
- }
187
- }
188
-
189
- const triangleRenderer = new CustomRenderer(app.backend);
190
-
191
- if (app.backend instanceof WebGpuBackend) { /* ... */ }
192
-
193
- // Anonymous-subclass scene (or named subclass)
194
- app.start(new class extends Scene { override update() { /* ... */ } });
195
- ```
196
-
197
- ## [0.5.1] - 2026-04-28
198
-
199
- Rendering-pipeline performance pass. No public API changes; all
200
- optimisations are internal to the renderer subsystem.
201
-
202
- ### Changed
203
-
204
- - **WebGL2 sprite batching is now multi-texture.** A single batch can
205
- bind up to eight textures (units 0..7); each vertex carries a uint
206
- texture-slot attribute and the fragment shader's per-slot if-chain
207
- selects the right sampler. Previously every texture change forced a
208
- flush, capping multi-atlas scenes at roughly one batch per texture.
209
- The vertex stride grows from 16 to 20 bytes (the new u32 slot at
210
- offset 16 is the only addition); position, packed UV, and packed
211
- RGBA8 tint are unchanged. Batches still flush on buffer-full,
212
- blend-mode change, and now slot exhaustion (more than eight
213
- textures in one batch).
214
- - **WebGPU sprite vertex layout compacted from 28 to 24 bytes.** The
215
- per-vertex `premultiplyAlpha` flag and `textureSlot` index
216
- previously took one u32 attribute each; they are now packed into a
217
- single u32 with the slot in bits 0..7 and the flag in bit 8. The
218
- WGSL vertex shader unpacks via bit ops. 16 bytes saved per sprite.
219
- - **Async-compile path now syncs the shader between buffer setup and
220
- attribute lookup.** The 0.5.0+slice-C deferral of attribute /
221
- uniform extraction from `initialize()` to first `sync()` broke
222
- connect-time `getAttribute()` callers under a real WebGL2 context
223
- (jest mocks didn't exercise that code path). Fixed in
224
- `AbstractWebGl2BatchedRenderer`, `WebGl2PrimitiveRenderer`, and
225
- `WebGl2MaskCompositor`. The driver still gets a parallel-compile
226
- window between `shader.connect()` and `shader.sync()` thanks to
227
- KHR_parallel_shader_compile; the eventual blocking status query is
228
- a no-op when compile already finished.
229
-
230
- ### Added
231
-
232
- - **`WebGl2SpriteRenderer.prewarmPipelines` equivalent for WebGPU.**
233
- `WebGpuSpriteRenderer.prewarmPipelines(formats)` calls
234
- `createRenderPipelineAsync` for every BlendMode × format combo in
235
- parallel during render-manager init. The first draw of every common
236
- blend mode no longer blocks on synchronous pipeline creation.
237
- Renderers without a `prewarmPipelines` method continue to create
238
- pipelines lazily on first use; the pre-warm fallback gracefully
239
- no-ops when `createRenderPipelineAsync` isn't available (older
240
- browsers, headless test mocks).
241
- - **`KHR_parallel_shader_compile` opt-in for WebGL2 shader compile.**
242
- When the extension is present (Chrome / Edge / Firefox by default,
243
- Safari since 17) the GL driver may compile shaders on a worker
244
- thread; status queries are deferred to the first `sync()` call so
245
- the main thread doesn't block on compile.
246
- - **`ShaderPrimitives.UnsignedInt`, `UnsignedIntVec2..4`** with their
247
- byte-size and array-constructor mappings, so `getActiveAttrib` /
248
- `getActiveUniform` on a `uint` shader slot resolves correctly. The
249
- enum gains four members; the runtime export inventory is unchanged.
250
- - **`WebGl2VertexArrayObject.addAttribute(..., integer)`** parameter
251
- routes integer-typed shader inputs (`uint`, `uvec`) to
252
- `vertexAttribIPointer` rather than `vertexAttribPointer`, so the
253
- shader receives the raw integer value instead of a coerced float.
254
- - **`RendererRegistry.renderers()`** iterator exposes the registered
255
- renderers so backend managers can dispatch optional lifecycle hooks
256
- (such as the WebGPU pipeline pre-warm above) without per-renderer
257
- private-field reach-ins.
258
-
259
- ### Performance notes
260
-
261
- - Sprite-heavy scenes with multiple atlases see a draw-call reduction
262
- proportional to atlas count (up to 8×) on WebGL2.
263
- - WebGPU sprite vertex bandwidth is reduced 14% (16 bytes per sprite).
264
- - First-frame stutter from JIT shader / pipeline compilation is
265
- largely eliminated when KHR_parallel_shader_compile (WebGL2) or
266
- `createRenderPipelineAsync` (WebGPU) is supported.
267
-
268
- ## [0.5.0] - 2026-04-28
269
-
270
- Three focused breaking changes targeted at the first pre-1.0 minor: a hierarchy-semantics boundary slice (per `.workspace/reviews/opus-pre-1.0-architecture-review/09-b1-implementation-rfc.md`), a unified mask API with full multi-source support (per `.workspace/reviews/opus-pre-1.0-architecture-review/10-mask-api-decision.md`), and a Scene API simplification that collapses the static factory into the constructor. No aliases.
271
-
272
- ### Removed
273
-
274
- - **`Transformable` class and `TransformableFlags` enum.** Inlined into `SceneNode`. `SceneNode` now owns its transform fields and accessors (`position`, `x`, `y`, `rotation`, `scale`, `origin`, `setPosition`, `setRotation`, `setScale`, `setOrigin`, `move`, `rotate`, `getTransform`, `updateTransform`, `flags`) directly. The public surface shrinks by two symbols. `Flags<T>` (the generic class) remains public.
275
- - **`SceneNode.render(runtime)` no-op.** Render belongs to `RenderNode` and below; bare `SceneNode` no longer pretends to participate in the render pass.
276
- - **`Scene.create(definition)` static factory.** Replaced by a typed constructor overload — see Changed below.
277
-
278
- ### Changed
279
-
280
- - **`RenderNode.render(runtime)` is now `abstract`.** All concrete subclasses (`Drawable`, `Container`, `Graphics`, `Sprite`, `AnimatedSprite`, `Text`, `Video`, `ParticleSystem`, `DrawableShape`) already implement it. The abstract declaration removes the SceneNode-render lie.
281
- - **`RenderNode.mask` is now the unified visual masking API**, accepting any `MaskSource = Rectangle | Texture | RenderTexture | RenderNode | null`. The behavior depends on the source:
282
- - `Rectangle` — fast axis-aligned scissor clip (O(1) GPU state). The most common case for UI panels and viewport regions.
283
- - `Texture` / `RenderTexture` — uses the texture's alpha channel as the mask, stretched to fit the masked node's local bounds. The texture has no transform of its own; for transform/scale/rotation control over the mask source, use a `Sprite(texture)` instead.
284
- - `RenderNode` (`Sprite`, `Graphics`, `Container`, etc.) — the node's full visual output (with its own transform, filters, cacheAsBitmap) is rendered into an intermediate render texture and used as the alpha mask. Bare `SceneNode` instances are rejected at compile time because they are structural-only.
285
- - `null` no mask.
286
-
287
- Setting `node.mask = node` (self-mask) throws at runtime.
288
- - **`SceneRenderRuntime` mask primitives renamed** to match the new vocabulary:
289
- - `pushMask(maskBounds)` / `popMask()` → `pushScissorRect(bounds)` / `popScissorRect()` (lower-level scissor primitive used internally by the `Rectangle` mask path).
290
- - New `composeWithAlphaMask(content, mask, x, y, width, height, blendMode)` used internally by the Texture/RenderTexture/RenderNode mask paths.
291
- - Backend implementations: `WebGl2MaskCompositor` (new) and `WebGpuMaskCompositor` (new) implement the alpha-compose pipeline. Each owns its own shader/pipeline, lazily initialized on first use, disconnected on manager destroy. Pipelines are cached per (target format, blend mode) on the WebGPU side.
292
- - **`Container._children` narrowed to `Array<RenderNode>`.** `addChild`, `addChildAt`, `removeChild`, `swapChildren`, `getChildIndex`, `setChildIndex`, `getChildAt`, and `Scene.addChild`/`removeChild` now require `RenderNode` instances. Bare `SceneNode` instances cannot be added to a container at compile time. (Previous behavior added them as no-op render nodes; observable behavior was unchanged for any code that already added Drawable/Container/Graphics/Sprite/etc.)
293
- - **`Scene` is now generic and constructable with an optional typed `SceneData` definition.** `class Scene<T extends SceneData = SceneData>` — `new Scene()` produces an empty scene; `new Scene({ update() { ... }, draw() { ... } })` accepts a typed definition object whose method bodies see `this` as `Scene<T> & T` via `ThisType<>`. `class extends Scene` is unchanged and remains the recommended path for stateful scenes — TypeScript only infers properties declared inside the definition object, so `this._foo = ...` assignments inside method bodies are still invisible to the type system without pre-declaration. The existing `SceneInstance<T>` type alias keeps its meaning (`Scene<T> & T`) and is still re-exported from the package root.
294
-
295
- ### Added
296
-
297
- - **`MaskSource` type alias** is exported from the package root: `Rectangle | Texture | RenderTexture | RenderNode | null`. This is the public type for `RenderNode.mask`.
298
- - **Root export runtime snapshot gate** (`test/core/root-index-snapshot.test.ts`). Captures every runtime-visible export name from `src/index.ts` and compares against a committed Jest snapshot. CI fails on any unintentional addition or removal.
299
- - **Root export type-level inventory** (`test/core/root-index-type-inventory.test.ts`). Enumerates all exported symbols — including interfaces and type aliases erased at runtime — with their kind annotations.
300
- - **RenderNode/SceneNode contract tests** (`test/rendering/render-node.test.ts`). Pin down the `SceneNode` is structural-only / `RenderNode.render` is abstract / `Container.addChild` rejects non-`RenderNode` contracts.
301
- - **MaskSource union tests** (`test/rendering/mask-source.test.ts`). 12 tests covering: Rectangle scissor routing, nested rectangles, zero-size and null masks; Texture / RenderTexture / Sprite / Graphics / Container as alpha-mask sources; bare `SceneNode` rejected at compile time; self-mask rejected at runtime; mask reassignment to null.
302
-
303
- ### Migration
304
-
305
- | Before (0.4.x) | After |
306
- |---|---|
307
- | `import { Transformable } from '@codexo/exojs'`; `class X extends Transformable` | `import { SceneNode } from '@codexo/exojs'`; `class X extends SceneNode` |
308
- | `import { TransformableFlags } from '@codexo/exojs'` | Internal flag enum is no longer public; use SceneNode's high-level transform accessors instead. |
309
- | `node.mask = anyShapeNode` *(silently clipped to bounding rect)* | `node.mask = anyShapeNode` *(now a real shape mask via alpha compositing — except bare SceneNode which is rejected at compile time)* |
310
- | Want fast axis-aligned clipping? | `node.mask = new Rectangle(x, y, w, h)` |
311
- | Want to clip with a texture's alpha channel? | `node.mask = texture` or `node.mask = renderTexture` |
312
- | Want a transformed/positioned alpha mask? | `node.mask = new Sprite(texture)` (Sprite's transform/position/scale apply to the mask source) |
313
- | `runtime.pushMask(rect)` / `runtime.popMask()` | `runtime.pushScissorRect(rect)` / `runtime.popScissorRect()` (renamed; behavior unchanged) |
314
- | `class Group extends SceneNode { override render() {...} }` | `class Group extends RenderNode { override render() {...} }` |
315
- | `class CustomContainer extends Container { override addChild(child: SceneNode) {...} }` | `class CustomContainer extends Container { override addChild(child: RenderNode) {...} }` |
316
- | `Scene.create({ update() {...} })` | `new Scene({ update() {...} })` (drop-in replacement; same `this` typing via `ThisType<Scene & T>`) |
317
- | `Scene.create({})` | `new Scene()` |
318
-
319
- No deprecated aliases are provided. The migration is mechanical and the project is pre-1.0 with explicit "may break between minors" policy.
320
-
321
- ### Modernized
322
-
323
- Quality-of-life cleanups using ES2022+ features. No public-API impact, but flagged here for transparency:
324
-
325
- - **`Scene` uses ECMAScript `#` private fields** (`#app`, `#root`, `#stackMode`, `#inputMode`) instead of TypeScript `private _xxx`. True runtime privacy — fields are unreachable from outside the class even via bracket notation. The rest of the codebase still uses `private _xxx`; full sweep is queued for a future release pending test refactor (existing tests reach into private state via `obj['_field']`, which `#` fields block).
326
- - **`Loader.ts` uses `Object.hasOwn(obj, key)`** instead of `Object.prototype.hasOwnProperty.call(obj, key)`. Same semantics, less ceremony.
327
- - **`SceneManager` uses `array.at(-1)`** for stack-tail access instead of `arr[arr.length - 1]`. Three sites: the active-scene getter, `popScene`, and `_unloadCoveredScenes`.
328
- - **`Loader.ts` uses `Error.cause`** for the wrapped error in `factory.create()` failures. `cause` carries the full original error (with stack trace) so DevTools, Sentry, etc. surface the underlying cause automatically. The wrapper message still contains the inner message for backward compatibility with consumers that string-match the error message.
329
-
330
- ### Performance notes
331
-
332
- - `mask = Rectangle` is O(1) GPU scissor — free at scale.
333
- - `mask = Texture` / `mask = RenderTexture` adds one intermediate render texture acquire and one composite pass per masked render.
334
- - `mask = RenderNode` adds a second intermediate render texture acquire (to bake the mask node's visual output) plus the composite pass — so two extra passes per masked render. Use sparingly for high-frequency draws; consider `cacheAsBitmap` on the masked content.
335
-
336
- ### Notes
337
-
338
- - The single dominant import model is intentional: `import { Application, Sprite } from '@codexo/exojs'` and `import * as Exo from '@codexo/exojs'` align with the IIFE/global bundle (`Exo.Application`, `Exo.Sprite`). Subpath exports are deferred until a stable API boundary warrants them.
339
- - `SceneNode` is now a concrete structural class — transform, hierarchy, collision, culling. `RenderNode` (abstract) is the render-capable base. Every render-participating class extends `RenderNode`; bare `SceneNode` instances are valid as user-defined data nodes but cannot be added to containers.
340
-
341
- ## [0.4.0] - 2026-04-26
342
-
343
- Pre-1.0 versioning reset. The active development line moves from `2.1.2` to `0.4.0` to honestly reflect that the public API is not yet stable. No runtime behavior change relative to the previous head — this release marks a versioning policy shift, not a code rewrite.
344
-
345
- ### Notes
346
-
347
- - The `2.x` releases (`2.0.0`, `2.1.0`, `2.1.1`, `2.1.2`) remain published on npm as a historical line and will be deprecated with a pointer to the `0.x` line.
348
- - New work happens on the `0.x` line. Expect breaking changes between `0.x` minors as the scene graph, renderer, and resource boundaries continue to evolve.
349
- - `1.0.0` will mark the first stable public API contract. Until then, treat any minor version as potentially breaking and pin exact versions in downstream experiments.
350
- - Current package identity for the reset line is `@codexo/exojs`. Historical `2.x` release notes may reference the legacy package/import name, old example layout, old scripts, or the former `master` branch target.
351
- - The `2.1.0` View camera note below used the old working name `setBoundsConstraint`; the current API is `setBounds(...)` / `clearBounds()`.
352
- - Past CHANGELOG entries for `2.x` are otherwise preserved below as the historical record of work that landed in those releases.
353
-
354
- ## [2.1.2] - 2026-04-19
355
-
356
- Patch release with one runtime fix, a toolchain modernization pass, and a legacy-artifact cleanup. No public API removals or renames.
357
-
358
- ### Fixed
359
-
360
- - **`Signal.dispatch` skipped sibling `once()` handlers.** `once()` wrappers self-remove mid-iteration, which compacts the underlying bindings array; the `for..of` iterator then advanced past the binding that shifted into the just-visited slot. `dispatch` now iterates a snapshot of bindings, so handler-driven mutation is safe. Visible symptom: the Audio Visualisation example received a set-up `Music` but an un-set-up `AudioAnalyser`, so frequency buffers stayed at zero.
361
-
362
- ### Changed
363
-
364
- - Removed the legacy bundled declaration file `dist/exo.d.ts` (emitted via `tsc --outFile` + `module: amd`, both deprecated in TypeScript 6). Modern consumers resolve types through `exports["."].types`, which points at the per-file tree in `dist/esm/`; `dist/exo.d.ts` was never part of the `exports` map. This also removes the `ignoreDeprecations: "6.0"` escape hatch from the build.
365
- - Build upgraded to TypeScript 6, ESLint 10, Jest 30. Internal imports now use the `@/*` path alias (mapped to `src/*`) and `baseUrl` is no longer required.
366
-
367
- ## [2.1.1] - 2026-04-19
368
-
369
- Patch release fixing a cluster of WebGPU and scene-graph bugs discovered after 2.1.0 shipped. No public API removals or renames; one backward-compatible addition on `Container.addChild`.
370
-
371
- ### Fixed
372
-
373
- - **WebGPU adapter ordering.** `WebGpuRenderManager` now requests the GPU adapter before acquiring the canvas WebGPU context. A null adapter previously locked the canvas into WebGPU mode, preventing `Application`'s automatic WebGL2 fallback from obtaining a context on the same element.
374
- - **WebGL2 shader program binding.** `WebGl2ShaderRuntime.sync()` now binds the program before writing uniforms. The previous draw pipeline never called `bindShader(shader)` with a non-null shader, so every `uniform*` write targeted the wrong or null program and `drawElements` reported "no valid shader program in use". Exposed by the WebGPU adapter fallback above.
375
- - **WGSL multi-texture sprite shader** uses `textureSampleGrad` with explicit screen-space derivatives. `textureSample`'s uniformity requirement prevented the 8-slot dispatch from compiling on any sprite batch spanning more than one texture slot.
376
- - **Sprite index buffer** allocation and lifecycle. Buffer size was 4× larger than intended (`indexData.byteLength * BYTES_PER_ELEMENT` instead of `indexData.byteLength`), and `_ensureBatchCapacity` ran inside the draw loop and could destroy a buffer the render pass had already bound. Capacity is now grown once up front.
377
- - **Sprite multi-batch rendering.** When a flush contained multiple batches (blend-mode change, texture-slot overflow, or pipeline switch), each batch's `queue.writeBuffer(vertexBuffer, offset: 0, ...)` serialised before the single submit, leaving only the last batch's vertex data in the buffer. All batch vertex data is now packed into one CPU buffer at distinct sprite offsets and uploaded once; `drawIndexed` uses `firstIndex` to target each range.
378
- - **Particle and primitive multi-drawcall rendering.** Same multi-write-to-offset-0 pattern, plus mid-loop `_ensureCapacity` destroying buffers still referenced by the pass. Particle renderer now submits one command buffer per system. Primitive renderer was rewritten: CPU bakes `view * globalTransform` into `vec4` clip-space positions per vertex, pipeline has no bind-group, one render pass per flush with packed vertex/index buffers.
379
- - **Primitive combine order.** `_combinedTransform.copy(view).combine(global)` produced `global * view` (`Matrix.combine` applies the argument on the left, confirmed by `SceneNode.getGlobalTransform` which chains `local.combine(parent.global)` to yield `parent.global * local`). Swapped to `copy(global).combine(view)` = `view * global`.
380
- - **WebGPU mipmap generation.** The full-screen downsample triangle's UVs are no longer Y-flipped relative to framebuffer orientation. Every odd mip level was being rendered upside-down, producing a visible sprite flip whenever the view zoomed far enough for the LOD selector to cross an odd/even boundary.
381
-
382
- ### Added
383
-
384
- - `Container.addChild` accepts multiple children via rest args (`addChild(...children)`). The previous single-argument signature silently dropped the tail of `addChild(a, b, c, d)`; callers only saw `a` in the scene graph. Single-child usage stays backward compatible.
385
- - Doc comment on `ParticleOptions.position` clarifying it is in the owning `ParticleSystem`'s local coordinate space. The shader applies the system's global transform on top, so passing world coordinates double-translates the emitter.
386
-
387
- ## [2.1.0] - 2026-04-18
388
-
389
- Product-readiness release. Additive across assets, game-feel, visuals, performance, optional physics, and WebGPU parity. No public contracts were removed or renamed since v2.0.0.
390
-
391
- ### Highlights
392
-
393
- - Typed asset manifests and bundle loading workflow.
394
- - `AnimatedSprite` with named clips, loop control, and frame signals.
395
- - Scene stacking with participation policies, input routing, and fade transitions.
396
- - View/camera polish: follow with lerp, bounds clamp, zoom, shake.
397
- - Audio sprites and sound pooling.
398
- - Visual capability wave: filter pipeline, masking, render passes, cache-as-bitmap, multi-texture batching on the WebGPU backend.
399
- - Automatic off-screen culling with observable render stats.
400
- - Optional Rapier physics integration behind an optional peer dependency.
401
- - WebGPU parity improvements and clearer initialization failure semantics.
402
- - Docs and examples overhaul; release verification hardening.
403
-
404
- ### Assets / workflow
405
-
406
- - `defineAssetManifest`, `AssetEntry`, and `loadBundle` with progress callbacks.
407
- - `BundleLoadError` surfaces per-entry failures with the responsible loader token.
408
- - Strict manifest validation runs at definition time.
409
- - `CacheStore` + `IndexedDbStore` remain the persistence path; strategy classes (`CacheFirstStrategy`, `NetworkOnlyStrategy`) are exposed for custom pipelines.
410
-
411
- ### Game-feel
412
-
413
- - `AnimatedSprite`: `defineClip`, `setClips`, `play`, `stop`, `loop` override, `onComplete` and `onFrame` signals.
414
- - `SceneManager` is now a real stack: `pushScene`, `popScene`, `setScene` with resolved `SceneParticipationPolicy` covering stack mode and input mode.
415
- - `SceneInputEvent` routing honours stack participation so overlay/modal scenes can intercept input cleanly.
416
- - Fade transitions integrated into scene switching.
417
- - `View` camera: `follow` with lerp, `setBoundsConstraint`, `zoom`/`setZoom`, `shake` with decay and configurable frequency.
418
- - `Sound`: `setPoolSize`, `playPooled`, `stopPooled`, `defineSprite`, `setSprites` for audio-sprite playback.
419
-
420
- ### Rendering / visuals
421
-
422
- - Filter pipeline: abstract `Filter` base with `BlurFilter` and `ColorFilter` implementations; per-node filter chains wired through the render runtime.
423
- - Masking support in both render managers and on `RenderNode`.
424
- - Render-pass composition: `RenderTargetPass`, `CallbackRenderPass`, `RenderTarget`, and the existing `RenderTexture` for off-screen work.
425
- - `RenderNode.cacheAsBitmap` flattens expensive subtrees to a cached texture with invalidation.
426
- - `Container.sortableChildren` + `SceneNode.zIndex` provide depth-sorted rendering with a stable fallback on insertion order.
427
- - Multi-texture batching on the WebGPU sprite renderer (`textureSlots`, `maxBatchTextures`). See caveat below.
428
- - WebGPU sprite, particle, and primitive renderers reached functional parity with the WebGL2 equivalents.
429
- - Context-loss handling preserved.
430
-
431
- ### Performance
432
-
433
- - Automatic off-screen culling: `Drawable` checks `inView(view)` each frame and counts skipped nodes.
434
- - `RenderStats` exposes `submittedNodes`, `culledNodes`, `drawCalls`, `batches`, `renderPasses`, and `frameTimeMs` for observability.
435
- - Hot-path cleanup across the renderers.
436
- - `npm run perf:benchmark` runs the rendering benchmark harness under `test/perf/`.
437
-
438
- ### Physics
439
-
440
- - Optional Rapier integration via `createRapierPhysicsWorld({ gravityY })`.
441
- - `@dimforge/rapier2d-compat` is declared as an optional `peerDependency`; apps that do not import the physics entry point incur zero runtime cost.
442
- - Collision groups/masks encoded into Rapier's 16/16 packed format; `PhysicsCollisionFilter` lets you declare membership and what each body collides with.
443
- - Triggers vs. solid colliders distinguished via `trigger` on the descriptor; `onTriggerEnter` / `onTriggerExit` signals on the body.
444
- - Transform sync helpers and a `createDebugGraphics`/`updateDebugGraphics` path for debug draw through the existing `Graphics` primitive.
445
-
446
- ### WebGPU
447
-
448
- - Sprite, particle, and primitive renderers now cover the WebGL2 feature surface used by the scene runtime.
449
- - Explicit `backend: { type: 'webgpu' }` errors out if WebGPU is unavailable or initialization fails — failures are not silently swallowed.
450
- - `backend: { type: 'auto' }` prefers WebGPU when `navigator.gpu` is present and falls back to WebGL2 only when the WebGPU init path throws.
451
- - Initialization error paths are now observable through the thrown error rather than partially constructed state.
452
-
453
- ### Docs / examples
454
-
455
- - README rewritten to match the shipped surface.
456
- - New docs hub under `docs/` with sections for getting-started, core-concepts, assets, scenes, rendering, audio, physics, performance, and examples.
457
- - New class-focused API pages: `Application`, `Renderer`, `Graphics`, `AnimatedSprite`, `AssetManifests`, `Audio`, `View`, `VisualEffects`, `PhysicsRapier`, `Performance`, `GameFeel`.
458
- - `examples/` folder contains focused source snippets (`01-quickstart.ts` … `08-physics-rapier.ts`) that are typechecked against the public API via `tsconfig.examples.json`.
459
-
460
- ### Tooling / release quality
461
-
462
- - `npm run typecheck:examples` typechecks the in-repo examples against `src/` to prevent example drift.
463
- - `npm run verify:exports` validates the package entry graph (`scripts/verify-exports.mjs`).
464
- - `npm run verify:package` runs build example typecheck export verification `npm pack --dry-run`.
465
- - `npm run verify:release` is the smallest release gate: typecheck lint tests verify:package.
466
- - CI runs lint, typecheck, tests, bundle build, declaration build, example typecheck, export verification, and pack dry-run on every PR to `master`.
467
-
468
- ### Behaviour changes worth knowing
469
-
470
- These are minor-level behaviour changes, not source-breaks; flagged here for transparency:
471
-
472
- - **Automatic culling**: nodes whose `inView(view)` check is false are no longer submitted and are counted in `RenderStats.culledNodes`. Apps that were already relying on correct bounds see no observable change. If a custom drawable under-reports its bounds, it may now be skipped when it was previously drawn off-screen.
473
- - **Scene input routing**: with the new stack, input dispatch honours the resolved `SceneInputMode` of each stack entry. Apps that only use `setScene(...)` with no `pushScene` keep single-scene v2.0.0 behaviour.
474
- - **Explicit WebGPU failures**: `backend: { type: 'webgpu' }` now throws rather than silently picking WebGL2. Apps that want the old "try WebGPU, otherwise WebGL2" behaviour should use `backend: { type: 'auto' }`.
475
-
476
- ### Known limitations / honest caveats
477
-
478
- - **WebGL2 is still single-texture batched.** Multi-texture batching is implemented only in the WebGPU sprite renderer. WebGL2 sprite-heavy scenes will still flush on texture changes.
479
- - **WebGPU is improved, not "production WebGPU".** Treat the WebGPU backend as functional parity with WebGL2 for the features this library ships, not as a general-purpose WebGPU renderer.
480
- - **Rapier is optional.** If you never import the physics entry point, Rapier is not installed or loaded. It is not bundled with the library.
481
- - **Tilemaps are not in scope.** There is no built-in tilemap renderer; engines targeting Tiled-centric games should continue to reach for dedicated tooling.
482
- - **Bitmap fonts are not shipped.** `Text` renders via Canvas with stroke support; `BitmapText` is not included.
483
- - **No tween library.** Animation curves and tween orchestration are left to consumer code or external libraries.
484
- - **Audio remains Web Audio decoded/streaming with pooling and sprites.** Spatial audio (`PannerNode`), effects (`ConvolverNode`, `BiquadFilterNode`, `DynamicsCompressorNode`), and fade helpers are not part of this release.
485
- - **Particles are still CPU-simulated.** The WebGPU particle renderer is a rendering path, not a GPU compute simulator.
486
- - **Graphics: no gradients, patterns, caps/joins, or dashing.** Basic fills and strokes only.
487
- - **Input gaps unchanged from 2.0.0**: no haptics/vibration, no rebinding capture, no gesture library, fixed gamepad dead zones.
488
-
489
- ### Upgrading from 2.0.0
490
-
491
- No code changes are required for typical applications. Review the behaviour-change notes above if your code:
492
-
493
- - requests `backend: { type: 'webgpu' }` explicitly and was relying on silent fallback,
494
- - implements a custom `Drawable` with inexact bounds,
495
- - pushed multiple scenes via manual orchestration outside `SceneManager`.
496
-
497
- ## [2.0.0] - previous major
498
-
499
- Baseline for the modernized architecture wave (renderer runtime, scene runtime, class-token loader v2, math and rendering contract renames).
1
+ # Changelog
2
+
3
+ All notable changes to ExoJS are documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.6.4] - 2026-05-02
8
+
9
+ > **Heads-upbreaking change despite the patch number.** Reshapes
10
+ > the capabilities API one version after it was introduced. Pre-1.0
11
+ > SemVer permits breaking changes within the 0.x.y line; we kept the
12
+ > minor digit unchanged because the previous shape only existed for
13
+ > a single release (0.6.3) and almost no one will have pinned to it.
14
+
15
+ 0.6.3 shipped a sync-only `capabilities` object plus an `isSupported`
16
+ helper; both are gone. The replacement is a `Capabilities` class with
17
+ a lazy-cached `static get ready` Promise — async-aware (real WebGPU
18
+ adapter check, not just API surface), flat-property, OOP-flavored to
19
+ match the rest of ExoJS.
20
+
21
+ ### Breaking
22
+
23
+ - **`capabilities` (lowercase const) and `isSupported` are removed.**
24
+ Replace with `await Capabilities.ready`. Properties on the resolved
25
+ instance carry the same information at richer fidelity:
26
+ - `capabilities.touch` (`boolean`) `caps.touch` (`boolean`) plus
27
+ new `caps.maxTouchPoints` (`number`).
28
+ - `capabilities.webgpu` (`boolean`, API-surface only)
29
+ `caps.webgpu` (`boolean`, same API-surface meaning) plus new
30
+ `caps.webgpuAdapter` (`GPUAdapter | null`, the actual adapter
31
+ request result), `caps.webgpuVendor`, `caps.webgpuArchitecture`.
32
+ - `capabilities.audio` (`boolean`) `caps.audio` (`boolean`).
33
+ - All other booleans (`pointer`, `keyboard`, `gamepad`,
34
+ `fullscreen`, `vibration`, `offscreenCanvas`, `webWorkers`,
35
+ `devicePixelRatio`, `webgl2`) carry over with identical names.
36
+ - **`CapabilityName` type is removed.** It existed only to type
37
+ `isSupported`'s parameter; with the function gone the union has no
38
+ consumer.
39
+
40
+ ### Added
41
+
42
+ - **`Capabilities` class** with lazy-cached static `ready` Promise.
43
+ First read fires the probes (sync ones immediate, the WebGPU
44
+ adapter check async); every subsequent read returns the same
45
+ Promise. The resolved instance is frozen.
46
+ - **`Application.capabilities`** accessor returns the same instance
47
+ after `await app.start(...)` resolves; reading before start throws.
48
+ Application's start now overlaps capability detection with backend
49
+ init via `Promise.all`-style parallelism — no extra startup
50
+ latency.
51
+ - **Real WebGPU adapter check** as part of detection: `webgpuAdapter`
52
+ is non-null only if `navigator.gpu.requestAdapter()` succeeded.
53
+ Solves the "API surface present but adapter not available" false
54
+ positive that the 0.6.3 sync `capabilities.webgpu` couldn't
55
+ distinguish.
56
+
57
+ ### Migration
58
+
59
+ ```ts
60
+ // Before (0.6.3)
61
+ import { capabilities, isSupported } from '@codexo/exojs';
62
+ if (capabilities.webgpu) startWebGpu(); // false positives possible
63
+ if (isSupported('touch')) showTouchUi();
64
+
65
+ // After (0.6.4)
66
+ import { Capabilities } from '@codexo/exojs';
67
+ const caps = await Capabilities.ready;
68
+ if (caps.webgpuAdapter) startWebGpu(); // strict adapter check
69
+ if (caps.touch) showTouchUi();
70
+
71
+ // Or via Application after start:
72
+ await app.start(scene);
73
+ if (app.capabilities.touch) showTouchUi();
74
+ ```
75
+
76
+ ## [0.6.3] - 2026-05-02
77
+
78
+ Adds the `capabilities` feature-detection API. Pure addition no
79
+ existing surface changes shape.
80
+
81
+ ### Added
82
+
83
+ - **`capabilities` and `isSupported`.** A frozen
84
+ `Readonly<Record<CapabilityName, boolean>>` evaluated once at
85
+ module load, plus a typed `isSupported(name)` lookup. Initial
86
+ probes: `webgl2`, `webgpu`, `audio`, `pointer`, `touch`, `gamepad`,
87
+ `keyboard`, `fullscreen`, `vibration`, `offscreenCanvas`. All
88
+ probes are synchronous; for "is the WebGPU adapter actually
89
+ available" the answer remains async and lives in `Application`'s
90
+ backend selection. `Capabilities` and `CapabilityName` types are
91
+ also exported.
92
+
93
+ ## [0.6.2] - 2026-05-02
94
+
95
+ Adds the `Mesh` primitive — the first new public Drawable since the
96
+ 0.6.0 cleanup. PATCH bump because the only change is additive: a new
97
+ class plus its two backend renderers; nothing existing changes shape.
98
+
99
+ ### Added
100
+
101
+ - **`Mesh` Drawable.** Arbitrary 2D triangle-mesh primitive sitting
102
+ alongside `Sprite` in the Drawable hierarchy. Construction takes a
103
+ `MeshOptions` object with required `vertices` (flat (x,y) pairs) and
104
+ optional `indices`, `uvs`, `colors` (packed RGBA8 u32 per vertex),
105
+ and `texture`. Mesh data is immutable post-construction, but the
106
+ underlying typed arrays may be mutated in place — call
107
+ `mesh.recomputeLocalBounds()` afterwards to keep culling correct.
108
+ Validation is enforced at construction (mismatched array lengths,
109
+ out-of-range indices, non-multiple-of-3 vertex/index counts all
110
+ throw).
111
+ - **`WebGl2MeshRenderer`.** Single-drawcall-per-mesh path on WebGL2.
112
+ Vertex layout is 20 bytes (pos f32x2 + uv f32x2 + color u8x4-norm).
113
+ Texture is bound to slot 0; meshes without an explicit texture
114
+ resolve to `Texture.white` so the fragment shader stays branchless.
115
+ - **`WebGpuMeshRenderer`.** Deferred batched-pass path on WebGPU. CPU
116
+ bakes (view × globalTransform) into vertex positions so the WGSL is
117
+ uniform-free except for a per-mesh dynamic-offset tint+flags slot.
118
+ Pipelines are created per (blendMode × format) and pre-warmed via
119
+ `prewarmPipelines` during backend init. Texture bind groups are
120
+ cached per Texture/RenderTexture instance.
121
+ - **Three live examples** under `examples/public/examples/rendering/`:
122
+ `mesh-triangle.js` (untextured, vertex-colored), `mesh-textured-quad.js`
123
+ (textured quad equivalent to a Sprite, hand-built from a Mesh), and
124
+ `mesh-deformed-grid.js` (16×16 grid whose vertex positions wave
125
+ each frame demonstrates the deformation use case Sprite can't
126
+ handle).
127
+
128
+ ## [0.6.1] - 2026-05-02
129
+
130
+ Playground-only release. Library code is unchanged from 0.6.0; the
131
+ npm tarball ships byte-for-byte the same `dist/` output. The version
132
+ bump exists so the published changelog and the playground's release
133
+ catalog stay in sync.
134
+
135
+ ### Changed
136
+
137
+ - **Playground version selector now reads GitHub Releases at runtime.**
138
+ The dropdown was previously fed by a committed `versions.json` plus
139
+ per-version snapshot directories under
140
+ `examples/public/examples/versions/<id>/` and
141
+ `examples/public/vendor/exojs/<id>/`. Both are gone. The dropdown
142
+ now fetches from the GitHub Releases API
143
+ (`api.github.com/repos/Exoridus/ExoJS/releases`); the special
144
+ "current" entry continues to load locally-vendored sources for the
145
+ build-time HEAD. Example sources for any released version load
146
+ from `raw.githubusercontent.com/Exoridus/ExoJS/v<id>/...` and the
147
+ library bundle loads from `cdn.jsdelivr.net/npm/@codexo/exojs@<id>`.
148
+ Versions appear in the dropdown automatically once a tag is
149
+ published no bookkeeping commit is needed any more.
150
+
151
+ ### Removed
152
+
153
+ - **Versioned-snapshot scaffolding in the playground.** The
154
+ `examples/public/examples/versions/` snapshot tree, the
155
+ per-version `examples/public/vendor/exojs/<id>/` mirrors, and
156
+ `examples/public/examples/versions.json` are all gone, along with
157
+ the `phase2-bundle.smoke.test.mjs` smoke test that policed their
158
+ byte-identical layout. The `versions.json` shape test in
159
+ `phase1-bundle.smoke.test.mjs` is also gone. `sync-exo-vendor.ts`
160
+ no longer mirrors the flat vendor into a versioned subdirectory.
161
+
162
+ ## [0.6.0] - 2026-05-02
163
+
164
+ A large pre-1.0 cleanup release. Two intentional API breaks (Backend
165
+ rename, Scene class-only), a full GPU-instancing pass across sprite
166
+ and particle renderers on both backends, and a slimmer npm package
167
+ shape. All on a single 0.x minor since the project is still pre-1.0
168
+ and breaks freely between minors.
169
+
170
+ ### Breaking
171
+
172
+ - **`Runtime` types renamed to `Backend`; render-manager classes
173
+ collapse into the same name.** `SceneRenderRuntime` →
174
+ `RenderBackend`. The split `WebGl2RendererRuntime` /
175
+ `WebGpuRendererRuntime` interfaces are gone the concrete classes
176
+ are the public type. `WebGl2RenderManager` → `WebGl2Backend`,
177
+ `WebGpuRenderManager` `WebGpuBackend`. `Application.renderManager`
178
+ → `Application.backend`. Internal field/parameter names follow
179
+ (`runtime` → `backend`, `_runtime` → `_backend`, `getRuntime()` →
180
+ `getBackend()`). `WebGl2ShaderRuntime` → `WebGl2ShaderProgram` (the
181
+ type stores a `WebGLProgram` plus its bound state — the new name
182
+ reflects that). `WebGl2RenderBufferRuntime` and
183
+ `WebGl2VertexArrayObjectRuntime` keep their names — they describe
184
+ per-resource lifecycle, not the render backend.
185
+ - **`Scene` is class-only; the plain-object definition constructor is
186
+ gone.** `new Scene({ update() { ... } })` no longer works. Subclass
187
+ to define a scene — `class GameScene extends Scene { override
188
+ update(...) { ... } }` for named scenes, `new class extends Scene
189
+ { ... }` for one-offs. The `SceneData` interface and
190
+ `SceneInstance<T>` type alias are removed (they only existed to
191
+ type the spread-into-`this` constructor). Internal Scene fields
192
+ move from ECMAScript `#`-private to TS `protected _app/_root/
193
+ _stackMode/_inputMode` subclasses can now reach internal state
194
+ directly when they need to.
195
+ - **npm package shape simplified.** Dropped: `dist/exo.global.js` /
196
+ `dist/exo.global.min.js` (legacy IIFE for `<script>` use) and
197
+ `dist/exo.esm.min.js` (consumers minify on their side). What ships
198
+ now: `dist/esm/` (per-module ESM tree, the canonical entry) and
199
+ `dist/exo.esm.js` (single-file ESM bundle for direct module
200
+ loading). `package.json#main`, `module`, `browser`, `exports` are
201
+ unchanged in semantics — only the auxiliary artifacts go away.
202
+
203
+ ### Performance
204
+
205
+ - **WebGL2 sprite renderer is now fully GPU-instanced.** Quad
206
+ corners derive from `gl_VertexID` in the vertex shader; per-instance
207
+ attributes carry `localBounds`, `transformAB`/`transformCD` (the 2D
208
+ affine), `uvBounds`, packed RGBA8 tint, and packed slot/flags (56
209
+ bytes per instance). The CPU per-frame cost is one bounded
210
+ `writeBuffer` per batch; no per-vertex stream is uploaded.
211
+ `drawArraysInstanced` over `TRIANGLE_STRIP` replaces the per-vertex
212
+ `drawElements` path.
213
+ - **WebGPU sprite renderer matches the same instanced layout.** Uses
214
+ `drawIndexed` over a static `[0,1,2,0,2,3]` index buffer with
215
+ `triangle-list` topology (the index buffer keeps mock-frame
216
+ bookkeeping deterministic the on-screen result is the same as a
217
+ triangle-strip).
218
+ - **Particle renderers fully instanced on both backends, with system
219
+ data hoisted out of per-instance.** `localBounds`, `uvBounds`, and
220
+ `systemTransform` are now uniforms (one upload per system per
221
+ frame). Per-instance shrinks from 56 to 24 bytes (translation,
222
+ scale, rotation, packed RGBA8 color). `WebGl2ParticleRenderer` no
223
+ longer extends `AbstractWebGl2BatchedRenderer` particles don't
224
+ share batch infrastructure with sprites anymore.
225
+
226
+ ### Removed
227
+
228
+ - `docs/` directory and the README's "Next Steps" link block. The
229
+ prose docs were drifting out of sync with the code; the in-repo
230
+ examples (`examples/README.md`) remain the supported reference.
231
+ - `SceneRenderRuntime`, `WebGl2RendererRuntime`, `WebGpuRendererRuntime`
232
+ interfaces (collapsed into the renamed classes — see Breaking).
233
+ - `SceneData` interface, `SceneInstance<T>` type alias (no longer
234
+ needed without the Scene definition-spread constructor).
235
+ - `WebGl2RenderManager`, `WebGpuRenderManager` class names (renamed
236
+ to `*Backend` see Breaking).
237
+ - `Sampler._premultiplyAlpha`, `Sampler._generateMipMap`,
238
+ `Sampler._flipY` (write-only texture pixel-store path consumes
239
+ these directly from `SamplerOptions`, the GL sampler object only
240
+ cares about scale and wrap modes).
241
+ - `AudioAnalyser._audioContext` (write-only never read after
242
+ setup).
243
+ - `WebGpuRenderManager._blendMode` (write-only renderers consult
244
+ `sprite.blendMode` directly; `setBlendMode` keeps its
245
+ not-yet-implemented blend-mode validation).
246
+ - `@rollup/plugin-terser` devDependency (no minified bundle output
247
+ any more).
248
+
249
+ ### Migration
250
+
251
+ ```ts
252
+ // Before (0.5.x)
253
+ class GameScene extends Scene {
254
+ override draw(runtime: SceneRenderRuntime): void {
255
+ this.root.render(runtime);
256
+ }
257
+ }
258
+
259
+ const triangleRenderer = new CustomRenderer(app.renderManager);
260
+
261
+ if (app.renderManager instanceof WebGpuRenderManager) { /* ... */ }
262
+
263
+ // Plain-object scene
264
+ app.start(new Scene({ update() { /* ... */ } }));
265
+ ```
266
+
267
+ ```ts
268
+ // After (0.6.0)
269
+ class GameScene extends Scene {
270
+ override draw(backend: RenderBackend): void {
271
+ this.root.render(backend);
272
+ }
273
+ }
274
+
275
+ const triangleRenderer = new CustomRenderer(app.backend);
276
+
277
+ if (app.backend instanceof WebGpuBackend) { /* ... */ }
278
+
279
+ // Anonymous-subclass scene (or named subclass)
280
+ app.start(new class extends Scene { override update() { /* ... */ } });
281
+ ```
282
+
283
+ ## [0.5.1] - 2026-04-28
284
+
285
+ Rendering-pipeline performance pass. No public API changes; all
286
+ optimisations are internal to the renderer subsystem.
287
+
288
+ ### Changed
289
+
290
+ - **WebGL2 sprite batching is now multi-texture.** A single batch can
291
+ bind up to eight textures (units 0..7); each vertex carries a uint
292
+ texture-slot attribute and the fragment shader's per-slot if-chain
293
+ selects the right sampler. Previously every texture change forced a
294
+ flush, capping multi-atlas scenes at roughly one batch per texture.
295
+ The vertex stride grows from 16 to 20 bytes (the new u32 slot at
296
+ offset 16 is the only addition); position, packed UV, and packed
297
+ RGBA8 tint are unchanged. Batches still flush on buffer-full,
298
+ blend-mode change, and now slot exhaustion (more than eight
299
+ textures in one batch).
300
+ - **WebGPU sprite vertex layout compacted from 28 to 24 bytes.** The
301
+ per-vertex `premultiplyAlpha` flag and `textureSlot` index
302
+ previously took one u32 attribute each; they are now packed into a
303
+ single u32 with the slot in bits 0..7 and the flag in bit 8. The
304
+ WGSL vertex shader unpacks via bit ops. 16 bytes saved per sprite.
305
+ - **Async-compile path now syncs the shader between buffer setup and
306
+ attribute lookup.** The 0.5.0+slice-C deferral of attribute /
307
+ uniform extraction from `initialize()` to first `sync()` broke
308
+ connect-time `getAttribute()` callers under a real WebGL2 context
309
+ (jest mocks didn't exercise that code path). Fixed in
310
+ `AbstractWebGl2BatchedRenderer`, `WebGl2PrimitiveRenderer`, and
311
+ `WebGl2MaskCompositor`. The driver still gets a parallel-compile
312
+ window between `shader.connect()` and `shader.sync()` thanks to
313
+ KHR_parallel_shader_compile; the eventual blocking status query is
314
+ a no-op when compile already finished.
315
+
316
+ ### Added
317
+
318
+ - **`WebGl2SpriteRenderer.prewarmPipelines` equivalent for WebGPU.**
319
+ `WebGpuSpriteRenderer.prewarmPipelines(formats)` calls
320
+ `createRenderPipelineAsync` for every BlendMode × format combo in
321
+ parallel during render-manager init. The first draw of every common
322
+ blend mode no longer blocks on synchronous pipeline creation.
323
+ Renderers without a `prewarmPipelines` method continue to create
324
+ pipelines lazily on first use; the pre-warm fallback gracefully
325
+ no-ops when `createRenderPipelineAsync` isn't available (older
326
+ browsers, headless test mocks).
327
+ - **`KHR_parallel_shader_compile` opt-in for WebGL2 shader compile.**
328
+ When the extension is present (Chrome / Edge / Firefox by default,
329
+ Safari since 17) the GL driver may compile shaders on a worker
330
+ thread; status queries are deferred to the first `sync()` call so
331
+ the main thread doesn't block on compile.
332
+ - **`ShaderPrimitives.UnsignedInt`, `UnsignedIntVec2..4`** with their
333
+ byte-size and array-constructor mappings, so `getActiveAttrib` /
334
+ `getActiveUniform` on a `uint` shader slot resolves correctly. The
335
+ enum gains four members; the runtime export inventory is unchanged.
336
+ - **`WebGl2VertexArrayObject.addAttribute(..., integer)`** parameter
337
+ routes integer-typed shader inputs (`uint`, `uvec`) to
338
+ `vertexAttribIPointer` rather than `vertexAttribPointer`, so the
339
+ shader receives the raw integer value instead of a coerced float.
340
+ - **`RendererRegistry.renderers()`** iterator exposes the registered
341
+ renderers so backend managers can dispatch optional lifecycle hooks
342
+ (such as the WebGPU pipeline pre-warm above) without per-renderer
343
+ private-field reach-ins.
344
+
345
+ ### Performance notes
346
+
347
+ - Sprite-heavy scenes with multiple atlases see a draw-call reduction
348
+ proportional to atlas count (up to 8×) on WebGL2.
349
+ - WebGPU sprite vertex bandwidth is reduced 14% (16 bytes per sprite).
350
+ - First-frame stutter from JIT shader / pipeline compilation is
351
+ largely eliminated when KHR_parallel_shader_compile (WebGL2) or
352
+ `createRenderPipelineAsync` (WebGPU) is supported.
353
+
354
+ ## [0.5.0] - 2026-04-28
355
+
356
+ Three focused breaking changes targeted at the first pre-1.0 minor: a hierarchy-semantics boundary slice (per `.workspace/reviews/opus-pre-1.0-architecture-review/09-b1-implementation-rfc.md`), a unified mask API with full multi-source support (per `.workspace/reviews/opus-pre-1.0-architecture-review/10-mask-api-decision.md`), and a Scene API simplification that collapses the static factory into the constructor. No aliases.
357
+
358
+ ### Removed
359
+
360
+ - **`Transformable` class and `TransformableFlags` enum.** Inlined into `SceneNode`. `SceneNode` now owns its transform fields and accessors (`position`, `x`, `y`, `rotation`, `scale`, `origin`, `setPosition`, `setRotation`, `setScale`, `setOrigin`, `move`, `rotate`, `getTransform`, `updateTransform`, `flags`) directly. The public surface shrinks by two symbols. `Flags<T>` (the generic class) remains public.
361
+ - **`SceneNode.render(runtime)` no-op.** Render belongs to `RenderNode` and below; bare `SceneNode` no longer pretends to participate in the render pass.
362
+ - **`Scene.create(definition)` static factory.** Replaced by a typed constructor overload — see Changed below.
363
+
364
+ ### Changed
365
+
366
+ - **`RenderNode.render(runtime)` is now `abstract`.** All concrete subclasses (`Drawable`, `Container`, `Graphics`, `Sprite`, `AnimatedSprite`, `Text`, `Video`, `ParticleSystem`, `DrawableShape`) already implement it. The abstract declaration removes the SceneNode-render lie.
367
+ - **`RenderNode.mask` is now the unified visual masking API**, accepting any `MaskSource = Rectangle | Texture | RenderTexture | RenderNode | null`. The behavior depends on the source:
368
+ - `Rectangle` — fast axis-aligned scissor clip (O(1) GPU state). The most common case for UI panels and viewport regions.
369
+ - `Texture` / `RenderTexture` uses the texture's alpha channel as the mask, stretched to fit the masked node's local bounds. The texture has no transform of its own; for transform/scale/rotation control over the mask source, use a `Sprite(texture)` instead.
370
+ - `RenderNode` (`Sprite`, `Graphics`, `Container`, etc.) — the node's full visual output (with its own transform, filters, cacheAsBitmap) is rendered into an intermediate render texture and used as the alpha mask. Bare `SceneNode` instances are rejected at compile time because they are structural-only.
371
+ - `null` — no mask.
372
+
373
+ Setting `node.mask = node` (self-mask) throws at runtime.
374
+ - **`SceneRenderRuntime` mask primitives renamed** to match the new vocabulary:
375
+ - `pushMask(maskBounds)` / `popMask()` `pushScissorRect(bounds)` / `popScissorRect()` (lower-level scissor primitive used internally by the `Rectangle` mask path).
376
+ - New `composeWithAlphaMask(content, mask, x, y, width, height, blendMode)` used internally by the Texture/RenderTexture/RenderNode mask paths.
377
+ - Backend implementations: `WebGl2MaskCompositor` (new) and `WebGpuMaskCompositor` (new) implement the alpha-compose pipeline. Each owns its own shader/pipeline, lazily initialized on first use, disconnected on manager destroy. Pipelines are cached per (target format, blend mode) on the WebGPU side.
378
+ - **`Container._children` narrowed to `Array<RenderNode>`.** `addChild`, `addChildAt`, `removeChild`, `swapChildren`, `getChildIndex`, `setChildIndex`, `getChildAt`, and `Scene.addChild`/`removeChild` now require `RenderNode` instances. Bare `SceneNode` instances cannot be added to a container at compile time. (Previous behavior added them as no-op render nodes; observable behavior was unchanged for any code that already added Drawable/Container/Graphics/Sprite/etc.)
379
+ - **`Scene` is now generic and constructable with an optional typed `SceneData` definition.** `class Scene<T extends SceneData = SceneData>` — `new Scene()` produces an empty scene; `new Scene({ update() { ... }, draw() { ... } })` accepts a typed definition object whose method bodies see `this` as `Scene<T> & T` via `ThisType<>`. `class extends Scene` is unchanged and remains the recommended path for stateful scenes — TypeScript only infers properties declared inside the definition object, so `this._foo = ...` assignments inside method bodies are still invisible to the type system without pre-declaration. The existing `SceneInstance<T>` type alias keeps its meaning (`Scene<T> & T`) and is still re-exported from the package root.
380
+
381
+ ### Added
382
+
383
+ - **`MaskSource` type alias** is exported from the package root: `Rectangle | Texture | RenderTexture | RenderNode | null`. This is the public type for `RenderNode.mask`.
384
+ - **Root export runtime snapshot gate** (`test/core/root-index-snapshot.test.ts`). Captures every runtime-visible export name from `src/index.ts` and compares against a committed Jest snapshot. CI fails on any unintentional addition or removal.
385
+ - **Root export type-level inventory** (`test/core/root-index-type-inventory.test.ts`). Enumerates all exported symbols including interfaces and type aliases erased at runtime with their kind annotations.
386
+ - **RenderNode/SceneNode contract tests** (`test/rendering/render-node.test.ts`). Pin down the `SceneNode` is structural-only / `RenderNode.render` is abstract / `Container.addChild` rejects non-`RenderNode` contracts.
387
+ - **MaskSource union tests** (`test/rendering/mask-source.test.ts`). 12 tests covering: Rectangle scissor routing, nested rectangles, zero-size and null masks; Texture / RenderTexture / Sprite / Graphics / Container as alpha-mask sources; bare `SceneNode` rejected at compile time; self-mask rejected at runtime; mask reassignment to null.
388
+
389
+ ### Migration
390
+
391
+ | Before (0.4.x) | After |
392
+ |---|---|
393
+ | `import { Transformable } from '@codexo/exojs'`; `class X extends Transformable` | `import { SceneNode } from '@codexo/exojs'`; `class X extends SceneNode` |
394
+ | `import { TransformableFlags } from '@codexo/exojs'` | Internal flag enum is no longer public; use SceneNode's high-level transform accessors instead. |
395
+ | `node.mask = anyShapeNode` *(silently clipped to bounding rect)* | `node.mask = anyShapeNode` *(now a real shape mask via alpha compositing — except bare SceneNode which is rejected at compile time)* |
396
+ | Want fast axis-aligned clipping? | `node.mask = new Rectangle(x, y, w, h)` |
397
+ | Want to clip with a texture's alpha channel? | `node.mask = texture` or `node.mask = renderTexture` |
398
+ | Want a transformed/positioned alpha mask? | `node.mask = new Sprite(texture)` (Sprite's transform/position/scale apply to the mask source) |
399
+ | `runtime.pushMask(rect)` / `runtime.popMask()` | `runtime.pushScissorRect(rect)` / `runtime.popScissorRect()` (renamed; behavior unchanged) |
400
+ | `class Group extends SceneNode { override render() {...} }` | `class Group extends RenderNode { override render() {...} }` |
401
+ | `class CustomContainer extends Container { override addChild(child: SceneNode) {...} }` | `class CustomContainer extends Container { override addChild(child: RenderNode) {...} }` |
402
+ | `Scene.create({ update() {...} })` | `new Scene({ update() {...} })` (drop-in replacement; same `this` typing via `ThisType<Scene & T>`) |
403
+ | `Scene.create({})` | `new Scene()` |
404
+
405
+ No deprecated aliases are provided. The migration is mechanical and the project is pre-1.0 with explicit "may break between minors" policy.
406
+
407
+ ### Modernized
408
+
409
+ Quality-of-life cleanups using ES2022+ features. No public-API impact, but flagged here for transparency:
410
+
411
+ - **`Scene` uses ECMAScript `#` private fields** (`#app`, `#root`, `#stackMode`, `#inputMode`) instead of TypeScript `private _xxx`. True runtime privacy — fields are unreachable from outside the class even via bracket notation. The rest of the codebase still uses `private _xxx`; full sweep is queued for a future release pending test refactor (existing tests reach into private state via `obj['_field']`, which `#` fields block).
412
+ - **`Loader.ts` uses `Object.hasOwn(obj, key)`** instead of `Object.prototype.hasOwnProperty.call(obj, key)`. Same semantics, less ceremony.
413
+ - **`SceneManager` uses `array.at(-1)`** for stack-tail access instead of `arr[arr.length - 1]`. Three sites: the active-scene getter, `popScene`, and `_unloadCoveredScenes`.
414
+ - **`Loader.ts` uses `Error.cause`** for the wrapped error in `factory.create()` failures. `cause` carries the full original error (with stack trace) so DevTools, Sentry, etc. surface the underlying cause automatically. The wrapper message still contains the inner message for backward compatibility with consumers that string-match the error message.
415
+
416
+ ### Performance notes
417
+
418
+ - `mask = Rectangle` is O(1) GPU scissor free at scale.
419
+ - `mask = Texture` / `mask = RenderTexture` adds one intermediate render texture acquire and one composite pass per masked render.
420
+ - `mask = RenderNode` adds a second intermediate render texture acquire (to bake the mask node's visual output) plus the composite pass — so two extra passes per masked render. Use sparingly for high-frequency draws; consider `cacheAsBitmap` on the masked content.
421
+
422
+ ### Notes
423
+
424
+ - The single dominant import model is intentional: `import { Application, Sprite } from '@codexo/exojs'` and `import * as Exo from '@codexo/exojs'` align with the IIFE/global bundle (`Exo.Application`, `Exo.Sprite`). Subpath exports are deferred until a stable API boundary warrants them.
425
+ - `SceneNode` is now a concrete structural class — transform, hierarchy, collision, culling. `RenderNode` (abstract) is the render-capable base. Every render-participating class extends `RenderNode`; bare `SceneNode` instances are valid as user-defined data nodes but cannot be added to containers.
426
+
427
+ ## [0.4.0] - 2026-04-26
428
+
429
+ Pre-1.0 versioning reset. The active development line moves from `2.1.2` to `0.4.0` to honestly reflect that the public API is not yet stable. No runtime behavior change relative to the previous head — this release marks a versioning policy shift, not a code rewrite.
430
+
431
+ ### Notes
432
+
433
+ - The `2.x` releases (`2.0.0`, `2.1.0`, `2.1.1`, `2.1.2`) remain published on npm as a historical line and will be deprecated with a pointer to the `0.x` line.
434
+ - New work happens on the `0.x` line. Expect breaking changes between `0.x` minors as the scene graph, renderer, and resource boundaries continue to evolve.
435
+ - `1.0.0` will mark the first stable public API contract. Until then, treat any minor version as potentially breaking and pin exact versions in downstream experiments.
436
+ - Current package identity for the reset line is `@codexo/exojs`. Historical `2.x` release notes may reference the legacy package/import name, old example layout, old scripts, or the former `master` branch target.
437
+ - The `2.1.0` View camera note below used the old working name `setBoundsConstraint`; the current API is `setBounds(...)` / `clearBounds()`.
438
+ - Past CHANGELOG entries for `2.x` are otherwise preserved below as the historical record of work that landed in those releases.
439
+
440
+ ## [2.1.2] - 2026-04-19
441
+
442
+ Patch release with one runtime fix, a toolchain modernization pass, and a legacy-artifact cleanup. No public API removals or renames.
443
+
444
+ ### Fixed
445
+
446
+ - **`Signal.dispatch` skipped sibling `once()` handlers.** `once()` wrappers self-remove mid-iteration, which compacts the underlying bindings array; the `for..of` iterator then advanced past the binding that shifted into the just-visited slot. `dispatch` now iterates a snapshot of bindings, so handler-driven mutation is safe. Visible symptom: the Audio Visualisation example received a set-up `Music` but an un-set-up `AudioAnalyser`, so frequency buffers stayed at zero.
447
+
448
+ ### Changed
449
+
450
+ - Removed the legacy bundled declaration file `dist/exo.d.ts` (emitted via `tsc --outFile` + `module: amd`, both deprecated in TypeScript 6). Modern consumers resolve types through `exports["."].types`, which points at the per-file tree in `dist/esm/`; `dist/exo.d.ts` was never part of the `exports` map. This also removes the `ignoreDeprecations: "6.0"` escape hatch from the build.
451
+ - Build upgraded to TypeScript 6, ESLint 10, Jest 30. Internal imports now use the `@/*` path alias (mapped to `src/*`) and `baseUrl` is no longer required.
452
+
453
+ ## [2.1.1] - 2026-04-19
454
+
455
+ Patch release fixing a cluster of WebGPU and scene-graph bugs discovered after 2.1.0 shipped. No public API removals or renames; one backward-compatible addition on `Container.addChild`.
456
+
457
+ ### Fixed
458
+
459
+ - **WebGPU adapter ordering.** `WebGpuRenderManager` now requests the GPU adapter before acquiring the canvas WebGPU context. A null adapter previously locked the canvas into WebGPU mode, preventing `Application`'s automatic WebGL2 fallback from obtaining a context on the same element.
460
+ - **WebGL2 shader program binding.** `WebGl2ShaderRuntime.sync()` now binds the program before writing uniforms. The previous draw pipeline never called `bindShader(shader)` with a non-null shader, so every `uniform*` write targeted the wrong or null program and `drawElements` reported "no valid shader program in use". Exposed by the WebGPU adapter fallback above.
461
+ - **WGSL multi-texture sprite shader** uses `textureSampleGrad` with explicit screen-space derivatives. `textureSample`'s uniformity requirement prevented the 8-slot dispatch from compiling on any sprite batch spanning more than one texture slot.
462
+ - **Sprite index buffer** allocation and lifecycle. Buffer size was 4× larger than intended (`indexData.byteLength * BYTES_PER_ELEMENT` instead of `indexData.byteLength`), and `_ensureBatchCapacity` ran inside the draw loop and could destroy a buffer the render pass had already bound. Capacity is now grown once up front.
463
+ - **Sprite multi-batch rendering.** When a flush contained multiple batches (blend-mode change, texture-slot overflow, or pipeline switch), each batch's `queue.writeBuffer(vertexBuffer, offset: 0, ...)` serialised before the single submit, leaving only the last batch's vertex data in the buffer. All batch vertex data is now packed into one CPU buffer at distinct sprite offsets and uploaded once; `drawIndexed` uses `firstIndex` to target each range.
464
+ - **Particle and primitive multi-drawcall rendering.** Same multi-write-to-offset-0 pattern, plus mid-loop `_ensureCapacity` destroying buffers still referenced by the pass. Particle renderer now submits one command buffer per system. Primitive renderer was rewritten: CPU bakes `view * globalTransform` into `vec4` clip-space positions per vertex, pipeline has no bind-group, one render pass per flush with packed vertex/index buffers.
465
+ - **Primitive combine order.** `_combinedTransform.copy(view).combine(global)` produced `global * view` (`Matrix.combine` applies the argument on the left, confirmed by `SceneNode.getGlobalTransform` which chains `local.combine(parent.global)` to yield `parent.global * local`). Swapped to `copy(global).combine(view)` = `view * global`.
466
+ - **WebGPU mipmap generation.** The full-screen downsample triangle's UVs are no longer Y-flipped relative to framebuffer orientation. Every odd mip level was being rendered upside-down, producing a visible sprite flip whenever the view zoomed far enough for the LOD selector to cross an odd/even boundary.
467
+
468
+ ### Added
469
+
470
+ - `Container.addChild` accepts multiple children via rest args (`addChild(...children)`). The previous single-argument signature silently dropped the tail of `addChild(a, b, c, d)`; callers only saw `a` in the scene graph. Single-child usage stays backward compatible.
471
+ - Doc comment on `ParticleOptions.position` clarifying it is in the owning `ParticleSystem`'s local coordinate space. The shader applies the system's global transform on top, so passing world coordinates double-translates the emitter.
472
+
473
+ ## [2.1.0] - 2026-04-18
474
+
475
+ Product-readiness release. Additive across assets, game-feel, visuals, performance, optional physics, and WebGPU parity. No public contracts were removed or renamed since v2.0.0.
476
+
477
+ ### Highlights
478
+
479
+ - Typed asset manifests and bundle loading workflow.
480
+ - `AnimatedSprite` with named clips, loop control, and frame signals.
481
+ - Scene stacking with participation policies, input routing, and fade transitions.
482
+ - View/camera polish: follow with lerp, bounds clamp, zoom, shake.
483
+ - Audio sprites and sound pooling.
484
+ - Visual capability wave: filter pipeline, masking, render passes, cache-as-bitmap, multi-texture batching on the WebGPU backend.
485
+ - Automatic off-screen culling with observable render stats.
486
+ - Optional Rapier physics integration behind an optional peer dependency.
487
+ - WebGPU parity improvements and clearer initialization failure semantics.
488
+ - Docs and examples overhaul; release verification hardening.
489
+
490
+ ### Assets / workflow
491
+
492
+ - `defineAssetManifest`, `AssetEntry`, and `loadBundle` with progress callbacks.
493
+ - `BundleLoadError` surfaces per-entry failures with the responsible loader token.
494
+ - Strict manifest validation runs at definition time.
495
+ - `CacheStore` + `IndexedDbStore` remain the persistence path; strategy classes (`CacheFirstStrategy`, `NetworkOnlyStrategy`) are exposed for custom pipelines.
496
+
497
+ ### Game-feel
498
+
499
+ - `AnimatedSprite`: `defineClip`, `setClips`, `play`, `stop`, `loop` override, `onComplete` and `onFrame` signals.
500
+ - `SceneManager` is now a real stack: `pushScene`, `popScene`, `setScene` with resolved `SceneParticipationPolicy` covering stack mode and input mode.
501
+ - `SceneInputEvent` routing honours stack participation so overlay/modal scenes can intercept input cleanly.
502
+ - Fade transitions integrated into scene switching.
503
+ - `View` camera: `follow` with lerp, `setBoundsConstraint`, `zoom`/`setZoom`, `shake` with decay and configurable frequency.
504
+ - `Sound`: `setPoolSize`, `playPooled`, `stopPooled`, `defineSprite`, `setSprites` for audio-sprite playback.
505
+
506
+ ### Rendering / visuals
507
+
508
+ - Filter pipeline: abstract `Filter` base with `BlurFilter` and `ColorFilter` implementations; per-node filter chains wired through the render runtime.
509
+ - Masking support in both render managers and on `RenderNode`.
510
+ - Render-pass composition: `RenderTargetPass`, `CallbackRenderPass`, `RenderTarget`, and the existing `RenderTexture` for off-screen work.
511
+ - `RenderNode.cacheAsBitmap` flattens expensive subtrees to a cached texture with invalidation.
512
+ - `Container.sortableChildren` + `SceneNode.zIndex` provide depth-sorted rendering with a stable fallback on insertion order.
513
+ - Multi-texture batching on the WebGPU sprite renderer (`textureSlots`, `maxBatchTextures`). See caveat below.
514
+ - WebGPU sprite, particle, and primitive renderers reached functional parity with the WebGL2 equivalents.
515
+ - Context-loss handling preserved.
516
+
517
+ ### Performance
518
+
519
+ - Automatic off-screen culling: `Drawable` checks `inView(view)` each frame and counts skipped nodes.
520
+ - `RenderStats` exposes `submittedNodes`, `culledNodes`, `drawCalls`, `batches`, `renderPasses`, and `frameTimeMs` for observability.
521
+ - Hot-path cleanup across the renderers.
522
+ - `npm run perf:benchmark` runs the rendering benchmark harness under `test/perf/`.
523
+
524
+ ### Physics
525
+
526
+ - Optional Rapier integration via `createRapierPhysicsWorld({ gravityY })`.
527
+ - `@dimforge/rapier2d-compat` is declared as an optional `peerDependency`; apps that do not import the physics entry point incur zero runtime cost.
528
+ - Collision groups/masks encoded into Rapier's 16/16 packed format; `PhysicsCollisionFilter` lets you declare membership and what each body collides with.
529
+ - Triggers vs. solid colliders distinguished via `trigger` on the descriptor; `onTriggerEnter` / `onTriggerExit` signals on the body.
530
+ - Transform sync helpers and a `createDebugGraphics`/`updateDebugGraphics` path for debug draw through the existing `Graphics` primitive.
531
+
532
+ ### WebGPU
533
+
534
+ - Sprite, particle, and primitive renderers now cover the WebGL2 feature surface used by the scene runtime.
535
+ - Explicit `backend: { type: 'webgpu' }` errors out if WebGPU is unavailable or initialization fails — failures are not silently swallowed.
536
+ - `backend: { type: 'auto' }` prefers WebGPU when `navigator.gpu` is present and falls back to WebGL2 only when the WebGPU init path throws.
537
+ - Initialization error paths are now observable through the thrown error rather than partially constructed state.
538
+
539
+ ### Docs / examples
540
+
541
+ - README rewritten to match the shipped surface.
542
+ - New docs hub under `docs/` with sections for getting-started, core-concepts, assets, scenes, rendering, audio, physics, performance, and examples.
543
+ - New class-focused API pages: `Application`, `Renderer`, `Graphics`, `AnimatedSprite`, `AssetManifests`, `Audio`, `View`, `VisualEffects`, `PhysicsRapier`, `Performance`, `GameFeel`.
544
+ - `examples/` folder contains focused source snippets (`01-quickstart.ts` … `08-physics-rapier.ts`) that are typechecked against the public API via `tsconfig.examples.json`.
545
+
546
+ ### Tooling / release quality
547
+
548
+ - `npm run typecheck:examples` typechecks the in-repo examples against `src/` to prevent example drift.
549
+ - `npm run verify:exports` validates the package entry graph (`scripts/verify-exports.mjs`).
550
+ - `npm run verify:package` runs build → example typecheck → export verification → `npm pack --dry-run`.
551
+ - `npm run verify:release` is the smallest release gate: typecheck → lint → tests → verify:package.
552
+ - CI runs lint, typecheck, tests, bundle build, declaration build, example typecheck, export verification, and pack dry-run on every PR to `master`.
553
+
554
+ ### Behaviour changes worth knowing
555
+
556
+ These are minor-level behaviour changes, not source-breaks; flagged here for transparency:
557
+
558
+ - **Automatic culling**: nodes whose `inView(view)` check is false are no longer submitted and are counted in `RenderStats.culledNodes`. Apps that were already relying on correct bounds see no observable change. If a custom drawable under-reports its bounds, it may now be skipped when it was previously drawn off-screen.
559
+ - **Scene input routing**: with the new stack, input dispatch honours the resolved `SceneInputMode` of each stack entry. Apps that only use `setScene(...)` with no `pushScene` keep single-scene v2.0.0 behaviour.
560
+ - **Explicit WebGPU failures**: `backend: { type: 'webgpu' }` now throws rather than silently picking WebGL2. Apps that want the old "try WebGPU, otherwise WebGL2" behaviour should use `backend: { type: 'auto' }`.
561
+
562
+ ### Known limitations / honest caveats
563
+
564
+ - **WebGL2 is still single-texture batched.** Multi-texture batching is implemented only in the WebGPU sprite renderer. WebGL2 sprite-heavy scenes will still flush on texture changes.
565
+ - **WebGPU is improved, not "production WebGPU".** Treat the WebGPU backend as functional parity with WebGL2 for the features this library ships, not as a general-purpose WebGPU renderer.
566
+ - **Rapier is optional.** If you never import the physics entry point, Rapier is not installed or loaded. It is not bundled with the library.
567
+ - **Tilemaps are not in scope.** There is no built-in tilemap renderer; engines targeting Tiled-centric games should continue to reach for dedicated tooling.
568
+ - **Bitmap fonts are not shipped.** `Text` renders via Canvas with stroke support; `BitmapText` is not included.
569
+ - **No tween library.** Animation curves and tween orchestration are left to consumer code or external libraries.
570
+ - **Audio remains Web Audio decoded/streaming with pooling and sprites.** Spatial audio (`PannerNode`), effects (`ConvolverNode`, `BiquadFilterNode`, `DynamicsCompressorNode`), and fade helpers are not part of this release.
571
+ - **Particles are still CPU-simulated.** The WebGPU particle renderer is a rendering path, not a GPU compute simulator.
572
+ - **Graphics: no gradients, patterns, caps/joins, or dashing.** Basic fills and strokes only.
573
+ - **Input gaps unchanged from 2.0.0**: no haptics/vibration, no rebinding capture, no gesture library, fixed gamepad dead zones.
574
+
575
+ ### Upgrading from 2.0.0
576
+
577
+ No code changes are required for typical applications. Review the behaviour-change notes above if your code:
578
+
579
+ - requests `backend: { type: 'webgpu' }` explicitly and was relying on silent fallback,
580
+ - implements a custom `Drawable` with inexact bounds,
581
+ - pushed multiple scenes via manual orchestration outside `SceneManager`.
582
+
583
+ ## [2.0.0] - previous major
584
+
585
+ Baseline for the modernized architecture wave (renderer runtime, scene runtime, class-token loader v2, math and rendering contract renames).