From ab309b78c16f60f4fd0d5eedd4ada9c4b8106b17 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Wed, 8 Jul 2026 14:55:18 +0800 Subject: [PATCH 01/20] feat(texture): generalize the Texture2d contract + setTexture accepts assets directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two pieces of 19.9 polish that must land before the release freezes the new texture surface — groundwork for the 19.10 renderer.toFrameTexture() screen-capture feature designed with the maintainer: - Texture2d contract generalized while still unreleased: the class was documented as "owns a drawable image source" with getTexture(): HTMLCanvasElement | HTMLImageElement, but its own docs already anticipated GPU-backed sources (the WebGPU note). The contract now deliberately admits both: a new exported Texture2dSource type (drawables | TextureResource), so GPU-resident subclasses — frame captures, future WebGPU surfaces — fit without a 19.10 compat debate. TextureResource itself stays internal (type-only reference). - ShaderEffect.setTexture accepts a Texture2d asset directly and resolves it via getTexture() internally — retiring the documented `noise.getTexture()` dance: setTexture("uNoise", noise) now works. Raw drawables unchanged. JSDoc + class example updated. Red-first tests: a minimal Texture2d subclass and a NoiseTexture2d passed directly to setTexture, sampled end-to-end via readPixels — both red pre-change (the raw asset object reached createTexture2D). Full suite 4741 passed / 0 failed; tsc build clean. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- packages/melonjs/CHANGELOG.md | 4 +- .../melonjs/src/video/texture/texture2d.ts | 57 +++++++++++------ .../melonjs/src/video/webgl/shadereffect.js | 14 +++-- .../tests/shadereffect-settexture.spec.js | 62 ++++++++++++++++++- 4 files changed, 112 insertions(+), 25 deletions(-) diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index 47572848f..ee8247f5a 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -5,11 +5,11 @@ ### Added - **BMFont XML font support for `BitmapText`** — bitmap fonts can now be loaded from the AngelCode BMFont **XML** flavour in addition to the text `.fnt` format (the serialisation is auto-detected). Many bitmap-font tools and asset packs (itch.io, dafont, …) export PNG + XML; those now load directly through the standard preloader (`{ type: "binary" }`), with no offline conversion step. The XML is parsed with a dependency-free regex parser (no `DOMParser`), so it also works outside the browser (Node / SSR) — unlike the DOM-based TMX XML path. - **Holes & compound paths in `Path2D` / SVG fills** (#1253) — a path may now contain multiple sub-paths (every `M` in an SVG string, or every `moveTo()` call, starts a new one). On `fill()` the first sub-path is the outer contour and each subsequent sub-path is treated as a hole, so donuts, rings, letter counters ("O", "A", "8") and other shapes with holes render correctly under both the WebGL renderer (earcut hole triangulation) and the Canvas renderer (native winding rule). Method semantics follow the standard [`Path2D`](https://developer.mozilla.org/en-US/docs/Web/API/Path2D) API; see the updated **SVG Shapes** example. -- **`Texture2d` base class for texture assets** — a new abstract base (`me.Texture2d`) for user-constructed texture objects that own a drawable source, exposed via `getTexture()`. `TextureAtlas` now extends it (no behavior change), and the renderables (`Sprite`, `Sprite3d`, `Mesh`) recognize any `Texture2d` by type and resolve it to its backing canvas/image — so a texture asset can be passed directly (`{ image: myTexture }`) just like a raw canvas. Raw DOM images/canvases and the loader's decoded `CompressedImage` data remain valid sources outside the class hierarchy. +- **`Texture2d` base class for texture assets** — a new abstract base (`me.Texture2d`) for user-constructed texture objects that own a drawable source, exposed via `getTexture()`. `TextureAtlas` now extends it (no behavior change), and the renderables (`Sprite`, `Sprite3d`, `Mesh`) recognize any `Texture2d` by type and resolve it to its backing canvas/image — so a texture asset can be passed directly (`{ image: myTexture }`) just like a raw canvas. Raw DOM images/canvases and the loader's decoded `CompressedImage` data remain valid sources outside the class hierarchy. The contract deliberately admits both CPU-backed sources (a drawable canvas/image) and **GPU-resident** ones (a renderer texture resource that never leaves the GPU) — the shape a future WebGPU backend and screen-capture textures follow. - **Procedural noise: `Noise` + `NoiseTexture2d`** — `me.Noise` is a renderer-free coherent-noise generator (simplex / perlin / value / valueCubic / cellular, with fBm, ridged and ping-pong fractal layering, plus optional domain warp) that you can sample on the CPU via `getNoise2d` / `getNoise3d` for gameplay (heightmaps, terrain, spawn jitter) — fully deterministic per `seed`. `me.NoiseTexture2d` is a `Texture2d` that bakes a `Noise` field into a drawable canvas, with three output modes: grayscale, a `Gradient` **color ramp**, or a tangent-space **normal map** (`asNormalMap` + `bumpStrength`) for per-pixel lighting; optional `seamless` tiling (`seamlessBlendSkirt`); and **live animation** (`animated` + `speed` — sampled in 3D, advanced by `update(dt)`). Each re-bake bumps a content `version` stamped on the canvas; the WebGL lit pipeline re-uploads the normal-map texture only when that version changes (the three.js `Texture.needsUpdate` model — no renderer handle needed). Both work under the Canvas and WebGL backends. - **`shared` flag on `ShaderEffect` / `GLShader`** — set `effect.shared = true` on a post-processing shader that is reused across several renderables, so one renderable's cleanup (the `shader` setter, `removePostEffect`, `clearPostEffects`, or `destroy()`) doesn't free the GL program the others still use. Defaults to `false` (unchanged auto-destroy behavior); when set, you own the shader's lifecycle and call `destroy()` yourself. - **`ShaderEffect.setTime(seconds)`** — a convenience for the `uTime` shader-animation convention, now on the base class: call it once per frame to write a shader's `uniform float uTime` (e.g. to scroll a static texture's UVs, or pulse / wave), driven by whatever clock you choose. A no-op when the shader doesn't declare `uTime`, so it's safe on any effect. Manual by design — the engine never calls it, so animation stays opt-in. The built-in `Hologram` / `Shine` effects now inherit it (their identical `setTime` overrides were removed). -- **`ShaderEffect.setTexture(name, image[, repeat])`** (closes [#1532](https://github.com/melonjs/melonJS/issues/1532)) — bind an **extra** `sampler2D` to a custom post-effect, beyond the sprite/target it processes (`uSampler`) — a noise map, mask, gradient, or flow table. Declare `uniform sampler2D ;` in your fragment and pass any engine texture (e.g. `noiseTexture.getTexture()`); the engine uploads it once, caches it, and (re)binds it to a reserved texture unit each draw while pointing the sampler uniform at it — no raw WebGL texture-unit juggling. Works on both post-effect paths (single-effect sprite shader and the multi-effect / camera FBO blit). Enables UV-distortion water, dissolve masks, flow maps, palette lookups, etc. Purely additive — effects that never call it are unchanged. See the new **Water Refraction** example (a perspective pool floor refracted through a GPU-scrolled seamless `NoiseTexture2d`). +- **`ShaderEffect.setTexture(name, image[, repeat])`** (closes [#1532](https://github.com/melonjs/melonJS/issues/1532)) — bind an **extra** `sampler2D` to a custom post-effect, beyond the sprite/target it processes (`uSampler`) — a noise map, mask, gradient, or flow table. Declare `uniform sampler2D ;` in your fragment and pass any engine texture — a `Texture2d` asset (`NoiseTexture2d`, `TextureAtlas`, …) directly, or a raw drawable source; the engine uploads it once, caches it, and (re)binds it to a reserved texture unit each draw while pointing the sampler uniform at it — no raw WebGL texture-unit juggling. Works on both post-effect paths (single-effect sprite shader and the multi-effect / camera FBO blit). Enables UV-distortion water, dissolve masks, flow maps, palette lookups, etc. Purely additive — effects that never call it are unchanged. See the new **Water Refraction** example (a perspective pool floor refracted through a GPU-scrolled seamless `NoiseTexture2d`). - **Anchor-point presets + `Sprite3d` anchor support** (#1514) — `settings.anchorPoint` now accepts the named presets `"center"`, `"top"`, `"bottom"`, `"left"`, `"right"`, `"top-left"`, `"top-right"`, `"bottom-left"`, `"bottom-right"` in addition to an `{x, y}` object, uniformly across `Sprite`, `Entity`, `Collectable`, `ImageLayer`, `Text`, `BitmapText` (and their subclasses), including as a plain-string Tiled property. `Sprite3d` gains anchoring through the same `settings.anchorPoint` key (centered default, same convention as `Sprite`): the anchor is baked into the quad's local vertices so it composes with billboarding, flips and trimmed/rotated atlas frames, never leaks into the renderer transform on either camera path, is mutable at runtime via `sprite3d.anchorPoint.set(...)` (automatic re-bake + cull-bounds update), and grows the frustum-cull bounds exactly so an anchored sprite can't pop out while still on screen. On the 2D classes invalid values (unknown preset, malformed object) keep their historical silent `(0, 0)` outcome for full backward compatibility, but now log a console warning; `Sprite3d` (a new surface) throws. See the updated billboard example (`anchorPoint: "bottom"` — feet on the ground plane). - **Shader preloading: `"shader"` asset type + `loader.getShader()` + `ShaderEffect.clone()`** — custom shaders can now be preloaded like any other asset and are **compiled at load time** (the GLSL compile cost lands in the loading screen, and a compile error fails the load — `loader.load()` rejects with an error carrying the asset's name). The source is a GLSL fragment body following the `ShaderEffect` convention (`uniform` declarations + `vec4 apply(vec4 color, vec2 uv)`), loaded from a `src` URL / data: URI or inline via the `data` field (the inline-TMX convention): `{ name: "waterRipple", type: "shader", src: "shaders/waterRipple.frag" }`. `loader.getShader(name)` returns the **shared, loader-owned instance** — the *same* object on every call, with `shared = true` so renderable cleanup never auto-destroys it; everything it is assigned to shares one set of uniform values, and it is freed only by `loader.unload()` / `loader.unloadAll()` (which destroy the GL program). For per-renderable uniform values, the new `ShaderEffect.clone()` compiles an independent, caller-owned copy: it copies the *recipe* (fragment source, precision, uniform values, `setTexture` bindings — with its own GL uploads) but never the *ownership* — the clone's `shared` flag is **always reset to `false`**, so it is auto-destroyed with the renderable it is assigned to unless explicitly re-shared. `GLShader.clone()` exists likewise for raw full-program (vertex + fragment) shaders, with the same recipe-not-ownership semantics. The **Water Refraction** example now ships its fragment as a preloaded shader asset. diff --git a/packages/melonjs/src/video/texture/texture2d.ts b/packages/melonjs/src/video/texture/texture2d.ts index cfe03bc94..4bbf42099 100644 --- a/packages/melonjs/src/video/texture/texture2d.ts +++ b/packages/melonjs/src/video/texture/texture2d.ts @@ -1,31 +1,52 @@ +import type { TextureResource } from "./resource.js"; + +/** + * The backing source a {@link Texture2d} resolves to through + * {@link Texture2d#getTexture}: a drawable (canvas/image) for CPU-backed + * assets, or a renderer texture resource for GPU-resident ones — a resource + * uploads/binds itself through the texture cache instead of being read back + * to the CPU. + */ +export type Texture2dSource = + | HTMLCanvasElement + | HTMLImageElement + | OffscreenCanvas + | ImageBitmap + | TextureResource; + /** - * Abstract base for a user-constructed 2D texture asset — an object that owns - * a drawable image source and can be used anywhere the engine expects an - * image: {@link Sprite#image}, {@link Sprite#normalMap}, an {@link ImageLayer}, - * or bound as a sampler uniform in a custom {@link GLShader}. + * Abstract base for a 2D texture asset — an object that owns a texture + * source and can be used anywhere the engine expects an image: + * {@link Sprite#image}, {@link Sprite#normalMap}, an {@link ImageLayer}, or + * bound as a sampler uniform in a custom shader (see + * {@link ShaderEffect#setTexture}). + * + * A `Texture2d` is recognized via `instanceof` and resolved to its backing + * source through {@link Texture2d#getTexture} — so passing the asset object + * directly (`{ image: myTexture }`) works the same as passing a raw + * `HTMLCanvasElement`. Raw DOM image/canvas sources and the loader's decoded + * `CompressedImage` data are accepted too, but are not part of this class + * hierarchy. * - * A `Texture2d` is recognized by the renderables via `instanceof` and resolved - * to its backing canvas/image through {@link Texture2d#getTexture} — so passing - * the asset object directly (`{ image: myTexture }`) works the same as passing a - * raw `HTMLCanvasElement`. Raw DOM image/canvas sources and the loader's - * decoded `CompressedImage` data are accepted too, but are not part of this - * class hierarchy. + * Most assets are CPU-backed and resolve to a drawable canvas/image. + * Subclasses may also be **GPU-resident**, resolving to a renderer texture + * resource that never leaves the GPU — the contract deliberately admits both + * (a future WebGPU backend follows the same shape with a `GPUTexture`-backed + * resource). * * Concrete implementations: * - {@link TextureAtlas} — packed multi-region sprite sheet - * - * A future WebGPU implementation would back {@link Texture2d#getTexture} with a - * `GPUTexture`-wrapping surface rather than an `HTMLCanvasElement`. * @category Game Objects */ export default abstract class Texture2d { /** - * Return the drawable source for this texture — assignable to - * {@link Sprite#image}, {@link Sprite#normalMap}, an {@link ImageLayer}, or - * bound as a sampler uniform in a custom shader. - * @returns the backing canvas/image + * Return the backing source for this texture — a drawable canvas/image + * for CPU-backed assets, or a renderer texture resource for GPU-resident + * ones. Assignable to {@link Sprite#image}, {@link Sprite#normalMap}, an + * {@link ImageLayer}, or bound as a sampler uniform in a custom shader. + * @returns the backing source */ - abstract getTexture(): HTMLCanvasElement | HTMLImageElement; + abstract getTexture(): Texture2dSource; /** * Release any GPU/CPU resources held by this texture. The texture must not diff --git a/packages/melonjs/src/video/webgl/shadereffect.js b/packages/melonjs/src/video/webgl/shadereffect.js index 8250f36fa..ef89cb9a5 100644 --- a/packages/melonjs/src/video/webgl/shadereffect.js +++ b/packages/melonjs/src/video/webgl/shadereffect.js @@ -4,6 +4,7 @@ import { off, on, } from "../../system/event.ts"; +import Texture2d from "../texture/texture2d.ts"; import GLShader from "./glshader.js"; import quadVertex from "./shaders/quad.vert"; @@ -223,10 +224,11 @@ export default class ShaderEffect { * texture-unit juggling. * * Declare the sampler in your fragment (`uniform sampler2D ;`) and pass - * that name here. Any engine texture works — e.g. `noiseTexture.getTexture()`. - * No-op in Canvas mode. + * that name here. Any engine texture works — a {@link Texture2d} asset + * (`NoiseTexture2d`, `TextureAtlas`, …) can be passed directly, or a raw + * drawable source. No-op in Canvas mode. * @param {string} name - the `sampler2D` uniform name declared in the fragment - * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image - the texture source + * @param {Texture2d|HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image - the texture: an engine texture asset, or a raw drawable source * @param {"repeat"|"repeat-x"|"repeat-y"|"no-repeat"} [repeat="no-repeat"] - wrap mode; use `"repeat"` for a tiled/scrolled texture (power-of-two size under WebGL 1) * @returns {ShaderEffect} this effect for chaining * @example @@ -239,12 +241,16 @@ export default class ShaderEffect { * vec2 flow = texture2D(uNoise, uv + uTime * 0.03).rg - 0.5; * return texture2D(uSampler, uv + flow * 0.02); * }`); - * water.setTexture("uNoise", noise.getTexture(), "repeat"); + * water.setTexture("uNoise", noise, "repeat"); * waterSprite.shader = water; * // each frame, in your Stage's update(dt): * water.setTime(me.timer.getTime() / 1000); */ setTexture(name, image, repeat = "no-repeat") { + // accept a Texture2d asset directly — resolve it to its backing source + if (image instanceof Texture2d) { + image = image.getTexture(); + } if (this.enabled) { const existing = this._extraTextures.get(name); if (existing) { diff --git a/packages/melonjs/tests/shadereffect-settexture.spec.js b/packages/melonjs/tests/shadereffect-settexture.spec.js index 17c1b527f..58ce5158f 100644 --- a/packages/melonjs/tests/shadereffect-settexture.spec.js +++ b/packages/melonjs/tests/shadereffect-settexture.spec.js @@ -1,5 +1,12 @@ import { beforeAll, describe, expect, it } from "vitest"; -import { boot, ShaderEffect, video, WebGLRenderer } from "../src/index.js"; +import { + boot, + NoiseTexture2d, + ShaderEffect, + Texture2d, + video, + WebGLRenderer, +} from "../src/index.js"; /** * `ShaderEffect.setTexture(name, image)` binds an extra `sampler2D` (a noise @@ -87,6 +94,59 @@ describe("ShaderEffect.setTexture (extra sampler binding)", () => { extra.destroy(); }); + it("accepts a Texture2d asset directly, without the .getTexture() dance", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + const blue = solidCanvas(0, 0, 255); + + // a minimal Texture2d subclass wrapping a red canvas — the same + // contract every texture asset (TextureAtlas, NoiseTexture2d…) + // implements + class CanvasTexture extends Texture2d { + constructor(canvas) { + super(); + this._canvas = canvas; + } + getTexture() { + return this._canvas; + } + } + + const effect = new ShaderEffect( + renderer, + "uniform sampler2D uExtra;\nvec4 apply(vec4 color, vec2 uv) { return texture2D(uExtra, uv); }", + ); + // the asset object itself — not asset.getTexture() + effect.setTexture("uExtra", new CanvasTexture(solidCanvas(255, 0, 0))); + const sampled = drawCentrePixel(effect, blue); + expect(sampled[0]).toBeGreaterThan(200); // red — asset resolved + sampled + expect(sampled[2]).toBeLessThan(60); + effect.destroy(); + }); + + it("accepts a NoiseTexture2d asset directly (the documented idiom, minus .getTexture())", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + const blue = solidCanvas(0, 0, 255); + const noise = new NoiseTexture2d({ width: SIZE, height: SIZE }); + const effect = new ShaderEffect( + renderer, + "uniform sampler2D uNoise;\nvec4 apply(vec4 color, vec2 uv) { return vec4(vec3(texture2D(uNoise, uv).r), 1.0); }", + ); + effect.setTexture("uNoise", noise); + const sampled = drawCentrePixel(effect, blue); + // noise output is greyscale: not the blue source, and alpha intact — + // proves the asset was resolved to its baked canvas and sampled + expect(sampled[0]).toBe(sampled[2]); // grey: r === b + expect(sampled[3]).toBe(255); + effect.destroy(); + noise.destroy(); + }); + it("uploads to a reserved unit, sets the sampler, caches, and frees on destroy", (ctx) => { if (!isWebGL) { ctx.skip(); From 472caeca1019a7b45bce0052a64acbf68000a618 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Wed, 8 Jul 2026 15:03:46 +0800 Subject: [PATCH 02/20] test: Canvas-stub coverage for setTexture(Texture2d) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unwrap runs before the enabled check, so an inert Canvas-mode stub receiving a Texture2d asset must still no-op safely — one more cell in the acceptance matrix for the direct-asset form. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- packages/melonjs/tests/shader-canvas.spec.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/melonjs/tests/shader-canvas.spec.js b/packages/melonjs/tests/shader-canvas.spec.js index 6909f9fdb..b5c476b58 100644 --- a/packages/melonjs/tests/shader-canvas.spec.js +++ b/packages/melonjs/tests/shader-canvas.spec.js @@ -1,5 +1,5 @@ import { beforeAll, describe, expect, it } from "vitest"; -import { boot, loader, ShaderEffect, video } from "../src/index.js"; +import { boot, loader, ShaderEffect, Texture2d, video } from "../src/index.js"; /** * Shader assets under a CANVAS renderer (e.g. a video.AUTO fallback): @@ -26,11 +26,18 @@ describe("shader assets under the Canvas renderer", () => { expect(fx.enabled).toBe(false); // born disabled → filtered from post-effect passes expect(fx.shared).toBe(true); // still loader-owned - // every method no-ops without throwing + // every method no-ops without throwing — including a Texture2d asset + // passed directly (the unwrap runs before the enabled check) + class CanvasTexture extends Texture2d { + getTexture() { + return document.createElement("canvas"); + } + } expect(() => { fx.setUniform("uIntensity", 1.0); fx.setTime(1.0); fx.setTexture("uNoise", document.createElement("canvas")); + fx.setTexture("uNoise2", new CanvasTexture()); }).not.toThrow(); // clone still works (the recipe is stored before the Canvas-mode From 9923a33d5ec8bf1dec657e9e73590fd9431be386 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Wed, 8 Jul 2026 15:21:07 +0800 Subject: [PATCH 03/20] fix(shader): setUniform/setTexture no longer drop values while disabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by re-examining the GL-core audit's rejected-findings list after the maintainer asked what pending review items link to this PR: the finder had flagged (and under-graded) a real inconsistency in the exact method this PR widens. _onContextLost auto-sets enabled = false; setUniform was gated on enabled — so a uniform set during the loss window was silently dropped ONE LAYER ABOVE the GLShader suspend-cache replay built precisely for that window (19.6), reverting to its pre-loss value after restore. The same gate ate setTexture bindings (permanently — no per-frame refresh to self-heal) and also dropped values while the USER had merely disabled an effect, no context loss required. - setUniform: forwards whenever a shader exists — GLShader already handles live vs suspended (cache deferral) correctly - setTexture: stores its entry regardless of enabled; the GL work is lazy in _prepareTextures anyway (loss handler nulls tex/unit, so the replacement cleanup is naturally safe mid-window). Canvas stubs keep the inert no-op via the no-shader guard - setTime: gate changed from enabled to genuinely-suspended (uniforms is null mid-loss; a per-frame call self-heals next frame) Red-first: user-disabled setUniform + mid-loss setUniform/setTexture survival across restore — both red pre-fix. Full suite green. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- packages/melonjs/CHANGELOG.md | 1 + .../melonjs/src/video/webgl/shadereffect.js | 58 +++++++---- .../tests/shadereffect-settexture.spec.js | 96 +++++++++++++++++++ 3 files changed, 136 insertions(+), 19 deletions(-) diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index ee8247f5a..8ea75cff8 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -36,6 +36,7 @@ - **`Rect.right`/`Rect.bottom` returned the width/height when the edge sat exactly at coordinate 0** — the getters computed `this.left + w || w`, so an edge landing precisely on 0 (falsy) fell back to the size. Bounds and collision math around the origin came out wrong for such rectangles. - **`Rect.toPolygon()` handed out the rectangle's live vertices** — `Polygon.setVertices` stores a `Vector2d[]` by reference, so the "new" polygon shared the rect's actual points array: transforming or mutating the polygon silently corrupted the rectangle. The vertices are now cloned. - **`loader.load()` mutated the caller's asset descriptor, so retries double-prepended `baseURL`** — the resolved URL (fontface `url()` unwrap + `baseURL` prefix) was written back into `asset.src`; `loader.reload()` (which re-loads the same stored object) — or simply `load()`ing the same manifest entry twice — then fetched `base + base + src`. The URL is now resolved into a copy handed to the parser; the caller's object is never touched. +- **`ShaderEffect.setUniform()` silently dropped values while the effect was disabled** — including during a context-loss window, where the auto-disable gate defeated the very suspend-cache replay built for it (a uniform set mid-loss reverted to its pre-loss value after restore); a user-disabled effect lost values the same way. `setUniform` now always forwards to the inner shader (which correctly defers while suspended); `setTexture` likewise stores its binding regardless of the enabled state for the lazy upload on the next enabled draw, and `setTime` skips only while genuinely suspended. Canvas-mode stubs keep no-oping. - **`loader.unload()` of a never-loaded fontface threw a TypeError** — the fontface case was the only one without a membership guard, and `FontFaceSet.delete(undefined)` is a WebIDL type error rather than a `false` return. It now returns `false` like every other asset type. - **Re-preloading an audio manifest silently replaced each Howl and leaked the old one** — the audio parser was the only asset parser without an already-loaded guard, so returning to a stage that preloads (a common pattern) churned every clip's decoded buffers / HTML5 nodes. An already-loaded clip is now reported as cached (like every other asset type); `me.audio.unload()` first to genuinely reload one. - **Parallel audio loads shared a single retry counter** — three failures of one flaky file pushed *another* sound's first failure straight over the give-up threshold, throwing a spurious fatal load error (the default `stopOnAudioError`). Retries are now budgeted per sound. diff --git a/packages/melonjs/src/video/webgl/shadereffect.js b/packages/melonjs/src/video/webgl/shadereffect.js index ef89cb9a5..9ee379057 100644 --- a/packages/melonjs/src/video/webgl/shadereffect.js +++ b/packages/melonjs/src/video/webgl/shadereffect.js @@ -174,7 +174,12 @@ export default class ShaderEffect { * @param {object|Float32Array} value - the value to assign to that uniform */ setUniform(name, value) { - if (this.enabled) { + // forward whenever a shader exists (WebGL mode): GLShader handles the + // suspended (context-lost) state by deferring to its uniform cache. + // Gating on `enabled` here silently dropped values set during a loss + // window — defeating that replay — or while the user had the effect + // disabled. Canvas stubs have no shader and keep no-oping. + if (typeof this._shader !== "undefined") { this._shader.setUniform(name, value); } } @@ -209,7 +214,14 @@ export default class ShaderEffect { // would make setUniform throw), and not cached (so it stays correct // across a context-loss recompile). `enabled` is false while suspended // or destroyed, so the uniforms map is never null here. - if (this.enabled && typeof this._shader.uniforms.uTime !== "undefined") { + // skipped while suspended (uniforms === null mid-context-loss; a + // per-frame call self-heals on the next frame) and on Canvas stubs; + // a USER-disabled but live effect still takes the value + if ( + typeof this._shader !== "undefined" && + !this._shader.suspended && + typeof this._shader.uniforms.uTime !== "undefined" + ) { this._shader.setUniform("uTime", seconds); } return this; @@ -247,29 +259,37 @@ export default class ShaderEffect { * water.setTime(me.timer.getTime() / 1000); */ setTexture(name, image, repeat = "no-repeat") { + // Canvas stub: no shader, keep the inert no-op + if (typeof this._shader === "undefined") { + return this; + } // accept a Texture2d asset directly — resolve it to its backing source if (image instanceof Texture2d) { image = image.getTexture(); } - if (this.enabled) { - const existing = this._extraTextures.get(name); - if (existing) { - // release the previous GL texture + unit reservation before - // replacing the binding - if (existing.tex !== null) { - this._shader.gl.deleteTexture(existing.tex); - } - if (existing.unit !== undefined) { - this._renderer.cache.releaseUnit(existing.unit); - } + // store the entry regardless of `enabled`: the GL work happens lazily + // in _prepareTextures on the next enabled draw, so a binding set while + // the effect is disabled — or mid-context-loss, where entries set + // during the window used to vanish permanently — survives intact + // (the loss handler already nulled `tex`/`unit`, so the replacement + // cleanup below is naturally safe in that window) + const existing = this._extraTextures.get(name); + if (existing) { + // release the previous GL texture + unit reservation before + // replacing the binding + if (existing.tex !== null) { + this._shader.gl.deleteTexture(existing.tex); + } + if (existing.unit !== undefined) { + this._renderer.cache.releaseUnit(existing.unit); } - this._extraTextures.set(name, { - image, - repeat, - tex: null, - unit: undefined, - }); } + this._extraTextures.set(name, { + image, + repeat, + tex: null, + unit: undefined, + }); return this; } diff --git a/packages/melonjs/tests/shadereffect-settexture.spec.js b/packages/melonjs/tests/shadereffect-settexture.spec.js index 58ce5158f..9aa80963b 100644 --- a/packages/melonjs/tests/shadereffect-settexture.spec.js +++ b/packages/melonjs/tests/shadereffect-settexture.spec.js @@ -221,3 +221,99 @@ describe("ShaderEffect.setTexture (extra sampler binding)", () => { expect(cache.reservedUnits.has(unit)).toBe(false); }); }); + +/** + * setUniform / setTexture must not silently drop values while the effect is + * disabled — whether by the user, or automatically during a context-loss + * window (where dropping defeats GLShader's suspend-cache replay: the value + * set mid-loss should survive the restore). Found by the 2026-07 GL-core + * audit; runs LAST in this file because losing the context disrupts GL state. + */ +describe("ShaderEffect value-setting while disabled (audit fix)", () => { + let renderer; + let gl; + let isWebGL; + + beforeAll(() => { + boot(); + video.init(SIZE, SIZE, { + parent: "screen", + renderer: video.WEBGL, + failIfMajorPerformanceCaveat: false, + antiAlias: false, + }); + renderer = video.renderer; + isWebGL = renderer instanceof WebGLRenderer; + if (isWebGL) { + gl = renderer.gl; + } + }); + + const tick = () => { + return new Promise((resolve) => { + setTimeout(resolve, 0); + }); + }; + + it("setUniform while USER-disabled applies once re-enabled", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + const fx = new ShaderEffect( + renderer, + "uniform float uIntensity;\nvec4 apply(vec4 color, vec2 uv) { return color * uIntensity; }", + ); + fx.setUniform("uIntensity", 0.25); + fx.enabled = false; + fx.setUniform("uIntensity", 0.75); // must not be dropped + fx.enabled = true; + const loc = fx._shader.gl.getUniformLocation( + fx._shader.program, + "uIntensity", + ); + expect(fx._shader.gl.getUniform(fx._shader.program, loc)).toBeCloseTo( + 0.75, + 5, + ); + fx.destroy(); + }); + + it("setUniform + setTexture during a context-loss window survive the restore", async (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + const ext = gl.getExtension("WEBGL_lose_context"); + if (!ext) { + ctx.skip(); + return; + } + const fx = new ShaderEffect( + renderer, + "uniform float uIntensity;\nuniform sampler2D uExtra;\nvec4 apply(vec4 color, vec2 uv) { return texture2D(uExtra, uv) * uIntensity; }", + ); + fx.setUniform("uIntensity", 0.25); + + ext.loseContext(); + await tick(); + expect(fx.enabled).toBe(false); // auto-disabled during the window + + // values set MID-LOSS must survive: setUniform defers to the + // GLShader suspend cache; setTexture stores its entry for the lazy + // _prepareTextures upload on the next enabled draw + fx.setUniform("uIntensity", 0.75); + fx.setTexture("uExtra", solidCanvas(255, 0, 0)); + + ext.restoreContext(); + await tick(); + await tick(); + expect(fx.enabled).toBe(true); + + const shader = fx._shader; + const loc = shader.gl.getUniformLocation(shader.program, "uIntensity"); + expect(shader.gl.getUniform(shader.program, loc)).toBeCloseTo(0.75, 5); + expect(fx._extraTextures.has("uExtra")).toBe(true); + fx.destroy(); + }); +}); From 0baac96ce734421c72af8e03be130aa1347aeec5 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Wed, 8 Jul 2026 16:13:42 +0800 Subject: [PATCH 04/20] fix(shader): keep the destroy() no-op guard on the ungated setters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ungating (previous commit) forwarded setUniform/setTexture/setTime whenever a shader existed — but a DESTROYED ShaderEffect still holds its _shader reference (partial-state immunity, tested in webgl_pipeline_adversarial), and GLShader.setUniform on a destroyed program throws "undefined (name) uniform". Re-add the destroyed guard so destroy-then-set stays an inert no-op, matching the pre-existing immunity contract, while a merely-disabled or context-lost effect keeps the new behaviour. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- .../melonjs/src/video/webgl/shadereffect.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/melonjs/src/video/webgl/shadereffect.js b/packages/melonjs/src/video/webgl/shadereffect.js index 9ee379057..9be37f34e 100644 --- a/packages/melonjs/src/video/webgl/shadereffect.js +++ b/packages/melonjs/src/video/webgl/shadereffect.js @@ -174,12 +174,13 @@ export default class ShaderEffect { * @param {object|Float32Array} value - the value to assign to that uniform */ setUniform(name, value) { - // forward whenever a shader exists (WebGL mode): GLShader handles the - // suspended (context-lost) state by deferring to its uniform cache. - // Gating on `enabled` here silently dropped values set during a loss - // window — defeating that replay — or while the user had the effect - // disabled. Canvas stubs have no shader and keep no-oping. - if (typeof this._shader !== "undefined") { + // forward whenever a live shader exists (WebGL mode): GLShader handles + // the suspended (context-lost) state by deferring to its uniform + // cache. Gating on `enabled` here silently dropped values set during + // a loss window — defeating that replay — or while the user had the + // effect disabled. Canvas stubs (no shader) and destroyed effects + // (partial-state immunity, see destroy()) keep no-oping. + if (typeof this._shader !== "undefined" && this.destroyed !== true) { this._shader.setUniform(name, value); } } @@ -219,6 +220,7 @@ export default class ShaderEffect { // a USER-disabled but live effect still takes the value if ( typeof this._shader !== "undefined" && + this.destroyed !== true && !this._shader.suspended && typeof this._shader.uniforms.uTime !== "undefined" ) { @@ -259,8 +261,8 @@ export default class ShaderEffect { * water.setTime(me.timer.getTime() / 1000); */ setTexture(name, image, repeat = "no-repeat") { - // Canvas stub: no shader, keep the inert no-op - if (typeof this._shader === "undefined") { + // Canvas stub (no shader) and destroyed effects: keep the inert no-op + if (typeof this._shader === "undefined" || this.destroyed === true) { return this; } // accept a Texture2d asset directly — resolve it to its backing source From 912cd3b272894ee8f0f5c807bdff0e0d448dc787 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Wed, 8 Jul 2026 16:35:08 +0800 Subject: [PATCH 05/20] =?UTF-8?q?refactor(texture):=20address=20PR=20#1543?= =?UTF-8?q?=20review=20=E2=80=94=20opaque=20type,=20unit=20reuse,=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review on #1543: - Texture2dSource named the internal TextureResource in an EXPORTED type, making it a de-facto public API commitment. Replaced with a new opaque GPUResidentTexture marker interface — the union no longer references any internal class, and TextureResource stays internal (its only consumers are the tmxlayer + material batcher, unchanged). - setTexture released a replaced sampler's reserved unit and re-reserved it lazily on the next enabled draw. Now that setTexture stores bindings while disabled, that window is unbounded — the cache allocator could hand the high unit to a regular texture, reintroducing the collision reserveUnit() prevents. Keep the reservation across a replace; release only on context loss / destroy. New regression test pins it (unit held + allocator still skips it while disabled, new image uploads into the same unit on re-enable). - CHANGELOG Texture2d bullet contradicted itself ("owns a drawable source" / "backing canvas/image" vs "admits GPU-resident"). Reworded to "texture source" / "backing source". - shader-canvas test comment claimed the Texture2d unwrap runs before an enabled check; in Canvas mode setTexture returns early on the no-shader guard, so getTexture() is never called. Comment corrected. setUniform/setTexture resolving the asset while a WEBGL effect is merely disabled is intentional (the audit drop-fix): the value/binding must survive to apply on re-enable. The Canvas no-op contract is preserved by the no-shader early return. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- packages/melonjs/CHANGELOG.md | 2 +- .../melonjs/src/video/texture/texture2d.ts | 25 +++++++-- .../melonjs/src/video/webgl/shadereffect.js | 26 ++++----- packages/melonjs/tests/shader-canvas.spec.js | 3 +- .../tests/shadereffect-settexture.spec.js | 56 +++++++++++++++++++ 5 files changed, 92 insertions(+), 20 deletions(-) diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index 8ea75cff8..732a04dcd 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -5,7 +5,7 @@ ### Added - **BMFont XML font support for `BitmapText`** — bitmap fonts can now be loaded from the AngelCode BMFont **XML** flavour in addition to the text `.fnt` format (the serialisation is auto-detected). Many bitmap-font tools and asset packs (itch.io, dafont, …) export PNG + XML; those now load directly through the standard preloader (`{ type: "binary" }`), with no offline conversion step. The XML is parsed with a dependency-free regex parser (no `DOMParser`), so it also works outside the browser (Node / SSR) — unlike the DOM-based TMX XML path. - **Holes & compound paths in `Path2D` / SVG fills** (#1253) — a path may now contain multiple sub-paths (every `M` in an SVG string, or every `moveTo()` call, starts a new one). On `fill()` the first sub-path is the outer contour and each subsequent sub-path is treated as a hole, so donuts, rings, letter counters ("O", "A", "8") and other shapes with holes render correctly under both the WebGL renderer (earcut hole triangulation) and the Canvas renderer (native winding rule). Method semantics follow the standard [`Path2D`](https://developer.mozilla.org/en-US/docs/Web/API/Path2D) API; see the updated **SVG Shapes** example. -- **`Texture2d` base class for texture assets** — a new abstract base (`me.Texture2d`) for user-constructed texture objects that own a drawable source, exposed via `getTexture()`. `TextureAtlas` now extends it (no behavior change), and the renderables (`Sprite`, `Sprite3d`, `Mesh`) recognize any `Texture2d` by type and resolve it to its backing canvas/image — so a texture asset can be passed directly (`{ image: myTexture }`) just like a raw canvas. Raw DOM images/canvases and the loader's decoded `CompressedImage` data remain valid sources outside the class hierarchy. The contract deliberately admits both CPU-backed sources (a drawable canvas/image) and **GPU-resident** ones (a renderer texture resource that never leaves the GPU) — the shape a future WebGPU backend and screen-capture textures follow. +- **`Texture2d` base class for texture assets** — a new abstract base (`me.Texture2d`) for user-constructed texture objects that own a texture source, exposed via `getTexture()`. `TextureAtlas` now extends it (no behavior change), and the renderables (`Sprite`, `Sprite3d`, `Mesh`) recognize any `Texture2d` by type and resolve it to its backing source — so a texture asset can be passed directly (`{ image: myTexture }`) just like a raw canvas. Raw DOM images/canvases and the loader's decoded `CompressedImage` data remain valid sources outside the class hierarchy. The contract deliberately admits both CPU-backed sources (a drawable canvas/image) and **GPU-resident** ones (an opaque backing that never leaves the GPU) — the shape a future WebGPU backend and screen-capture textures follow. - **Procedural noise: `Noise` + `NoiseTexture2d`** — `me.Noise` is a renderer-free coherent-noise generator (simplex / perlin / value / valueCubic / cellular, with fBm, ridged and ping-pong fractal layering, plus optional domain warp) that you can sample on the CPU via `getNoise2d` / `getNoise3d` for gameplay (heightmaps, terrain, spawn jitter) — fully deterministic per `seed`. `me.NoiseTexture2d` is a `Texture2d` that bakes a `Noise` field into a drawable canvas, with three output modes: grayscale, a `Gradient` **color ramp**, or a tangent-space **normal map** (`asNormalMap` + `bumpStrength`) for per-pixel lighting; optional `seamless` tiling (`seamlessBlendSkirt`); and **live animation** (`animated` + `speed` — sampled in 3D, advanced by `update(dt)`). Each re-bake bumps a content `version` stamped on the canvas; the WebGL lit pipeline re-uploads the normal-map texture only when that version changes (the three.js `Texture.needsUpdate` model — no renderer handle needed). Both work under the Canvas and WebGL backends. - **`shared` flag on `ShaderEffect` / `GLShader`** — set `effect.shared = true` on a post-processing shader that is reused across several renderables, so one renderable's cleanup (the `shader` setter, `removePostEffect`, `clearPostEffects`, or `destroy()`) doesn't free the GL program the others still use. Defaults to `false` (unchanged auto-destroy behavior); when set, you own the shader's lifecycle and call `destroy()` yourself. - **`ShaderEffect.setTime(seconds)`** — a convenience for the `uTime` shader-animation convention, now on the base class: call it once per frame to write a shader's `uniform float uTime` (e.g. to scroll a static texture's UVs, or pulse / wave), driven by whatever clock you choose. A no-op when the shader doesn't declare `uTime`, so it's safe on any effect. Manual by design — the engine never calls it, so animation stays opt-in. The built-in `Hologram` / `Shine` effects now inherit it (their identical `setTime` overrides were removed). diff --git a/packages/melonjs/src/video/texture/texture2d.ts b/packages/melonjs/src/video/texture/texture2d.ts index 4bbf42099..3b300db9f 100644 --- a/packages/melonjs/src/video/texture/texture2d.ts +++ b/packages/melonjs/src/video/texture/texture2d.ts @@ -1,18 +1,33 @@ -import type { TextureResource } from "./resource.js"; +/** + * An opaque, GPU-resident texture backing — the source a GPU-resident + * {@link Texture2d} resolves to (for example the framebuffer copy a future + * `renderer.toFrameTexture()` produces). Such a backing uploads/binds itself + * through the texture cache instead of being read back to the CPU. Its + * concrete shape is an engine internal that may change between releases: + * obtain values from the engine and pass them back to the engine — do not + * construct or inspect them. + */ +export interface GPUResidentTexture { + /** + * Structural brand only — GPU-resident backings are opaque and expose no + * public runtime member. Present so this stays distinct from a plain + * object without naming any internal class in the public type surface. + * @ignore + */ + readonly __gpuResidentTexture?: never; +} /** * The backing source a {@link Texture2d} resolves to through * {@link Texture2d#getTexture}: a drawable (canvas/image) for CPU-backed - * assets, or a renderer texture resource for GPU-resident ones — a resource - * uploads/binds itself through the texture cache instead of being read back - * to the CPU. + * assets, or an opaque {@link GPUResidentTexture} for GPU-resident ones. */ export type Texture2dSource = | HTMLCanvasElement | HTMLImageElement | OffscreenCanvas | ImageBitmap - | TextureResource; + | GPUResidentTexture; /** * Abstract base for a 2D texture asset — an object that owns a texture diff --git a/packages/melonjs/src/video/webgl/shadereffect.js b/packages/melonjs/src/video/webgl/shadereffect.js index 9be37f34e..b2cae2b14 100644 --- a/packages/melonjs/src/video/webgl/shadereffect.js +++ b/packages/melonjs/src/video/webgl/shadereffect.js @@ -272,25 +272,25 @@ export default class ShaderEffect { // store the entry regardless of `enabled`: the GL work happens lazily // in _prepareTextures on the next enabled draw, so a binding set while // the effect is disabled — or mid-context-loss, where entries set - // during the window used to vanish permanently — survives intact - // (the loss handler already nulled `tex`/`unit`, so the replacement - // cleanup below is naturally safe in that window) + // during the window used to vanish permanently — survives intact. const existing = this._extraTextures.get(name); - if (existing) { - // release the previous GL texture + unit reservation before - // replacing the binding - if (existing.tex !== null) { - this._shader.gl.deleteTexture(existing.tex); - } - if (existing.unit !== undefined) { - this._renderer.cache.releaseUnit(existing.unit); - } + if (existing && existing.tex !== null) { + // drop the old GL texture so the new image re-uploads into the + // SAME reserved unit (kept below) on the next enabled draw + this._shader.gl.deleteTexture(existing.tex); } this._extraTextures.set(name, { image, repeat, tex: null, - unit: undefined, + // keep any unit already reserved for this sampler across the + // replace: releasing it here (then re-reserving lazily) would open + // a window — unbounded while the effect is disabled — in which the + // cache allocator could hand that high unit to a regular texture, + // reintroducing the collision reserveUnit() exists to prevent. The + // reservation is released only where the unit is truly invalid: + // context loss (_onContextLost) and destroy(). + unit: existing ? existing.unit : undefined, }); return this; } diff --git a/packages/melonjs/tests/shader-canvas.spec.js b/packages/melonjs/tests/shader-canvas.spec.js index b5c476b58..da2c7d109 100644 --- a/packages/melonjs/tests/shader-canvas.spec.js +++ b/packages/melonjs/tests/shader-canvas.spec.js @@ -27,7 +27,8 @@ describe("shader assets under the Canvas renderer", () => { expect(fx.shared).toBe(true); // still loader-owned // every method no-ops without throwing — including a Texture2d asset - // passed directly (the unwrap runs before the enabled check) + // passed directly: the Canvas stub has no `_shader`, so setTexture + // returns early before any unwrap (getTexture() is never called) class CanvasTexture extends Texture2d { getTexture() { return document.createElement("canvas"); diff --git a/packages/melonjs/tests/shadereffect-settexture.spec.js b/packages/melonjs/tests/shadereffect-settexture.spec.js index 9aa80963b..f9c84c185 100644 --- a/packages/melonjs/tests/shadereffect-settexture.spec.js +++ b/packages/melonjs/tests/shadereffect-settexture.spec.js @@ -220,6 +220,62 @@ describe("ShaderEffect.setTexture (extra sampler binding)", () => { fx.destroy(); expect(cache.reservedUnits.has(unit)).toBe(false); }); + + // #1543 review (Copilot): replacing a bound sampler must KEEP its reserved + // unit — releasing it (then re-reserving lazily) opens a window, unbounded + // while the effect is disabled, in which the allocator could hand that unit + // to a regular texture and reintroduce the collision reserveUnit() prevents. + it("keeps the reserved unit when a bound sampler is replaced while disabled", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + const cache = renderer.cache; + cache.resetUnitAssignments(); + + const fx = new ShaderEffect( + renderer, + "uniform sampler2D uExtra;\nvec4 apply(vec4 color, vec2 uv) { return texture2D(uExtra, uv); }", + ); + fx.setTexture("uExtra", solidCanvas(255, 0, 0)); + + const batcher = renderer.setBatcher("quad"); + batcher.useShader(fx); + fx._prepareTextures(batcher); + + const unit = fx._extraTextures.get("uExtra").unit; + const firstTex = fx._extraTextures.get("uExtra").tex; + expect(unit).toBeTypeOf("number"); + expect(cache.reservedUnits.has(unit)).toBe(true); + + // disable, then swap the image for the same sampler name + fx.enabled = false; + fx.setTexture("uExtra", solidCanvas(0, 255, 0)); + + const replaced = fx._extraTextures.get("uExtra"); + // same reservation held across the replace (not dropped to undefined)… + expect(replaced.unit).toBe(unit); + expect(cache.reservedUnits.has(unit)).toBe(true); + // …old GL texture freed, new one pending a re-upload + expect(gl.isTexture(firstTex)).toBe(false); + expect(replaced.tex).toBe(null); + + // the allocator still refuses to hand that unit out while disabled + for (let u = 0; u < unit; u++) { + cache.allocateTextureUnit(); + } + expect(cache.allocateTextureUnit()).not.toBe(unit); + cache.resetUnitAssignments(); + + // re-enable → the new image uploads into the SAME unit + fx.enabled = true; + fx._prepareTextures(batcher); + expect(fx._extraTextures.get("uExtra").unit).toBe(unit); + expect(gl.isTexture(fx._extraTextures.get("uExtra").tex)).toBe(true); + + fx.destroy(); + expect(cache.reservedUnits.has(unit)).toBe(false); + }); }); /** From 7c764c61af569e383096bcae0c3bc599025fcab1 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Wed, 8 Jul 2026 18:27:17 +0800 Subject: [PATCH 06/20] =?UTF-8?q?feat(renderer):=20toFrameTexture()=20?= =?UTF-8?q?=E2=80=94=20GPU=20frame=20capture=20as=20a=20Texture2d=20(close?= =?UTF-8?q?s=20#1544)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folds the #1544 ticket into this PR (no reason to defer it to 19.10 — the Texture2d contract + setTexture(Texture2d) groundwork this PR already lands is only half the feature; toFrameTexture is the half users actually asked for, to replace a readPixels screen-capture fork hack). Capture the current framebuffer into a Texture2d entirely on the GPU: - WebGL2: blitFramebuffer into an RGBA texture (converts an alpha-less default backbuffer — melonJS uses alpha:transparent, usually false — into an opaque RGBA capture; sidesteps the copyTex format constraints) - WebGL1: copyTexImage2D(RGBA) (ES2 fills the missing alpha with 1) - Canvas: offscreen-canvas self-copy, for toDataURL/toBlob/toImageBitmap family parity Returns a Texture2d (unexported FrameTexture / CanvasFrameTexture). setTexture gains a live-bind branch: a GPU-resident capture is bound by its live handle each draw (never uploaded as a static copy), so re-capturing into the shared slot refreshes what a shader samples with no re-bind. options.target reuses (or, with null, mints) a caller-owned capture; options.region copies a sub-region. Shared slot is dropped + reallocated on context loss (self-heal via gl.isTexture). Tests (tests/toframetexture.spec.js): capture content via FBO readback, shared-slot reuse, target mint/refresh, region, context-loss self-heal, Canvas variant, and the setTexture live-bind sampling path. NOTE: the headless software rasterizer in CI captures + reads back a blit-destination texture fine but cannot SAMPLE one, so capture content is verified by FBO readback and the live-bind sampling path is verified with a normally-uploaded texture; the end-to-end shader sampling of a real capture is exercised by the Aquarium example on a real GPU. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- packages/melonjs/CHANGELOG.md | 1 + .../src/video/canvas/canvas_renderer.js | 64 ++++ .../melonjs/src/video/texture/frametexture.js | 106 +++++++ .../melonjs/src/video/webgl/shadereffect.js | 29 +- .../melonjs/src/video/webgl/webgl_renderer.js | 207 +++++++++++++ packages/melonjs/tests/toframetexture.spec.js | 291 ++++++++++++++++++ 6 files changed, 694 insertions(+), 4 deletions(-) create mode 100644 packages/melonjs/src/video/texture/frametexture.js create mode 100644 packages/melonjs/tests/toframetexture.spec.js diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index 732a04dcd..2ffb02f3a 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -3,6 +3,7 @@ ## [19.9.0] (melonJS 2) - _unreleased_ ### Added +- **`renderer.toFrameTexture()` — GPU frame capture as a `Texture2d`** (closes [#1544](https://github.com/melonjs/melonJS/issues/1544)) — capture the current camera view / framebuffer into a `Texture2d` entirely on the GPU, for screen-space effects (water refraction, heat haze, frosted glass) that need to sample "what's on screen behind me". The fourth member of the `toDataURL` / `toBlob` / `toImageBitmap` family — "the current frame as X" — and the only one whose result never leaves the GPU: a `blitFramebuffer` (WebGL 2) / `copyTexImage2D` (WebGL 1) copy of the bound framebuffer, ~10–100× cheaper than a `readPixels` round-trip and with zero CPU⇄GPU stall. The capture reflects the framebuffer **at the call site** (a `flush()` runs first — Godot `BackBufferCopy` placement), reads whichever framebuffer is bound (backbuffer or a camera post-effect FBO, so it works under `Camera2d` and `Camera3d`), and returns a `Texture2d` you feed straight to a custom effect: `effect.setTexture("uScene", renderer.toFrameTexture())`. `setTexture` now binds such a GPU-resident capture as a **live** handle — re-capturing each frame into the shared slot refreshes what the shader samples with no re-bind. Options: `target` (reuse/mint a caller-owned capture) and `region` (sub-region copy). Color only (RGB, opaque); the Canvas renderer ships an offscreen-canvas equivalent for family parity. See the new **Aquarium** example (fish rendered to the scene, refracted through a rippling water surface that samples the captured frame). - **BMFont XML font support for `BitmapText`** — bitmap fonts can now be loaded from the AngelCode BMFont **XML** flavour in addition to the text `.fnt` format (the serialisation is auto-detected). Many bitmap-font tools and asset packs (itch.io, dafont, …) export PNG + XML; those now load directly through the standard preloader (`{ type: "binary" }`), with no offline conversion step. The XML is parsed with a dependency-free regex parser (no `DOMParser`), so it also works outside the browser (Node / SSR) — unlike the DOM-based TMX XML path. - **Holes & compound paths in `Path2D` / SVG fills** (#1253) — a path may now contain multiple sub-paths (every `M` in an SVG string, or every `moveTo()` call, starts a new one). On `fill()` the first sub-path is the outer contour and each subsequent sub-path is treated as a hole, so donuts, rings, letter counters ("O", "A", "8") and other shapes with holes render correctly under both the WebGL renderer (earcut hole triangulation) and the Canvas renderer (native winding rule). Method semantics follow the standard [`Path2D`](https://developer.mozilla.org/en-US/docs/Web/API/Path2D) API; see the updated **SVG Shapes** example. - **`Texture2d` base class for texture assets** — a new abstract base (`me.Texture2d`) for user-constructed texture objects that own a texture source, exposed via `getTexture()`. `TextureAtlas` now extends it (no behavior change), and the renderables (`Sprite`, `Sprite3d`, `Mesh`) recognize any `Texture2d` by type and resolve it to its backing source — so a texture asset can be passed directly (`{ image: myTexture }`) just like a raw canvas. Raw DOM images/canvases and the loader's decoded `CompressedImage` data remain valid sources outside the class hierarchy. The contract deliberately admits both CPU-backed sources (a drawable canvas/image) and **GPU-resident** ones (an opaque backing that never leaves the GPU) — the shape a future WebGPU backend and screen-capture textures follow. diff --git a/packages/melonjs/src/video/canvas/canvas_renderer.js b/packages/melonjs/src/video/canvas/canvas_renderer.js index 75c24233c..d81ec8236 100644 --- a/packages/melonjs/src/video/canvas/canvas_renderer.js +++ b/packages/melonjs/src/video/canvas/canvas_renderer.js @@ -9,6 +9,7 @@ import { import { Gradient } from "./../gradient.js"; import Renderer from "./../renderer.js"; import TextureCache from "./../texture/cache.js"; +import { CanvasFrameTexture } from "./../texture/frametexture.js"; /** * additional import for TypeScript @@ -18,6 +19,8 @@ import TextureCache from "./../texture/cache.js"; * @import {Line} from "./../../geometries/line.ts"; * @import {Ellipse} from "./../../geometries/ellipse.ts"; * @import {Matrix2d} from "../../math/matrix2d.ts"; + * @import {Bounds} from "../../physics/bounds.ts"; + * @import {default as Texture2d} from "../texture/texture2d.ts"; */ /** @@ -85,6 +88,67 @@ export default class CanvasRenderer extends Renderer { this._lightCache = undefined; } + /** + * Capture the current frame into a {@link Texture2d} — the Canvas-renderer + * counterpart of {@link WebGLRenderer#toFrameTexture}, keeping the + * `toDataURL` / `toBlob` / `toImageBitmap` family renderer-complete. Backed + * by an offscreen `` copy of the drawing buffer (Canvas rendering is + * immediate, so no flush is needed). Custom shaders don't run under the + * Canvas renderer, so the result is a plain drawable for CPU-side reuse + * rather than a live shader input. + * @param {object} [options] + * @param {Texture2d} [options.target] - a capture previously returned here to + * refresh in place, instead of the shared renderer slot + * @param {Rect|Bounds|{x: number, y: number, width: number, height: number}} [options.region] - capture + * only this sub-region (canvas pixels, top-left origin) + * @returns {Texture2d} a texture holding the captured frame + */ + toFrameTexture(options = {}) { + const src = this.getCanvas(); + let x = 0; + let y = 0; + let w = src.width; + let h = src.height; + const region = options.region; + if (typeof region !== "undefined") { + const rx = typeof region.pos !== "undefined" ? region.pos.x : region.x; + const ry = typeof region.pos !== "undefined" ? region.pos.y : region.y; + x = Math.max(0, Math.floor(rx || 0)); + y = Math.max(0, Math.floor(ry || 0)); + w = Math.max(1, Math.min(src.width - x, Math.ceil(region.width))); + h = Math.max(1, Math.min(src.height - y, Math.ceil(region.height))); + } + + // no target → shared slot; target: null → mint owned; target: + // → refresh in place (mirrors WebGLRenderer#toFrameTexture) + const shared = typeof options.target === "undefined"; + let frame = shared + ? this._frameTexture + : options.target === null + ? undefined + : options.target; + if ( + typeof frame === "undefined" || + frame.width !== w || + frame.height !== h + ) { + const canvas = globalThis.document + ? globalThis.document.createElement("canvas") + : new OffscreenCanvas(w, h); + canvas.width = w; + canvas.height = h; + frame = new CanvasFrameTexture(canvas); + if (shared) { + this._frameTexture = frame; + } + } + + const ctx = frame.canvas.getContext("2d"); + ctx.clearRect(0, 0, w, h); + ctx.drawImage(src, x, y, w, h, 0, 0, w, h); + return frame; + } + /** * Reset the canvas transform to identity */ diff --git a/packages/melonjs/src/video/texture/frametexture.js b/packages/melonjs/src/video/texture/frametexture.js new file mode 100644 index 000000000..d851305f3 --- /dev/null +++ b/packages/melonjs/src/video/texture/frametexture.js @@ -0,0 +1,106 @@ +import Texture2d from "./texture2d.ts"; + +/** + * A GPU-resident texture holding a captured frame — the value returned by + * {@link WebGLRenderer#toFrameTexture}. Its backing is a live `WebGLTexture` + * that the renderer refreshes **in place** on each capture, so a shader that + * has bound it once (via {@link ShaderEffect#setTexture}) keeps sampling the + * latest frame with no re-bind. + * + * Not part of the public class surface: obtain instances from + * `renderer.toFrameTexture()` and treat them as opaque {@link Texture2d} + * values. The `isGPUResident` flag is the discriminant `setTexture` uses to + * take its live-bind path (bind the existing handle) instead of uploading a + * static copy. + * @augments Texture2d + * @ignore + */ +export class FrameTexture extends Texture2d { + /** + * @param {WebGLRenderer} renderer - the owning renderer (for the GL context) + * @param {number} width - capture width in pixels + * @param {number} height - capture height in pixels + */ + constructor(renderer, width, height) { + super(); + this._renderer = renderer; + /** @type {number} */ + this.width = width; + /** @type {number} */ + this.height = height; + /** + * the live backing texture, (re)filled by `renderer.toFrameTexture` and + * bound (never re-uploaded) by consumers + * @type {WebGLTexture|null} + */ + this.glTexture = null; + /** + * the blit-destination framebuffer (WebGL2 capture path), or null + * @type {WebGLFramebuffer|null} + */ + this._fbo = null; + /** + * marks this as a live GPU-resident source — see {@link ShaderEffect#setTexture} + * @type {boolean} + */ + this.isGPUResident = true; + } + + /** + * The opaque GPU-resident backing — itself. + * @returns {FrameTexture} + */ + getTexture() { + return this; + } + + /** + * Delete the backing GL texture. Invoked on the renderer-owned shared slot + * at context loss / renderer teardown, or by a caller that owns a `target` + * capture and is done with it. Idempotent. + */ + destroy() { + const gl = this._renderer.gl; + if (this.glTexture !== null) { + gl.deleteTexture(this.glTexture); + this.glTexture = null; + } + if (this._fbo !== null) { + gl.deleteFramebuffer(this._fbo); + this._fbo = null; + } + } +} + +/** + * The Canvas-renderer counterpart of {@link FrameTexture}: a captured frame + * backed by an offscreen `` copy of the drawing buffer, keeping the + * `toFrameTexture` family renderer-complete. A drawable source (not + * GPU-resident), so it flows through the normal image path; custom shaders + * don't run under the Canvas renderer, so this exists for API parity and + * CPU-side reuse (e.g. drawing the captured frame back onto the scene). + * @augments Texture2d + * @ignore + */ +export class CanvasFrameTexture extends Texture2d { + /** + * @param {HTMLCanvasElement|OffscreenCanvas} canvas - the backing copy + */ + constructor(canvas) { + super(); + /** @type {HTMLCanvasElement|OffscreenCanvas} */ + this.canvas = canvas; + /** @type {number} */ + this.width = canvas.width; + /** @type {number} */ + this.height = canvas.height; + } + + /** + * The backing canvas copy. + * @returns {HTMLCanvasElement|OffscreenCanvas} + */ + getTexture() { + return this.canvas; + } +} diff --git a/packages/melonjs/src/video/webgl/shadereffect.js b/packages/melonjs/src/video/webgl/shadereffect.js index b2cae2b14..0f71912d8 100644 --- a/packages/melonjs/src/video/webgl/shadereffect.js +++ b/packages/melonjs/src/video/webgl/shadereffect.js @@ -265,8 +265,14 @@ export default class ShaderEffect { if (typeof this._shader === "undefined" || this.destroyed === true) { return this; } - // accept a Texture2d asset directly — resolve it to its backing source - if (image instanceof Texture2d) { + // A GPU-resident LIVE source (a frame capture from + // renderer.toFrameTexture): keep the wrapper and bind its live GL + // handle every draw — never upload a static copy — so re-capturing + // into the same slot each frame is picked up with no re-bind. Any + // other Texture2d is a static asset: resolve it to its backing + // drawable source once. + const live = image instanceof Texture2d && image.isGPUResident === true; + if (!live && image instanceof Texture2d) { image = image.getTexture(); } // store the entry regardless of `enabled`: the GL work happens lazily @@ -275,14 +281,17 @@ export default class ShaderEffect { // during the window used to vanish permanently — survives intact. const existing = this._extraTextures.get(name); if (existing && existing.tex !== null) { - // drop the old GL texture so the new image re-uploads into the - // SAME reserved unit (kept below) on the next enabled draw + // drop the old STATIC GL texture we own so the new image re-uploads + // into the SAME reserved unit (kept below) on the next enabled draw. + // A live entry never owns a GL texture (tex stays null — the + // renderer/caller owns the handle), so this correctly skips it. this._shader.gl.deleteTexture(existing.tex); } this._extraTextures.set(name, { image, repeat, tex: null, + live, // keep any unit already reserved for this sampler across the // replace: releasing it here (then re-reserving lazily) would open // a window — unbounded while the effect is disabled — in which the @@ -333,6 +342,18 @@ export default class ShaderEffect { cache.reserveUnit(nextUnit); } nextUnit = entry.unit - 1; + if (entry.live === true) { + // live GPU-resident source (frame capture): bind the current + // handle fresh each draw — the renderer refreshes it in place, + // so this samples the latest frame. Never uploaded, never freed + // by this effect. Skip binding if not captured yet (null). + const glTex = entry.image.glTexture; + if (glTex !== null && typeof glTex !== "undefined") { + batcher.bindTexture2D(glTex, entry.unit, false); + this._shader.setUniform(name, entry.unit); + } + continue; + } if (entry.tex === null) { batcher.createTexture2D( entry.unit, diff --git a/packages/melonjs/src/video/webgl/webgl_renderer.js b/packages/melonjs/src/video/webgl/webgl_renderer.js index a052b6764..0a457e63a 100644 --- a/packages/melonjs/src/video/webgl/webgl_renderer.js +++ b/packages/melonjs/src/video/webgl/webgl_renderer.js @@ -17,6 +17,7 @@ import RenderTargetPool from "../rendertarget/render_target_pool.js"; import WebGLRenderTarget from "../rendertarget/webglrendertarget.js"; import { createAtlas, TextureAtlas } from "./../texture/atlas.js"; import TextureCache from "./../texture/cache.js"; +import { FrameTexture } from "./../texture/frametexture.js"; import { dashPath, dashSegments } from "../utils/dash.js"; import { generateJoinCircles, @@ -42,6 +43,7 @@ import { getMaxShaderPrecision } from "./utils/precision.js"; * @import {Matrix2d} from "../../math/matrix2d.ts"; * @import {Matrix3d} from "../../math/matrix3d.ts"; * @import {Batcher} from "./batchers/batcher.js"; + * @import {default as Texture2d} from "../texture/texture2d.ts"; */ // reusable constants for 2D→3D matrix operations @@ -464,6 +466,14 @@ export default class WebGLRenderer extends Renderer { this._lightAtlas = undefined; } + // the shared frame-capture texture (toFrameTexture) holds a GL texture + // tied to this context — drop it so the next capture reallocates. Safe + // on a still-valid context too (a resize/reset just re-creates it). + if (typeof this._frameTexture !== "undefined") { + this._frameTexture.destroy(); + this._frameTexture = undefined; + } + // Context-loss-only cleanup for the TMX GPU renderer: the cached // `GLShader` and per-layer GL textures reference the OLD context // and are invalid. On a regular `GAME_RESET` (context still @@ -802,6 +812,203 @@ export default class WebGLRenderer extends Renderer { return this._lightAtlas; } + /** + * Capture the current frame — everything drawn to the active framebuffer so + * far — into a {@link Texture2d}, entirely on the GPU (a single + * `copyTexSubImage2D`, no `readPixels` round-trip or pipeline stall). The + * fourth member of the {@link Renderer#toDataURL} / {@link Renderer#toBlob} / + * {@link Renderer#toImageBitmap} family — "the current frame as X" — and the + * only one whose result never leaves the GPU. + * + * The capture reflects the framebuffer **at the call site** (a `flush()` + * runs first), so call it right before drawing the surface that needs the + * backdrop — the Godot `BackBufferCopy` placement model. It reads whichever + * framebuffer is currently bound: the backbuffer normally, or a camera's + * post-effect FBO during that pass — so it works under both `Camera2d` and + * `Camera3d`. Under a perspective camera, "drawn so far" is draw order, not + * depth order: capture after the opaque scene, just before the refracting + * surface (same guidance as Unity `_CameraOpaqueTexture` / Godot screen + * texture). + * + * Feed the result straight into a custom post-effect: + * ```js + * effect.setTexture("uScene", renderer.toFrameTexture()); + * ``` + * {@link ShaderEffect#setTexture} binds the **live** handle, so re-capturing + * each frame into the shared slot refreshes what the shader samples without + * any re-bind. + * + * Color only (RGBA); the framebuffer's depth is not captured. By default the + * result is a shared, renderer-owned texture valid until the next + * parameterless call — pass `options.target` for an independent, caller-owned + * capture (e.g. two captures alive in the same frame). + * @param {object} [options] + * @param {Texture2d|null} [options.target] - controls the destination: omit + * for the shared renderer slot (default); pass a capture previously + * returned by this method to refresh it in place; pass `null` to mint a + * fresh, caller-owned capture (for two independent captures in one frame — + * `destroy()` it yourself when done) + * @param {Rect|Bounds|{x: number, y: number, width: number, height: number}} [options.region] - capture + * only this sub-region (framebuffer pixels, bottom-left origin); a smaller + * region is a proportionally cheaper copy. Defaults to the whole framebuffer. + * @returns {Texture2d} a GPU-resident texture holding the captured frame + * @example + * // a water surface that refracts the scene rendered behind it + * draw(renderer) { + * const scene = renderer.toFrameTexture(); // grab the backdrop + * this.shader.setTexture("uScene", scene); // live-bound + * super.draw(renderer); // draw the rippling water + * } + */ + toFrameTexture(options = {}) { + const gl = this.gl; + const canvas = this.getCanvas(); + + // resolve the capture rect in framebuffer pixels (default: everything). + // accepts a Rect (pos + width/height), a Bounds, or a plain {x,y,w,h}. + let x = 0; + let y = 0; + let w = canvas.width; + let h = canvas.height; + const region = options.region; + if (typeof region !== "undefined") { + const rx = typeof region.pos !== "undefined" ? region.pos.x : region.x; + const ry = typeof region.pos !== "undefined" ? region.pos.y : region.y; + x = Math.max(0, Math.floor(rx || 0)); + y = Math.max(0, Math.floor(ry || 0)); + w = Math.max(1, Math.min(canvas.width - x, Math.ceil(region.width))); + h = Math.max(1, Math.min(canvas.height - y, Math.ceil(region.height))); + } + + // flush pending geometry so the capture holds everything drawn so far + this.flush(); + + // destination: + // - no `target` → the shared, renderer-owned slot (default; + // overwritten by the next parameterless call) + // - `target: null` → mint a fresh, caller-owned capture (for two + // independent captures alive in one frame) + // - `target: ` → refresh that caller-owned capture in place + const shared = typeof options.target === "undefined"; + let frame = shared + ? this._frameTexture + : options.target === null + ? undefined + : options.target; + + // (re)allocate when missing, resized, or the GL handle went stale (a + // context-loss/restore cycle deletes it — gl.isTexture catches that) + if ( + typeof frame === "undefined" || + frame.width !== w || + frame.height !== h || + frame.glTexture === null || + gl.isTexture(frame.glTexture) === false + ) { + if (typeof frame !== "undefined") { + frame.destroy(); + frame.width = w; + frame.height = h; + } else { + frame = new FrameTexture(this, w, h); + } + if (shared) { + this._frameTexture = frame; + } + } + + // The destination is an RGBA texture (RGBA is what shaders sample + // cleanly). The tricky part is copying from an ALPHA-LESS framebuffer — + // melonJS creates the default context with `alpha: transparent` + // (usually false), so the backbuffer has no alpha, and both + // copyTexImage2D and copyTexSubImage2D into an RGBA texture are then + // INVALID_OPERATION under WebGL2. + const batcher = this.setBatcher("quad"); + const unit = batcher.maxBatchTextures - 1; + if (this.WebGLVersion > 1) { + // WebGL2: blitFramebuffer converts formats, filling a missing alpha + // with 1 — so an alpha-less source lands as an opaque RGBA texture. + // It also reads/writes framebuffers directly (no texture-unit games). + const srcFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); + if (frame.glTexture === null) { + const tex = gl.createTexture(); + // bind through the batcher (keeps boundTextures consistent) only + // to set the sampler params; the copy itself is an FBO blit + batcher.bindTexture2D(tex, unit, false); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + w, + h, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + null, + ); + frame.glTexture = tex; + } + // one reusable scratch FBO for the blit destination. The target + // texture is attached only for the blit and DETACHED immediately + // after — a texture left attached to a framebuffer is refused for + // sampling by strict drivers (feedback-loop protection). + if (typeof this._captureFBO === "undefined") { + this._captureFBO = gl.createFramebuffer(); + } + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, srcFB); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this._captureFBO); + gl.framebufferTexture2D( + gl.DRAW_FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + frame.glTexture, + 0, + ); + gl.blitFramebuffer( + x, + y, + x + w, + y + h, + 0, + 0, + w, + h, + gl.COLOR_BUFFER_BIT, + gl.NEAREST, + ); + // detach the texture and restore the source framebuffer + gl.framebufferTexture2D( + gl.DRAW_FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + null, + 0, + ); + gl.bindFramebuffer(gl.FRAMEBUFFER, srcFB); + } else { + // WebGL1: copyTexImage2D(RGBA) fills a missing alpha with 1 (ES2 + // semantics), so it works from an alpha-less framebuffer. Realloc + // per capture — the legacy path; live-bind reads the handle fresh. + if (frame.glTexture !== null) { + gl.deleteTexture(frame.glTexture); + } + const tex = gl.createTexture(); + batcher.bindTexture2D(tex, unit, false); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, x, y, w, h, 0); + frame.glTexture = tex; + } + + return frame; + } + /** * Begin capturing rendering to an offscreen FBO for post-effect processing. * @param {Renderable} renderable - the renderable requesting post-effect processing diff --git a/packages/melonjs/tests/toframetexture.spec.js b/packages/melonjs/tests/toframetexture.spec.js new file mode 100644 index 000000000..c3017cb7b --- /dev/null +++ b/packages/melonjs/tests/toframetexture.spec.js @@ -0,0 +1,291 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { + boot, + CanvasRenderer, + ShaderEffect, + Texture2d, + video, + WebGLRenderer, +} from "../src/index.js"; + +/** + * `renderer.toFrameTexture()` captures the current framebuffer into a + * {@link Texture2d} entirely on the GPU (WebGL2 blitFramebuffer / WebGL1 + * copyTexImage2D — no readPixels round-trip), for screen-space effects (water + * refraction, heat haze, glass). The fourth member of the toDataURL / toBlob / + * toImageBitmap family; the only one that never leaves the GPU. Ticket #1544. + * + * Capture CONTENT is verified by attaching the returned texture to a scratch + * framebuffer and reading it back. The live-bind SAMPLING path (a shader + * reading the capture) is verified separately with a normally-uploaded texture: + * the headless software rasterizer used in CI cannot *sample* a blit-destination + * texture (it captures + reads back fine, and real GPUs sample it fine), so the + * two concerns are checked independently. + */ +const SIZE = 32; + +describe("WebGLRenderer.toFrameTexture", () => { + let renderer; + let gl; + let isWebGL; + + beforeAll(() => { + boot(); + video.init(SIZE, SIZE, { + parent: "screen", + renderer: video.WEBGL, + failIfMajorPerformanceCaveat: false, + antiAlias: false, + }); + renderer = video.renderer; + isWebGL = renderer instanceof WebGLRenderer; + if (isWebGL) { + gl = renderer.gl; + } + }); + + // paint the whole scene a known color THROUGH the engine (drawImage of a + // solid canvas — the proven path the shadereffect specs use), then flush + const paintScene = (hex) => { + const c = document.createElement("canvas"); + c.width = SIZE; + c.height = SIZE; + const cx = c.getContext("2d"); + cx.fillStyle = hex; + cx.fillRect(0, 0, SIZE, SIZE); + renderer.save(); + renderer.drawImage(c, 0, 0, SIZE, SIZE, 0, 0, SIZE, SIZE); + renderer.flush(); + renderer.restore(); + }; + + // read a capture's content back directly: attach its GL texture to a scratch + // FBO (the RGBA capture texture is colour-renderable) and readPixels + const readCapture = (frame, px = SIZE / 2, py = SIZE / 2) => { + const fb = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + frame.glTexture, + 0, + ); + const out = new Uint8Array(4); + if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) { + gl.readPixels(px, py, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, out); + } + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.deleteFramebuffer(fb); + return out; + }; + + it("returns a GPU-resident Texture2d sized to the framebuffer", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + paintScene("#ff0000"); + const frame = renderer.toFrameTexture(); + expect(frame).toBeInstanceOf(Texture2d); + expect(frame.isGPUResident).toBe(true); + expect(frame.width).toBe(SIZE); + expect(frame.height).toBe(SIZE); + expect(gl.isTexture(frame.glTexture)).toBe(true); + // getTexture() returns the opaque backing (itself) + expect(frame.getTexture()).toBe(frame); + }); + + it("captures the current framebuffer contents (GPU-side, opaque alpha)", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + paintScene("#ff0000"); // red + const frame = renderer.toFrameTexture(); + const px = readCapture(frame); + expect(px[0]).toBeGreaterThan(200); + expect(px[1]).toBeLessThan(60); + expect(px[2]).toBeLessThan(60); + expect(px[3]).toBe(255); // alpha-less source captured as opaque + }); + + it("reuses the shared slot (same object + handle), refreshed in place", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + paintScene("#00ff00"); + const a = renderer.toFrameTexture(); + const handleA = a.glTexture; + paintScene("#0000ff"); + const b = renderer.toFrameTexture(); + expect(b).toBe(a); // same shared texture object + expect(b.glTexture).toBe(handleA); // same GL handle, refreshed in place + // contents now reflect the SECOND capture (blue) + const px = readCapture(b); + expect(px[2]).toBeGreaterThan(200); + expect(px[0]).toBeLessThan(60); + }); + + it("mints (target:null) then refreshes an independent caller-owned capture", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + paintScene("#ff0000"); + const shared = renderer.toFrameTexture(); + // target: null → a fresh owned capture, distinct from the shared slot + paintScene("#00ff00"); + const owned = renderer.toFrameTexture({ target: null }); + expect(owned).not.toBe(shared); + expect(gl.isTexture(owned.glTexture)).toBe(true); + + // refreshing the SHARED slot must not disturb the owned one + paintScene("#0000ff"); + renderer.toFrameTexture(); + let px = readCapture(owned); + expect(px[1]).toBeGreaterThan(200); // still green + expect(px[2]).toBeLessThan(60); + + // refreshing the owned capture in place returns the SAME object + paintScene("#0000ff"); + const again = renderer.toFrameTexture({ target: owned }); + expect(again).toBe(owned); + px = readCapture(owned); + expect(px[2]).toBeGreaterThan(200); // now blue + + owned.destroy(); + }); + + it("captures only the requested sub-region", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + paintScene("#ff0000"); + // capture a 16x16 region around the centre (known painted) + const frame = renderer.toFrameTexture({ + region: { x: 8, y: 8, width: 16, height: 16 }, + }); + expect(frame.width).toBe(16); + expect(frame.height).toBe(16); + const px = readCapture(frame, 8, 8); + expect(px[0]).toBeGreaterThan(200); + }); + + it("reallocates when the backing handle goes stale (context-loss self-heal)", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + paintScene("#ff0000"); + const first = renderer.toFrameTexture(); + // simulate the post-context-loss state: the GL handle is dead + gl.deleteTexture(first.glTexture); + expect(gl.isTexture(first.glTexture)).toBe(false); + + paintScene("#00ff00"); + const healed = renderer.toFrameTexture(); + expect(healed).toBe(first); // same shared slot object… + expect(gl.isTexture(healed.glTexture)).toBe(true); // …with a fresh handle + const px = readCapture(healed); + expect(px[1]).toBeGreaterThan(200); // green + }); + + // the live-bind SAMPLING path: setTexture stores a live GPU-resident entry, + // and _prepareTextures binds its current GL handle each draw so a shader + // samples it. Verified with a normally-uploaded texture standing in as the + // capture's backing (the headless rasterizer can't sample blit textures). + it("setTexture live-binds a frame texture for a shader to sample", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + // upload a red texture the ordinary way, then reuse its GL handle + const redC = document.createElement("canvas"); + redC.width = SIZE; + redC.height = SIZE; + const rcx = redC.getContext("2d"); + rcx.fillStyle = "#ff0000"; + rcx.fillRect(0, 0, SIZE, SIZE); + const uploader = new ShaderEffect( + renderer, + "uniform sampler2D uX;\nvec4 apply(vec4 c, vec2 uv) { return texture2D(uX, uv); }", + ); + uploader.setTexture("uX", redC); + const blank = document.createElement("canvas"); + blank.width = SIZE; + blank.height = SIZE; + const drawEffect = (effect) => { + paintScene("#101010"); // sentinel so a failed sample is visible + renderer.save(); + renderer.customShader = effect; + renderer.drawImage(blank, 0, 0, SIZE, SIZE, 0, 0, SIZE, SIZE); + renderer.flush(); + renderer.customShader = undefined; + renderer.restore(); + const px = new Uint8Array(4); + gl.readPixels(SIZE / 2, SIZE / 2, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px); + return px; + }; + drawEffect(uploader); // triggers the createTexture2D upload + const redHandle = uploader._extraTextures.get("uX").tex; + + // stand a real FrameTexture up on that known-sampleable handle + const frame = renderer.toFrameTexture(); + frame.glTexture = redHandle; + + const effect = new ShaderEffect( + renderer, + "uniform sampler2D uScene;\nvec4 apply(vec4 c, vec2 uv) { return texture2D(uScene, uv); }", + ); + effect.setTexture("uScene", frame); + // the setTexture entry is a LIVE GPU-resident binding, not a static copy + const entry = effect._extraTextures.get("uScene"); + expect(entry.live).toBe(true); + expect(entry.tex).toBe(null); // never uploads its own copy + + const px = drawEffect(effect); + expect(px[0]).toBeGreaterThan(200); // red — sampled the live handle + expect(px[2]).toBeLessThan(60); + + effect.destroy(); + uploader.destroy(); + }); +}); + +describe("CanvasRenderer.toFrameTexture", () => { + let renderer; + + beforeAll(() => { + boot(); + video.init(SIZE, SIZE, { parent: "screen", renderer: video.CANVAS }); + renderer = video.renderer; + expect(renderer).toBeInstanceOf(CanvasRenderer); + }); + + it("returns a Texture2d backed by a canvas copy of the frame", () => { + const ctx2d = renderer.getContext(); + ctx2d.setTransform(1, 0, 0, 1, 0, 0); + ctx2d.fillStyle = "#ff0000"; + ctx2d.fillRect(0, 0, SIZE, SIZE); + + const frame = renderer.toFrameTexture(); + expect(frame).toBeInstanceOf(Texture2d); + expect(frame.width).toBe(SIZE); + expect(frame.height).toBe(SIZE); + const backing = frame.getTexture(); + // a drawable canvas copy (not GPU-resident) + const bctx = backing.getContext("2d"); + const px = bctx.getImageData(SIZE / 2, SIZE / 2, 1, 1).data; + expect(px[0]).toBeGreaterThan(200); + expect(px[1]).toBeLessThan(60); + }); + + it("reuses the shared canvas slot across calls", () => { + const a = renderer.toFrameTexture(); + const b = renderer.toFrameTexture(); + expect(b).toBe(a); + }); +}); From cc00415fb34ea84f4ca8de9db5e3be52e5f880b8 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Wed, 8 Jul 2026 18:38:53 +0800 Subject: [PATCH 07/20] =?UTF-8?q?feat(examples):=20Aquarium=20=E2=80=94=20?= =?UTF-8?q?toFrameTexture=20screen-space=20refraction;=20fix=20capture=20t?= =?UTF-8?q?o=20copyTexImage2D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the Water Refraction (swimming-pool) example with an Aquarium that exercises the real toFrameTexture use case: fish swim across a planted tank, then a full-screen water surface (drawn last) captures the live frame in its draw() via renderer.toFrameTexture(), binds it to a custom ShaderEffect as uScene, and refracts it through a scrolling NoiseTexture2d flow map — the fish shimmer as if seen through moving water. The screen-texture pattern (Godot hint_screen_texture / Unity _CameraOpaqueTexture). Building the example on a real GPU (ANGLE Metal) surfaced that the earlier blitFramebuffer capture path produced a texture that some drivers refuse to SAMPLE in the same frame (the capture + FBO-readback were correct, but a shader sampling it read black — on Metal too, not just the CI software rasterizer). Switched to copyTexImage2D — the standard framebuffer→texture path (Three.js copyFramebufferToTexture) — with an RGB internalformat: a valid subset of both an alpha-less default backbuffer (melonJS uses alpha:transparent, usually false) and an RGBA camera FBO, so the copy never trips INVALID_OPERATION on a format mismatch, samples as an opaque backdrop (alpha=1), and samples reliably on every driver tested. Removes the blit scratch-FBO + WebGL1 split + FrameTexture._fbo. Verified end-to-end on ANGLE Metal (fish visibly refracted + animating); all 9 toFrameTexture specs green in CI. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- .../src/examples/aquarium/ExampleAquarium.tsx | 411 ++++++++++++++++++ .../ExampleWaterRefraction.tsx | 269 ------------ packages/examples/src/main.tsx | 16 +- packages/melonjs/CHANGELOG.md | 2 +- .../melonjs/src/video/texture/frametexture.js | 12 +- .../melonjs/src/video/webgl/webgl_renderer.js | 99 +---- 6 files changed, 439 insertions(+), 370 deletions(-) create mode 100644 packages/examples/src/examples/aquarium/ExampleAquarium.tsx delete mode 100644 packages/examples/src/examples/waterRefraction/ExampleWaterRefraction.tsx diff --git a/packages/examples/src/examples/aquarium/ExampleAquarium.tsx b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx new file mode 100644 index 000000000..6d0e775c0 --- /dev/null +++ b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx @@ -0,0 +1,411 @@ +/** + * melonJS — aquarium example (renderer.toFrameTexture()). + * Copyright (C) 2011 - 2026 AltByte Pte Ltd — MIT License. + * See `packages/examples/LICENSE.md` for full license + asset credits. + * + * Fish swim across a planted tank; a full-screen water surface then refracts + * the LIVE scene rendered behind it. The surface renderable, drawn last, calls + * `renderer.toFrameTexture()` in its `draw()` to grab everything painted so far + * (background + fish + bubbles) as a GPU-resident `Texture2d`, hands it to a + * custom `ShaderEffect` as an extra sampler (`uScene`), and distorts it with a + * scrolling `NoiseTexture2d` flow map — so the fish shimmer as if seen through + * moving water, per-frame, with no `readPixels` round-trip. + * + * This is the industry-standard "screen texture" pattern (Godot + * `hint_screen_texture` / Unity `_CameraOpaqueTexture` / Three.js + * `copyFramebufferToTexture`): capture the opaque scene, sample it from the + * refracting surface. `toFrameTexture()` returns the public `Texture2d`, so it + * plugs straight into `setTexture()` and is re-captured every frame into the + * same shared slot — the shader samples the latest frame with no re-bind. + */ +import { + type Application, + loader, + NoiseTexture2d, + type ShaderEffect, + Sprite, + Stage, + state, + video, +} from "melonjs"; +import { createExampleComponent } from "../utils"; + +// The refraction fragment (GLSL ES 1.00). It ignores the surface quad's own +// texture and re-samples the CAPTURED scene (uScene) in screen space, offset by +// a scrolling noise flow field — plus a caustic shimmer and a faint underwater +// tint. Sampling in screen space (gl_FragCoord / uResolution) matches the +// capture's coordinate system regardless of the quad's own UVs. +const WATER_FRAGMENT = ` +uniform sampler2D uScene; // the captured frame, bound each draw via setTexture +uniform sampler2D uNoise; // static seamless flow map +uniform float uTime; // seconds (setTime) +uniform vec2 uResolution; // framebuffer size in pixels +uniform float uStrength; // ripple strength (slider) + +vec4 apply(vec4 color, vec2 uv) { + vec2 s = gl_FragCoord.xy / uResolution; + + // two noise layers scrolling in different directions → a living flow field + vec2 f1 = texture2D(uNoise, s * 1.6 + vec2(uTime * 0.03, uTime * 0.05)).rg; + vec2 f2 = texture2D(uNoise, s * 2.7 - vec2(uTime * 0.04, uTime * 0.02)).rg; + vec2 flow = (f1 + f2 - 1.0); + + // refract the captured scene: sample it at the displaced screen coord + vec2 rUV = clamp(s + flow * uStrength, 0.0, 1.0); + vec3 scene = texture2D(uScene, rUV).rgb; + + // caustic sparkle where the flow layers pinch together, brighter up top + float caustic = pow(max(f1.r * f2.g, 0.0), 3.0) * (1.0 - s.y) * 1.5; + scene += vec3(0.10, 0.22, 0.26) * caustic; + + // gentle underwater colour grade + scene = mix(scene, scene * vec3(0.82, 0.96, 1.06), 0.35); + return vec4(scene, 1.0); +} +`; + +type Rgb = [number, number, number]; + +// paint a small side-on fish: an elliptical body, a tail fin and an eye +const fishCanvas = (w: number, h: number, body: Rgb, fin: Rgb) => { + const canvas = document.createElement("canvas"); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext("2d") as CanvasRenderingContext2D; + const [br, bg, bb] = body; + const [fr, fg, fb] = fin; + + // tail fin (triangle at the back / left) + ctx.fillStyle = `rgb(${fr}, ${fg}, ${fb})`; + ctx.beginPath(); + ctx.moveTo(2, h / 2); + ctx.lineTo(w * 0.34, h * 0.2); + ctx.lineTo(w * 0.34, h * 0.8); + ctx.closePath(); + ctx.fill(); + + // body (ellipse), facing right + ctx.fillStyle = `rgb(${br}, ${bg}, ${bb})`; + ctx.beginPath(); + ctx.ellipse(w * 0.6, h / 2, w * 0.34, h * 0.32, 0, 0, Math.PI * 2); + ctx.fill(); + + // top fin + ctx.fillStyle = `rgba(${fr}, ${fg}, ${fb}, 0.9)`; + ctx.beginPath(); + ctx.moveTo(w * 0.5, h * 0.2); + ctx.lineTo(w * 0.72, h * 0.06); + ctx.lineTo(w * 0.72, h * 0.42); + ctx.closePath(); + ctx.fill(); + + // belly highlight + ctx.fillStyle = "rgba(255, 255, 255, 0.18)"; + ctx.beginPath(); + ctx.ellipse(w * 0.62, h * 0.62, w * 0.24, h * 0.12, 0, 0, Math.PI * 2); + ctx.fill(); + + // eye + ctx.fillStyle = "#fff"; + ctx.beginPath(); + ctx.arc(w * 0.82, h * 0.42, Math.max(2, w * 0.05), 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = "#111"; + ctx.beginPath(); + ctx.arc(w * 0.84, h * 0.42, Math.max(1, w * 0.025), 0, Math.PI * 2); + ctx.fill(); + return canvas; +}; + +// the tank backdrop: a vertical water gradient, a sandy bottom, and a few +// simple plant fronds — a static, opaque scene for the surface to refract +const tankCanvas = (w: number, h: number) => { + const canvas = document.createElement("canvas"); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext("2d") as CanvasRenderingContext2D; + + const grad = ctx.createLinearGradient(0, 0, 0, h); + grad.addColorStop(0, "#0a6b86"); + grad.addColorStop(0.6, "#064b63"); + grad.addColorStop(1, "#04303f"); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, w, h); + + // soft light shafts from the top + ctx.globalAlpha = 0.08; + ctx.fillStyle = "#bfefff"; + for (let i = 0; i < 5; i++) { + const x = (i + 0.5) * (w / 5); + ctx.beginPath(); + ctx.moveTo(x - 10, 0); + ctx.lineTo(x + 10, 0); + ctx.lineTo(x + 60, h); + ctx.lineTo(x + 30, h); + ctx.closePath(); + ctx.fill(); + } + ctx.globalAlpha = 1; + + // plants (wavy fronds) rooted in the sand + const plant = (px: number, ph: number, hue: string) => { + ctx.strokeStyle = hue; + ctx.lineWidth = 5; + ctx.lineCap = "round"; + for (let b = -1; b <= 1; b++) { + ctx.beginPath(); + ctx.moveTo(px, h - 24); + ctx.quadraticCurveTo( + px + b * 22 + 14, + h - 24 - ph * 0.5, + px + b * 10, + h - 24 - ph, + ); + ctx.stroke(); + } + }; + plant(w * 0.12, h * 0.42, "#0c8a4a"); + plant(w * 0.2, h * 0.3, "#0aa15a"); + plant(w * 0.8, h * 0.5, "#0c8a4a"); + plant(w * 0.9, h * 0.34, "#0aa15a"); + + // sandy bottom + const sand = ctx.createLinearGradient(0, h - 28, 0, h); + sand.addColorStop(0, "#c9a86a"); + sand.addColorStop(1, "#a8874c"); + ctx.fillStyle = sand; + ctx.beginPath(); + ctx.moveTo(0, h - 20); + for (let x = 0; x <= w; x += 24) { + ctx.lineTo(x, h - 20 - Math.sin(x * 0.05) * 4); + } + ctx.lineTo(w, h); + ctx.lineTo(0, h); + ctx.closePath(); + ctx.fill(); + return canvas; +}; + +// a fish that swims horizontally and turns around at the tank edges +class Fish extends Sprite { + private speed: number; + private minX: number; + private maxX: number; + private bobPhase: number; + private bobAmp: number; + private baseY: number; + + constructor( + x: number, + y: number, + image: HTMLCanvasElement, + speed: number, + bounds: { min: number; max: number }, + ) { + super(x, y, { image, framewidth: image.width, frameheight: image.height }); + this.speed = speed; + this.minX = bounds.min; + this.maxX = bounds.max; + this.baseY = y; + this.bobPhase = x * 0.05; + this.bobAmp = 6 + Math.abs(speed) * 0.2; + this.flipX(speed < 0); + } + + update(dt: number) { + super.update(dt); + const s = dt / 1000; + this.pos.x += this.speed * s; + this.bobPhase += s * 2; + this.pos.y = this.baseY + Math.sin(this.bobPhase) * this.bobAmp; + if (this.pos.x < this.minX) { + this.pos.x = this.minX; + this.speed = Math.abs(this.speed); + this.flipX(false); + } else if (this.pos.x > this.maxX) { + this.pos.x = this.maxX; + this.speed = -Math.abs(this.speed); + this.flipX(true); + } + return true; + } +} + +// the water surface: drawn LAST, it captures the frame rendered so far and +// re-draws it refracted. A full-viewport renderable whose draw() grabs the +// backdrop via toFrameTexture(), binds it to the shader (uScene), and blits a +// full-screen quad through that shader. +class WaterSurface extends Sprite { + private effect: ShaderEffect; + + constructor( + x: number, + y: number, + w: number, + h: number, + effect: ShaderEffect, + ) { + // a 1x1 white image stretched to the viewport — the shader ignores it + // (it samples the captured scene instead), it just supplies the quad + const white = document.createElement("canvas"); + white.width = 1; + white.height = 1; + const ctx = white.getContext("2d") as CanvasRenderingContext2D; + ctx.fillStyle = "#fff"; + ctx.fillRect(0, 0, 1, 1); + super(x, y, { + image: white, + framewidth: 1, + frameheight: 1, + anchorPoint: { x: 0.5, y: 0.5 }, + }); + // stretch the 1x1 quad to fill the whole viewport + this.scale(w, h); + this.effect = effect; + this.shader = effect; + } + + draw(renderer: Parameters[0], viewport?: object) { + // capture everything drawn so far this frame — the opaque aquarium — + // as a GPU-resident texture, and hand it to the refraction shader. + // Re-captured every frame into the shared slot: the live-bound sampler + // picks up the latest frame with no re-bind. + const scene = renderer.toFrameTexture(); + this.effect.setTexture("uScene", scene); + // biome-ignore lint/suspicious/noExplicitAny: viewport shape varies by call site + super.draw(renderer, viewport as any); + } +} + +class PlayScreen extends Stage { + private elapsed = 0; + private effect!: ShaderEffect; + private panel?: HTMLDivElement; + + onResetEvent(app: Application) { + const w = app.viewport.width; + const h = app.viewport.height; + + // static seamless noise flow map (baked once; scrolled on the GPU) + const noise = new NoiseTexture2d({ + width: 256, + height: 256, + type: "simplex", + seed: 11, + frequency: 0.035, + octaves: 4, + gain: 0.5, + domainWarp: true, + domainWarpAmp: 8, + seamless: true, + }); + + // the refraction effect, preloaded as a "shader" asset + this.effect = loader.getShader("aquariumWater") as ShaderEffect; + this.effect.setTexture("uNoise", noise.getTexture(), "repeat"); + this.effect.setUniform("uResolution", new Float32Array([w, h])); + this.effect.setUniform("uStrength", 0.02); + + // backdrop (z 0) + const tank = new Sprite(w / 2, h / 2, { + image: tankCanvas(w, h), + framewidth: w, + frameheight: h, + anchorPoint: { x: 0.5, y: 0.5 }, + }); + app.world.addChild(tank, 0); + + // a school of fish (z 1..) swimming at various depths and speeds + const palette: Array<{ body: Rgb; fin: Rgb; size: number }> = [ + { body: [244, 148, 42], fin: [214, 92, 20], size: 54 }, // clownfish + { body: [86, 170, 220], fin: [40, 120, 190], size: 44 }, // blue tang + { body: [230, 210, 90], fin: [200, 160, 40], size: 40 }, // yellow + { body: [220, 90, 120], fin: [170, 50, 90], size: 48 }, // pink + { body: [150, 210, 150], fin: [90, 160, 100], size: 36 }, // green + ]; + for (let i = 0; i < 7; i++) { + const p = palette[i % palette.length]; + const img = fishCanvas(p.size, p.size * 0.66, p.body, p.fin); + const dir = i % 2 === 0 ? 1 : -1; + const speed = dir * (36 + (i % 3) * 22); + const y = 70 + ((i * 47) % (h - 150)); + const x = 60 + ((i * 97) % (w - 120)); + app.world.addChild( + new Fish(x, y, img, speed, { min: 40, max: w - 40 }), + 1 + i, + ); + } + + // the water surface post-pass (z 100, above everything it refracts) + app.world.addChild(new WaterSurface(w / 2, h / 2, w, h, this.effect), 100); + + this.buildSlider(app); + } + + private buildSlider(app: Application) { + const panel = document.createElement("div"); + panel.style.cssText = + "position:absolute;top:60px;left:16px;z-index:1000;font-family:sans-serif;" + + "color:#e8f6fa;background:rgba(0,0,0,0.45);padding:8px 12px;border-radius:6px;"; + const label = document.createElement("div"); + label.textContent = "🐟 Water ripple"; + label.style.cssText = "font-size:12px;margin-bottom:6px;"; + const slider = document.createElement("input"); + slider.type = "range"; + slider.min = "0"; + slider.max = "0.06"; + slider.step = "0.002"; + slider.value = "0.02"; + slider.style.cssText = "width:190px;display:block;"; + slider.addEventListener("input", () => { + this.effect.setUniform("uStrength", Number.parseFloat(slider.value)); + }); + const hint = document.createElement("div"); + hint.textContent = "the fish are refracted through the captured frame"; + hint.style.cssText = "font-size:10px;margin-top:6px;opacity:0.7;"; + panel.appendChild(label); + panel.appendChild(slider); + panel.appendChild(hint); + const parent = app.renderer.getCanvas().parentElement; + if (parent) { + parent.style.position = "relative"; + parent.appendChild(panel); + } + this.panel = panel; + } + + update(dt: number) { + this.elapsed += dt / 1000; + this.effect.setTime(this.elapsed); + super.update(dt); + return true; // keep animating every frame + } + + onDestroyEvent() { + loader.unload({ name: "aquariumWater", type: "shader" }); + this.panel?.remove(); + } +} + +const createGame = () => { + video.init(728, 410, { + parent: "screen", + scaleMethod: "flex", + // toFrameTexture + ShaderEffect are WebGL features + renderer: video.WEBGL, + antiAlias: true, + }); + + state.set(state.PLAY, new PlayScreen()); + + // preload the refraction shader (compiled once at load time) + loader.preload( + [{ name: "aquariumWater", type: "shader", data: WATER_FRAGMENT }], + () => { + state.change(state.PLAY); + }, + false, + ); +}; + +export const ExampleAquarium = createExampleComponent(createGame); diff --git a/packages/examples/src/examples/waterRefraction/ExampleWaterRefraction.tsx b/packages/examples/src/examples/waterRefraction/ExampleWaterRefraction.tsx deleted file mode 100644 index 46206eb5c..000000000 --- a/packages/examples/src/examples/waterRefraction/ExampleWaterRefraction.tsx +++ /dev/null @@ -1,269 +0,0 @@ -/** - * melonJS — water refraction example (ShaderEffect.setTexture). - * Copyright (C) 2011 - 2026 AltByte Pte Ltd — MIT License. - * See `packages/examples/LICENSE.md` for full license + asset credits. - * - * A swimming pool seen at a perspective angle, its tiled floor rippling under - * the water — built with a single custom `ShaderEffect` that reads TWO textures: - * - `uSampler` — the sprite's own albedo (the seamless pool tiles), bound by the - * engine as usual. - * - `uNoise` — a *static* seamless `NoiseTexture2d`, bound as an **extra** - * sampler via `effect.setTexture("uNoise", noise.getTexture())`. - * - * The fragment projects each screen pixel onto a tilted floor plane (a "mode-7" - * perspective floor receding to a horizon), then distorts the floor UVs with two - * noise layers scrolled on the GPU (driven by `effect.setTime`). Because the - * distortion is constant in *floor* space, the ripples are automatically - * perspective-correct — fine and tight near the horizon, broad up close. A - * pointer swell, depth fog and animated caustics ride on top. No per-frame CPU - * noise re-bake, no `Light2d`; `setTexture` is what makes the second sampler a - * one-liner (see issue #1532). - * - * The fragment ships as a preloaded `"shader"` asset: compiled once at load - * time and retrieved as a shared, loader-owned `ShaderEffect` via - * `loader.getShader()` — freed with `loader.unload()` on stage teardown. - */ -import { - type Application, - input, - loader, - NoiseTexture2d, - type ShaderEffect, - Sprite, - Stage, - state, - video, -} from "melonjs"; -import { createExampleComponent } from "../utils"; - -// A GLSL ES 1.00 fragment (what ShaderEffect wraps). `apply(color, uv)` receives -// the pre-sampled/tinted sprite color and its texture coords; we ignore the flat -// color and re-sample `uSampler` at perspective-projected, refracted coords. -const REFRACTION_FRAGMENT = ` -uniform sampler2D uNoise; // extra texture, bound via setTexture -uniform float uTime; // seconds, fed via setTime -uniform float uAmount; // ripple strength (slider) -uniform float uCaustic; // caustic sparkle intensity -uniform vec2 uMouse; // pointer position in screen uv -uniform float uMouseStr; // decaying pointer impulse - -const float HORIZON = 0.30; // screen height of the far water edge -const float DEPTH = 0.22; // how fast the floor recedes past the horizon -const float TILES = 1.6; // pool-tile repeats per unit of floor depth - -vec4 apply(vec4 color, vec2 uv) { - // a radial swell that follows the pointer and fades over ~1s (screen space) - vec2 toMouse = uv - uMouse; - float md = length(toMouse); - float swell = uMouseStr * exp(-md * 11.0) * sin(md * 42.0 - uTime * 7.0); - vec2 suv = uv + (md > 0.001 ? toMouse / md : vec2(0.0)) * swell * 0.02; - - // above the waterline: distant water fading up to a faint horizon glow - if (suv.y <= HORIZON) { - float t = suv.y / HORIZON; // 0 at top .. 1 at the horizon - vec3 sky = mix(vec3(0.02, 0.10, 0.16), vec3(0.06, 0.30, 0.38), t); - sky += vec3(0.10, 0.16, 0.18) * pow(t, 6.0); - return vec4(sky, 1.0); - } - - // project the pixel onto the tilted floor plane (perspective / mode-7) - float depth = DEPTH / (suv.y - HORIZON); // grows toward the horizon - vec2 floorUV = vec2((suv.x - 0.5) * depth * 2.0, depth); - - // two noise layers scrolling in different directions refract the floor. - // a CONSTANT offset in floor space is what makes the ripples shrink with - // distance — no extra perspective math needed. - vec2 n1 = texture2D(uNoise, floorUV * 0.16 + vec2(uTime * 0.03, uTime * 0.06)).rg; - vec2 n2 = texture2D(uNoise, floorUV * 0.26 - vec2(uTime * 0.02, uTime * 0.05)).rg; - vec2 flow = n1 + n2 - 1.0; - - vec2 tileUV = floorUV * TILES + flow * uAmount; - vec4 base = texture2D(uSampler, fract(tileUV)); // seamless tiles → fract wraps - - // depth fog: fade toward the horizon into deep teal - float near = clamp((suv.y - HORIZON) / (1.0 - HORIZON), 0.0, 1.0); // 0 far .. 1 near - base.rgb = mix(vec3(0.03, 0.15, 0.20), base.rgb, near * 0.8 + 0.2); - - // caustics where the two flow layers overlap, brighter near the camera - float caustic = pow(max(n1.r * n2.g, 0.0), 3.0) * uCaustic * (near * 0.7 + 0.3); - return vec4(base.rgb + vec3(caustic), 1.0); -} -`; - -// paint a seamless swimming-pool tile texture: grout-coloured base with inset -// tiles, so the outer border is grout on all four edges → tiles cleanly under -// the shader's fract() wrapping. -const poolTiles = (size: number) => { - const canvas = document.createElement("canvas"); - canvas.width = size; - canvas.height = size; - const ctx = canvas.getContext("2d") as CanvasRenderingContext2D; - - // grout base (also the seamless edge colour) - ctx.fillStyle = "#063a47"; - ctx.fillRect(0, 0, size, size); - - const tile = size / 4; // 4×4 tiles - for (let ry = 0; ry < 4; ry++) { - for (let rx = 0; rx < 4; rx++) { - ctx.fillStyle = (rx + ry) % 2 === 0 ? "#0d7086" : "#0a6076"; - ctx.fillRect(rx * tile + 3, ry * tile + 3, tile - 6, tile - 6); - // a soft top-left highlight on each tile for a wet, glossy read - ctx.fillStyle = "rgba(180, 230, 240, 0.06)"; - ctx.fillRect(rx * tile + 3, ry * tile + 3, tile - 6, 4); - } - } - return canvas; -}; - -class PlayScreen extends Stage { - private app!: Application; - private elapsed = 0; - private mouseStr = 0; - private effect!: ShaderEffect; - private panel?: HTMLDivElement; - - onResetEvent(app: Application) { - this.app = app; - const w = app.viewport.width; - const h = app.viewport.height; - - // the extra texture: a STATIC seamless noise field — baked once, never - // re-baked per frame (the shader scrolls it on the GPU instead) - const noise = new NoiseTexture2d({ - width: 256, - height: 256, - type: "simplex", - seed: 7, - frequency: 0.03, - octaves: 4, - gain: 0.55, - domainWarp: true, - domainWarpAmp: 6, - seamless: true, - }); - - // the refraction effect, preloaded as a "shader" asset — compiled once - // during loading; getShader returns the SHARED, loader-owned instance - this.effect = loader.getShader("refraction") as ShaderEffect; - // bind the noise as an extra sampler — "repeat" so the projected/scrolled - // floor UVs wrap seamlessly toward the horizon - this.effect.setTexture("uNoise", noise.getTexture(), "repeat"); - this.effect.setUniform("uAmount", 0.06); - this.effect.setUniform("uCaustic", 0.5); - this.effect.setUniform("uMouse", new Float32Array([0.5, 0.7])); - this.effect.setUniform("uMouseStr", 0); - - // a full-viewport sprite whose albedo is the pool tiles; the effect - // projects + refracts that texture into a perspective pool floor - const floor = new Sprite(w / 2, h / 2, { - image: poolTiles(256), - framewidth: w, - frameheight: h, - anchorPoint: { x: 0.5, y: 0.5 }, - }); - floor.shader = this.effect; - app.world.addChild(floor, 0); - - // touch the water → a swell that follows the pointer and decays - const setMouse = (gx: number, gy: number) => { - this.effect.setUniform("uMouse", new Float32Array([gx / w, gy / h])); - }; - input.registerPointerEvent("pointermove", app.viewport, (event) => { - setMouse(event.gameX, event.gameY); - this.mouseStr = Math.min(1, this.mouseStr + 0.35); - }); - input.registerPointerEvent("pointerdown", app.viewport, (event) => { - setMouse(event.gameX, event.gameY); - this.mouseStr = 1; - }); - - this.buildSlider(app); - } - - // a live "ripple strength" slider — writes the uAmount uniform directly - private buildSlider(app: Application) { - const panel = document.createElement("div"); - panel.style.cssText = - "position:absolute;top:60px;left:16px;z-index:1000;font-family:sans-serif;" + - "color:#e8f6fa;background:rgba(0,0,0,0.45);padding:8px 12px;border-radius:6px;"; - const label = document.createElement("div"); - label.textContent = "🌊 Ripple strength"; - label.style.cssText = "font-size:12px;margin-bottom:6px;"; - const slider = document.createElement("input"); - slider.type = "range"; - slider.min = "0"; - slider.max = "0.15"; - slider.step = "0.005"; - slider.value = "0.06"; - slider.style.cssText = "width:190px;display:block;"; - slider.addEventListener("input", () => { - this.effect.setUniform("uAmount", Number.parseFloat(slider.value)); - }); - const hint = document.createElement("div"); - hint.textContent = "move / click the pool to make waves"; - hint.style.cssText = "font-size:10px;margin-top:6px;opacity:0.7;"; - panel.appendChild(label); - panel.appendChild(slider); - panel.appendChild(hint); - const parent = app.renderer.getCanvas().parentElement; - if (parent) { - parent.style.position = "relative"; - parent.appendChild(panel); - } - this.panel = panel; - } - - update(dt: number) { - this.elapsed += dt / 1000; - // drive the shader clock — this is what animates the refraction, - // NOT a per-frame CPU noise re-bake - this.effect.setTime(this.elapsed); - // decay the pointer swell - this.mouseStr = Math.max(0, this.mouseStr - (dt / 1000) * 1.5); - this.effect.setUniform("uMouseStr", this.mouseStr); - super.update(dt); - // keep redrawing so the water animates every frame - return true; - } - - onDestroyEvent() { - input.releasePointerEvent("pointermove", this.app.viewport); - input.releasePointerEvent("pointerdown", this.app.viewport); - // the loader owns the shared shader — unloading is what frees it - // (the floor sprite's own cleanup skips it, since `shared` is true). - // NOTE: this example is single-stage, so its onDestroyEvent only fires - // on final teardown. In a multi-stage game, unload in your GAME's - // teardown instead — onDestroyEvent fires on every stage switch, and - // a re-entered stage would then getShader() a name that's gone. - loader.unload({ name: "refraction", type: "shader" }); - this.panel?.remove(); - } -} - -const createGame = () => { - video.init(728, 410, { - parent: "screen", - scaleMethod: "flex", - // ShaderEffect is a WebGL-only feature; under a Canvas fallback the - // custom shader is a no-op and the floor would render undistorted. - renderer: video.WEBGL, - // LINEAR filtering so the refracted floor + noise sample smoothly - antiAlias: true, - }); - - state.set(state.PLAY, new PlayScreen()); - - // ship the refraction shader as a preloaded "shader" asset: compiled once - // at load time (inline GLSL via `data` here — a `.frag` URL via `src` - // works the same) and retrieved ready-made with loader.getShader() - loader.preload( - [{ name: "refraction", type: "shader", data: REFRACTION_FRAGMENT }], - () => { - state.change(state.PLAY); - }, - false, - ); -}; - -export const ExampleWaterRefraction = createExampleComponent(createGame); diff --git a/packages/examples/src/main.tsx b/packages/examples/src/main.tsx index 7a52c9457..283f667ae 100644 --- a/packages/examples/src/main.tsx +++ b/packages/examples/src/main.tsx @@ -98,9 +98,9 @@ const ExampleSpriteIlluminator = lazy(() => default: m.ExampleSpriteIlluminator, })), ); -const ExampleWaterRefraction = lazy(() => - import("./examples/waterRefraction/ExampleWaterRefraction").then((m) => ({ - default: m.ExampleWaterRefraction, +const ExampleAquarium = lazy(() => + import("./examples/aquarium/ExampleAquarium").then((m) => ({ + default: m.ExampleAquarium, })), ); const ExampleLineOfSight = lazy(() => @@ -362,12 +362,12 @@ const examples: { "Per-pixel sprite lighting from normal maps. Animated character + foreground prop tile lit by a moving cursor light, faithfully ported from CodeAndWeb's cocos2d-x demo.", }, { - component: , - label: "Water Refraction", - path: "water-refraction", - sourceDir: "waterRefraction", + component: , + label: "Aquarium", + path: "aquarium", + sourceDir: "aquarium", description: - "GPU UV-refraction: a tiled pool floor rippled through a custom ShaderEffect that samples a second, static NoiseTexture2d bound via setTexture() — scrolled on the GPU (setTime), no per-frame CPU re-bake. Pointer swells, animated caustics, live ripple-strength slider.", + "Screen-space water refraction via renderer.toFrameTexture(): fish swim across a planted tank, then a full-screen water surface captures the live frame on the GPU and re-samples it through a scrolling NoiseTexture2d flow map — the fish shimmer as if seen through moving water, with no readPixels round-trip. The industry-standard screen-texture pattern (Godot hint_screen_texture / Unity _CameraOpaqueTexture).", }, { component: , diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index 2ffb02f3a..c3a374f76 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -3,7 +3,7 @@ ## [19.9.0] (melonJS 2) - _unreleased_ ### Added -- **`renderer.toFrameTexture()` — GPU frame capture as a `Texture2d`** (closes [#1544](https://github.com/melonjs/melonJS/issues/1544)) — capture the current camera view / framebuffer into a `Texture2d` entirely on the GPU, for screen-space effects (water refraction, heat haze, frosted glass) that need to sample "what's on screen behind me". The fourth member of the `toDataURL` / `toBlob` / `toImageBitmap` family — "the current frame as X" — and the only one whose result never leaves the GPU: a `blitFramebuffer` (WebGL 2) / `copyTexImage2D` (WebGL 1) copy of the bound framebuffer, ~10–100× cheaper than a `readPixels` round-trip and with zero CPU⇄GPU stall. The capture reflects the framebuffer **at the call site** (a `flush()` runs first — Godot `BackBufferCopy` placement), reads whichever framebuffer is bound (backbuffer or a camera post-effect FBO, so it works under `Camera2d` and `Camera3d`), and returns a `Texture2d` you feed straight to a custom effect: `effect.setTexture("uScene", renderer.toFrameTexture())`. `setTexture` now binds such a GPU-resident capture as a **live** handle — re-capturing each frame into the shared slot refreshes what the shader samples with no re-bind. Options: `target` (reuse/mint a caller-owned capture) and `region` (sub-region copy). Color only (RGB, opaque); the Canvas renderer ships an offscreen-canvas equivalent for family parity. See the new **Aquarium** example (fish rendered to the scene, refracted through a rippling water surface that samples the captured frame). +- **`renderer.toFrameTexture()` — GPU frame capture as a `Texture2d`** (closes [#1544](https://github.com/melonjs/melonJS/issues/1544)) — capture the current camera view / framebuffer into a `Texture2d` entirely on the GPU, for screen-space effects (water refraction, heat haze, frosted glass) that need to sample "what's on screen behind me". The fourth member of the `toDataURL` / `toBlob` / `toImageBitmap` family — "the current frame as X" — and the only one whose result never leaves the GPU: a `copyTexImage2D` of the bound framebuffer, ~10–100× cheaper than a `readPixels` round-trip and with zero CPU⇄GPU stall. The capture reflects the framebuffer **at the call site** (a `flush()` runs first — Godot `BackBufferCopy` placement), reads whichever framebuffer is bound (backbuffer or a camera post-effect FBO, so it works under `Camera2d` and `Camera3d`), and returns a `Texture2d` you feed straight to a custom effect: `effect.setTexture("uScene", renderer.toFrameTexture())`. `setTexture` now binds such a GPU-resident capture as a **live** handle — re-capturing each frame into the shared slot refreshes what the shader samples with no re-bind. Options: `target` (reuse/mint a caller-owned capture) and `region` (sub-region copy). Color only (RGB, opaque); the Canvas renderer ships an offscreen-canvas equivalent for family parity. See the new **Aquarium** example (fish rendered to the scene, refracted through a rippling water surface that samples the captured frame). - **BMFont XML font support for `BitmapText`** — bitmap fonts can now be loaded from the AngelCode BMFont **XML** flavour in addition to the text `.fnt` format (the serialisation is auto-detected). Many bitmap-font tools and asset packs (itch.io, dafont, …) export PNG + XML; those now load directly through the standard preloader (`{ type: "binary" }`), with no offline conversion step. The XML is parsed with a dependency-free regex parser (no `DOMParser`), so it also works outside the browser (Node / SSR) — unlike the DOM-based TMX XML path. - **Holes & compound paths in `Path2D` / SVG fills** (#1253) — a path may now contain multiple sub-paths (every `M` in an SVG string, or every `moveTo()` call, starts a new one). On `fill()` the first sub-path is the outer contour and each subsequent sub-path is treated as a hole, so donuts, rings, letter counters ("O", "A", "8") and other shapes with holes render correctly under both the WebGL renderer (earcut hole triangulation) and the Canvas renderer (native winding rule). Method semantics follow the standard [`Path2D`](https://developer.mozilla.org/en-US/docs/Web/API/Path2D) API; see the updated **SVG Shapes** example. - **`Texture2d` base class for texture assets** — a new abstract base (`me.Texture2d`) for user-constructed texture objects that own a texture source, exposed via `getTexture()`. `TextureAtlas` now extends it (no behavior change), and the renderables (`Sprite`, `Sprite3d`, `Mesh`) recognize any `Texture2d` by type and resolve it to its backing source — so a texture asset can be passed directly (`{ image: myTexture }`) just like a raw canvas. Raw DOM images/canvases and the loader's decoded `CompressedImage` data remain valid sources outside the class hierarchy. The contract deliberately admits both CPU-backed sources (a drawable canvas/image) and **GPU-resident** ones (an opaque backing that never leaves the GPU) — the shape a future WebGPU backend and screen-capture textures follow. diff --git a/packages/melonjs/src/video/texture/frametexture.js b/packages/melonjs/src/video/texture/frametexture.js index d851305f3..c7cf5385a 100644 --- a/packages/melonjs/src/video/texture/frametexture.js +++ b/packages/melonjs/src/video/texture/frametexture.js @@ -34,11 +34,6 @@ export class FrameTexture extends Texture2d { * @type {WebGLTexture|null} */ this.glTexture = null; - /** - * the blit-destination framebuffer (WebGL2 capture path), or null - * @type {WebGLFramebuffer|null} - */ - this._fbo = null; /** * marks this as a live GPU-resident source — see {@link ShaderEffect#setTexture} * @type {boolean} @@ -60,15 +55,10 @@ export class FrameTexture extends Texture2d { * capture and is done with it. Idempotent. */ destroy() { - const gl = this._renderer.gl; if (this.glTexture !== null) { - gl.deleteTexture(this.glTexture); + this._renderer.gl.deleteTexture(this.glTexture); this.glTexture = null; } - if (this._fbo !== null) { - gl.deleteFramebuffer(this._fbo); - this._fbo = null; - } } } diff --git a/packages/melonjs/src/video/webgl/webgl_renderer.js b/packages/melonjs/src/video/webgl/webgl_renderer.js index 0a457e63a..b9c2b8fb9 100644 --- a/packages/melonjs/src/video/webgl/webgl_renderer.js +++ b/packages/melonjs/src/video/webgl/webgl_renderer.js @@ -917,94 +917,31 @@ export default class WebGLRenderer extends Renderer { } } - // The destination is an RGBA texture (RGBA is what shaders sample - // cleanly). The tricky part is copying from an ALPHA-LESS framebuffer — - // melonJS creates the default context with `alpha: transparent` - // (usually false), so the backbuffer has no alpha, and both - // copyTexImage2D and copyTexSubImage2D into an RGBA texture are then - // INVALID_OPERATION under WebGL2. + // Copy the bound framebuffer into the capture texture with + // copyTexImage2D — the standard framebuffer→texture path (Three.js + // copyFramebufferToTexture uses it). An RGB internalformat is a valid + // subset of BOTH an alpha-less default framebuffer (melonJS creates the + // context with `alpha: transparent`, usually false) AND an RGBA camera + // FBO, so the copy never trips INVALID_OPERATION on a format mismatch, + // and the capture samples as an opaque backdrop (alpha = 1). Unlike a + // blitFramebuffer destination — which some drivers won't sample in the + // same frame — a copyTexImage2D texture samples reliably everywhere. const batcher = this.setBatcher("quad"); const unit = batcher.maxBatchTextures - 1; - if (this.WebGLVersion > 1) { - // WebGL2: blitFramebuffer converts formats, filling a missing alpha - // with 1 — so an alpha-less source lands as an opaque RGBA texture. - // It also reads/writes framebuffers directly (no texture-unit games). - const srcFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); - if (frame.glTexture === null) { - const tex = gl.createTexture(); - // bind through the batcher (keeps boundTextures consistent) only - // to set the sampler params; the copy itself is an FBO blit - batcher.bindTexture2D(tex, unit, false); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA, - w, - h, - 0, - gl.RGBA, - gl.UNSIGNED_BYTE, - null, - ); - frame.glTexture = tex; - } - // one reusable scratch FBO for the blit destination. The target - // texture is attached only for the blit and DETACHED immediately - // after — a texture left attached to a framebuffer is refused for - // sampling by strict drivers (feedback-loop protection). - if (typeof this._captureFBO === "undefined") { - this._captureFBO = gl.createFramebuffer(); - } - gl.bindFramebuffer(gl.READ_FRAMEBUFFER, srcFB); - gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this._captureFBO); - gl.framebufferTexture2D( - gl.DRAW_FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, - gl.TEXTURE_2D, - frame.glTexture, - 0, - ); - gl.blitFramebuffer( - x, - y, - x + w, - y + h, - 0, - 0, - w, - h, - gl.COLOR_BUFFER_BIT, - gl.NEAREST, - ); - // detach the texture and restore the source framebuffer - gl.framebufferTexture2D( - gl.DRAW_FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, - gl.TEXTURE_2D, - null, - 0, - ); - gl.bindFramebuffer(gl.FRAMEBUFFER, srcFB); - } else { - // WebGL1: copyTexImage2D(RGBA) fills a missing alpha with 1 (ES2 - // semantics), so it works from an alpha-less framebuffer. Realloc - // per capture — the legacy path; live-bind reads the handle fresh. - if (frame.glTexture !== null) { - gl.deleteTexture(frame.glTexture); - } - const tex = gl.createTexture(); - batcher.bindTexture2D(tex, unit, false); + if (frame.glTexture === null) { + frame.glTexture = gl.createTexture(); + // bind through the batcher so boundTextures stays in lockstep; the + // sampler params only need setting once (copyTexImage2D re-specifies + // the storage, not the parameters) + batcher.bindTexture2D(frame.glTexture, unit, false); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, x, y, w, h, 0); - frame.glTexture = tex; + } else { + batcher.bindTexture2D(frame.glTexture, unit, false); } + gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGB, x, y, w, h, 0); return frame; } From 79af16b0add3639fbd7bf56a933e376a0fbb4ce4 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Wed, 8 Jul 2026 19:26:18 +0800 Subject: [PATCH 08/20] feat(examples): port the contributor's aquarium to toFrameTexture (real assets + debug panel) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworks the Aquarium example to use the actual assets the contributor provided (seabed background, fish swim sheet, water texture — TexturePacker atlas shipped as aquarium.webp) instead of procedural stand-ins, and adapts their water effect to the 19.9 API: the original captured the screen with gl.readPixels + texImage2D on a fork; the WaterSurface now calls renderer.toFrameTexture() in draw() to grab the frame on the GPU and binds it to the refraction ShaderEffect as uScene. - Regions cropped from the atlas webp (seabed + water pre-scaled to the viewport so a full-screen sprite's framewidth matches its image → exact cover + clean UVs; fish sheet kept at 128² as a 2×2 of 64px frames). - Fixed-resolution `scale:"auto"` (not flex, which grew the viewport after onResetEvent read it and left the scene in a corner). - Shader samples the capture in the quad's own UV space (Y-flipped for the Y-up framebuffer), not gl_FragCoord/a resolution uniform, so it's correct under any canvas scaling. - @melonjs/debug-plugin registered and shown by default (an HTML overlay, so toFrameTexture doesn't capture it). Asset credit added to LICENSE.md. Verified end-to-end on ANGLE Metal: seabed fills the viewport, fish swim and refract through the captured frame, debug panel visible. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- packages/examples/LICENSE.md | 9 + .../public/assets/aquarium/aquarium.webp | Bin 0 -> 324436 bytes .../src/examples/aquarium/ExampleAquarium.tsx | 310 +++++++----------- packages/examples/src/main.tsx | 2 +- 4 files changed, 120 insertions(+), 201 deletions(-) create mode 100644 packages/examples/public/assets/aquarium/aquarium.webp diff --git a/packages/examples/LICENSE.md b/packages/examples/LICENSE.md index cac19e573..9b5329a0a 100644 --- a/packages/examples/LICENSE.md +++ b/packages/examples/LICENSE.md @@ -26,6 +26,15 @@ SOFTWARE. ## Third-party assets +### `aquarium` example + +The texture atlas in `public/assets/aquarium/aquarium.webp` was contributed by +a melonJS user with their aquarium demo (adapted here to the `toFrameTexture` +API). It packs a top-down seabed background ("Free Top-Down Seabed Objects +Pixel Art"), a fish animation sheet, and a water-surface texture. Provided by +the contributor for use in the melonJS examples; refer to the original asset +packs for any upstream attribution terms. + ### `pool-matter` example Pool table backdrop and 16 numbered-ball sprites in diff --git a/packages/examples/public/assets/aquarium/aquarium.webp b/packages/examples/public/assets/aquarium/aquarium.webp new file mode 100644 index 0000000000000000000000000000000000000000..92e150238c2339802fc6315d187da068602cbd55 GIT binary patch literal 324436 zcmaI7Q*6=>B z^>jZ~)m19e5)yI`P+*$kB1#%cJX#1~U|?wf76|gcDJ-WXr3DEF29Ek2gpMAV*mPyj zbfj92py-hD$IP)HZ0Mr8E*L3?>OP)mR#*9(^{Q-NnrkUFu}49rhfz6&5FXYX2)@tZ zkIjqW|1oy*Od5%YK)nxB`b@{kBa<euwT3BMa8f>!WhDN=vw zMDXiIC_!2dN|aI7Xkc_*QBvJnGz^%xw=d>*=H1j4Rj2uV!xDQy*~zaDsrNy)}|j7r8mw9;r?G}csBy(0J2j8IqX z#!1C+*a1oKXG*fS_X^zjn$_X0Hg`A`f?I7`(X+pG=3++1hu0^wqOF8{o84dBH$Sd> zd*=ieq-s*Lv01pSszmLHs%UF4bahSHgE}W->_!_?hLW0{%Tj{SBWBYHggOv}eh_Gs zRRyQH!+$adw2}=rOa{BPa{bdf3#>{3BY0YRA_gfB2}jK6L1oiZ8d}gP5y3}5C*I$M zg=*IamavQRpWYDPr4aqo$ooje{m)Q0UjO7Y(q45X!1a=u!!%rP-633BM{1? zO=TrPJxq2YSq2sVTksj4DaBJ&UkH{4^9bDC@DcgG!L1eWC4C_PC8owU4zo!*q5W<;+b zV=*id%$rplQZAKft?4md^?31UW#3`7NK-@~b`~gqcdYbbmMkCHgRw(gWh3dh)qMl} zfSuY*uZ}Fh2~v+plGz_BOZiWBN(Mv5TA4^0R-p1v=sG%xTB%5Xb~E3QIdv5DGTxR#x(M&HA+r1O_RS0Dh+n3<{eh5W<5&03g4pNf2kZ@D07`{ z)&D1TUP<}UV(TTBs1~;IqxSzYZagGkKmahve>aZw?+p}K-4H?!I6W8?7?dC$La;Cy zw1RgO6qqri{f_@=#+G!Hxv%5zo!h(v5E;m~bJq+c=y&lMc9r*9`_+5pr}_%2n=$=5 z_+0sLyajy;ZGH9ly?``z4Gw@iAixL9}jP zW%manJ zgSJUb032VIUpFr|kA@#Y%YLU{fk5%ki!a#co%`NLz&P;O=f%em2)GB`c+a|#+=AYE z$3gWk(9dBPpa-ELKoO`PSoGq3{B0L>1IhxKgBo8B4jt!(y8bE80GtGcea3-6{zB72 zHGmNiGB6I<`}XgI9(p=Kx}c4()*VoN*n00G;0a{;*82?kMe#}U1$YEp0j@x)AP`9R z&i4JE$u9t#pj(jRzYWP@p#{Jc=p3XB+yI4rD1t!m0O8&ofCDH4DEg%c{AU6X=+O@h zunu|x@d1TEphBTOp%c(6C=Nsd0?n`7qCNm#Kr5hf(9%Eqf8P83@x=c9Qi=P;8aVsA zbrjp@&nV&$?o7r_Zfe1BHgA@x`+gwL{Rx=%#&lc%hR*3+ubIPU1#DszqD7@Mgc73c z32OFdA@169`XJsDEL$fhW<{|FtbUhbr2q8=iGL2~%1))8c}6e1eEG%VUURk%2{H=G z{btW{?+%{k1B*$Rb6$Gxc;yNAc9YuCefAo$ol8zzzGjg7j8!T@fm$pYHux7l@m!cWaRY`te;~W>cQ4Z{-fXHqI)UT@)P8lTplrlH9ftlH`7a+ zlnr8>CJZH?DwK?mgT)KMai7QS_=2l zwPUCXn;b;)ST94&kb&xL8_3;#urVYQm{eE&N6S|gAGgABKEebgKmu#_j>gFLNlcZ} zDR2VQnY%|pWEPuD`B`=ns=tlCHI0#StlB8Jr7*RbP6-pitLcq1(vv=s5AV*G*Gm`I z$&Is+YLot2Q=W4MQ9>En?b>^B55Da(GGMO6pIDeHvRI-TP4V3L0v%4Fm?yRaJ~8L- zM@Iw7-etw3)r2_FRjT-$N`_RUM^w7)lw7!`amoW{k1#qSq*PCQH(f7$;hOEhjH19& zhQ z+63`ZhjH2kmcbvRd$AXTVxcSs zy^e2ijGi?Y0^L7*Fprc$8#~3T6#|g6$qRU}nlm0+#Wvx{dm}<4IGE=Pg)lDS&=YS3 zXH4q4#TrOn7Locx_Aa{Os)(u8d}=r0#ZI$rgFwC2cT2=y)I=PVSqM_muRIH5X-Ci} z4n|5CIno-r!P5Ljv71x;bAuuXtEoh)%s(IrX?^Cy0ReXF$WPZdNGuTiq$oHx=19yB ztMQr9+bJ*b#Davh*5F$EPC%!F%d3hA764Jl==ZpKXKFCyTw-KCL5`QA8zdiwpXmWl z5et4@11E{`C*|DYkj&L(!7TgJ#GcP4uy|(U5d(5W$G*NHGQZ(Q4;n4iG!`>Ce3h+n zwyNNttOBm$8y!7Rl7nf)V8AAy`cD+#M(}I|*#KXHLgbOl+3kn+6tO{p`^d$`Z-3mQ zdFxt7JLrr~_BH+#L$SiMD$Zc_)Pndykkk7PKJ)cuEFq1hl{(7fzC9>3FSA42D_Wn+ z`URL}>=a?=44?V}yV$!+9}{3-N2ZU?ugDE{BDY&lnJZ$Aq1OHNp%`*;a-tRHw5lW$-@m=*-3l-0KGvm|*vaz_r&AP`VZ^2%+ znWRTw=O98+NoMYq_QF(LXj0U7X+~{|-x(qE(Y=X~gWz0EGHe5Cu$s-AxTA_9x4V0M zY)sU}RG2yt1^FA+xBsdv(hd}ET-nh)i;&b(G+ehXn{9>vNFQI`U6@wWTsYNJmU0X7 ztN%)v&5ih^B&DaVA@{P^WAP?q8{O})X1jN7ZK>Hqx!`_GJlQ~PAqwyL}t{{6Ur{UH24ul1tefCEnv(_HydQb`2LvlA~}RJmh> zLB`cADx_Xf`*o_HqCB1tn*olg=TPBAYSx6)qOG$7u0s6A{!8ZX2ypvoO`QS7U(iYI zc{g0{4)10%tWSIiA9txHr4@Pg>cstUnP6)>q}8Ny4Y;ZSc|X0Z_h4L7fj74vsri4cclD+Gvvz#*|3$)Kw;I#t-`uY{`0B&iLQu1E(^C`m{O6u(F#~X^e z4LcaJM%)g*WR+fge1o{tBC7Yn-YCC|-sEz}?jCc}+WQy5`Ec(|N5mzbJttB^HOXRF z4y$}Mjx%H=o`?jYE|ncd%eA z?~9fRZL$5#mNk?-5R9D1`g;@prs|_kvcZ>wh-kzCxW(zci`m1 zWYh&$R*)|6{OsS&ThH484KL`FcOj(q%Ud*DhJ5^XNm2dJbD; zG}IiOnc&bNOo4x+A<8N8a-=9JzOrdGbm!qopUh)0kQdQF9W6xyMGT~_A1 zYfK)=c&KmV5-;AaCD6w|RrsrJPCZ0AZK){7J)zTjQP5CaUOr<5Uiodkp!IStuSt@oK;G0jT=}Zj+pvF_fW?08Ah114A>L_t>L2zg!L zE{gFRL#D2FHk}n{zuf;)=0k9k1lkx3%x{laL1arBCT0=Ftz8y8tn)PgVvcIwRjNh4 zaTN__{|hNRdqS9+Ldh6mPF9j}S#%!)A@@3F*{V&bIMa$_URupnBZ(^^O1BLlU*qes z&4o^7h{L8Bu945#pV6SNAh2<`<}To=)CrVfdE)c`TVA+uKoER<{@K^F4o9wA_t6P|{ow(_;kF?Ye#=YrAskFK2KW<=?bG$?_E(LoF&oN&Bv_t9>F>d2Cmbua{IxSs+9AeN?z*0{ zCPVVY%R$DlgJ37i@zD`{P8T(P%k%bU5K0t5ZTw6)H>PE_uI+~R^6+3kbe>?{She8$ zwF9b)axXDP41d9mmO^`9o7Wkv$J8rN`#nML88cXdtt;dOn(XYI=}yNAl05y!3$r~9 z-9!M0FDU-?s(k|o`y*2CLY+hfj(Dfp?15cLDuTYv_tAmM=(tL+iq^;aQ1a)$EDWcU zqHBertf_i3TXl^i9X?SOg+a2%N(*Px+ZN?<4dr(C4NPImV$w!-rPShK8xvgQ8^f=s zHaKPiULpU=eo#ZJ)>FJwivWf1c^0!BlV@-!^Q_NaIbLFN*qCD7lX>&; zuhe@VG;=?=v=kE=TacD`1OA(S8A0E>egf110`FskiS^5l9-Wq5!Q5;^9+)4n1@WecrJF!}3sx7}s885gzNNE(4c z5oh1_;C{HGrBn4lAb8PISJKEL9c<6)1pP+8WdGn`D}1xc7loJ(hjcCEla=DRg_fxs z>|3cgE{FM7@hhqse}MURTZo}<<1{u}Gs-W7PiCR4ij`gH4G+)g8W^m0D`mHxBh2{B zF=M%LPmoy+5S+Tf(mLE7lW^V9a&kCV=j+)0JZ-5a`7TWD(3xYsFz#g+v*i8*DwKRW zZ+sBMI^dW9HF#fdFU6QgU{ z{6=Do29pHW-39B{?@A+2bNC3#VIEDg@9pH zw$Sb2V*Z(ZUmCZ#-!3rZprVYXyrx7qOXB0raa{Ph6o@?NnvF6%$ia&YY~LvqhJ>uy zShmPghPF5S@mg`c)GdfNHzRd6T#7g4O}1M0$|C;$Q-l;eFimZz2zz9@F3}v{7PU;_ zmDtG-4%UoUAj&+{R<=Px$}0@N^mj?cmutN5TW+mC*)$Zn6GF({O-K#jN(7px5*NkX^W>so-x|`|0pz$Vhrp-VMr9Bo!HDE$0B+jH zcQq;1G4PsO8lZ-I22VJrnYVOpb7DST_$6sFPhh9HxQuuuf~T9-w<2?TM1jBc7HMzx z?%CRvd>rpgvW+-YkdTSoW{#-$y~zKB`ZILX3{M>2%&fJX-v75|BpqjrFzz?m{>6K6 zENKn#DwbNtaFg?#-i}W}qF-lu!Y$tY32S@r>BSww`@7VdrfgR}Wv!Cq7=-0$Ym>;k zeBtMIgA@to&H8AE3-A|x&PN$88QhuvKHF^lM3aQ;&f$qf)T!v7i;(3y+TP?1E4wB1 zngf4NT(fbM)J`vuA?3)IN4U1aRvc1(GfV=;WuFwmZ(NizVC7+2Odbge-^21KEu2>4 z9s^pRDMuVz4)BiT)FpC0KX9aqup1aIHu$DwDKYEc?hQ>iyIb*s(uAW~ambKk^ z`xM0!c#k(zI%Bq^Fhq#tpKrtMX!Xko8#i%&aj`*UF+Nj@*0;NCq*}p~)-8r)#_>~} zySP;>I$(_4U3&+gX}we5YW2?>N~kE^`y0KQ<@y{Nc$-=g6;2#-t0G@m%`v2(AZF)C zh3Y%_6`!%%A$@a#`b8fi{?e{n(?Yb`niQ!`rPNGUozsy~Y7 zQ;<`5h(J^>LMPE4MZm?1cWzsWBlCRVQrhoEt)i^J#o|QZt=0pF`~z$~LpRqGMMR%& zm`tZ@6*R{4hPj>zEKfXy-Kh@3LcLf>`oPQxS#JCU0kKe4>+I_qxA=ITaZXz_9tDP{K;Wsu%F&L?!{wI-KHk2vQumla!} zrZ}(C{&+E>Aiu#mvTGPfv>_0=(*g;}1eX~kGMvnwu+ci^M3D&NHt`%=`TZPYIAG-> zQF%XgiEHH_Gt$4lkbE|SF--=`5giCKuu4Z%hVeLxBgt3!{K)K^Uers z-eY36ul_LB9>p{RpS~P_%{=rynAPGf*&m9Q^3I#+D~x@F47XfTb@<9}bod&|&+Hp( zqra%>e)m)L9Gb|>emw@#yDt_c`jCb*F8Ee+Ae$w%vZ4E`lK+V0agcL~nxM*Su}GWR zaqW31&gzy)$iqmz^Ktdi_tG>*6QvG%kMr=;jh{%@$rGDO;SOLE{}b|FKy$vvI|b@eu&lw643B?I<)@GgNp0W+QrtRRVeO zASA4~^P-wnhA548)E7E!$r?&}9aeL^aNH)@C!vzuUcGjaPr1r&l=<^@5p&3kQ)LX^ zUnPU-5R(3A7(pcMd`qc}lFP&nI}e)(7((Z2_=1veIzNubwLJ6Ta8KZ+McZ2)dyL2N z^ZSl6S{1#Jj0odTu3kyQkDH- zEk0rHl2Jj1_}VC$+bYvFEpXX?tdiyFRJ8_5kC3NqV3x(4_pa;L zk|jFU{*XAHM?<&Szhy&#v81VI>K;Z)UNrq}p_P7x1;hseds)Qp9JBBhS>XP&?f;~s z>)xsAuK6VR{mSY7sqb@Qe7CJ4)|9yGyBYiu_G%N?(aEQ*Jg7t?V!X%+0Jh%Wi4hRt zCk%ZYQ8+2)>Q#mb%H_Ctmo3Sd-7Oi*zm;@Ov{(46U#}0&L5Q@ z;I-9(!K>!GJVw=hOe?wPYJ#R6va#Fu)3avIzFP-_rh{%6NSkgm%`EqO_`h5|x>Rul z#U^#DoLG)|d=|BynSca*O4Ws%3e9ikdDb$#tcjPQ9Ggh4<1|spj(peft>;8TQK*1p zG~Ig;nwL@B3QEb^W=a@{B@eZ6@CpypF_%^lcL~pK4Z_*?n7+{8P~fAT3lMsli3UYj zUM%HkG1hR18x8H1R9)G8DumpaiEyfGC9m(Z%N%BL>9kUo>(bxbo(r!P!TUA;^YOk}EE>wH zBVjQ7uB0-pk7wCYu@fcC;aL+Ig(`Y7%iR6djw77m?kwxZtsd)p8&rR}K?#AF1x-Ri z0!n#^ISfsaZomIUpPTSm9P|Bl0;voPU;2jNi)$Ais%*P>zaQFlpo;|8OI;>0ba=%7GV909K)l-u zCZA-{mx<4ZDMtnj+4xD^M|K$`(|rlhcVyvMRL^``d$4Z73-;jg`&eJZsd9g$>eA}1 zU8_#%eG+<&BtRpUu@{|zs-L<=g7r4iHMCfOFqt5EP+R?8kVg5$)wm@03o}-0h)zYW zocZ#|I?>_O8kN?=>5t+QlYZ}w*Sl=CgUXzu|NamTLL_PL6)*AhCQ1DYb%YgX;w^sk z`R%ZGk81rAK66F7!lhxqP^k&x5T2EK1&+(pBJ6_~T6EZb zIe0hc_B|F(PTbOxbck(a&Wt$mW@Cxi1FaLc@8(i5@h_{?EeSAR}iM4E`ZV1>x`bSM*Gjpt#=F^X^|hPzpDH(9bsw1nf++(bjW z^8&v_pp>e&UB2BDyrch&qN6`gs=^MW;wrDBGSs;uF<2z$u$vUE&-ap&$iQ>+BWZ4z z@2IO(xAW)9eE!PMfLF1oO z9%_2|s(bWG(-^5muuBCvh37}`YIo05fTO3Y26)U{9!wi1;WHU7 zD|VcA?$+nQ*GvYcwxmw3v!<8R&O0wQJN^Z8PvTXqt)qa=gQg{pc#FCdyk4dCYv`9g9cah@eT;w)~RJAuqwuBoewvST)AGk!$)UIW1B z!hPG|S}DuNnG3S!_)kZ+gMCHMc0^WOL4EGEjutm(j~}U)SO5LOW}=+*^|K4t`Ad4* zR*t*_Qm=@+)H@$Y_A^#rs??YmYgnrf2+7|&7Honf=>rw;y)+?Xkpodk$c_QQ1?U%{ zvP~!Rr;&SzeBxqGI0NLiGd`oB9va`T+l0JjF;sTUOyp99$q}j9^lu^RImqN})0xE+ zZ=(@QRc{`nma60=sEldRDc2i$SsD$G$SFE^)Zte%mILGuPYFeuR6eHmmp+kshvLcp(~$Zw-fhW+K1|N&~DbZ z{$&;=t@1_-IGy|iM1{9D@r}V}@CFA~iL7pdg33Zb9xs0vsZ*r41%|zQ>5>?YL>eir z0y;mSngMrguk&$|=>=i_0LzWF6r_uvNfP;1B&5?Ad#$9yEKf-&)4_jy<;4ofk23l1 z3}&E(;!nf2hNLGSLVupQByHls@!1i4_c1jv!5q8sZfmN5z$$%AI@sHnDc49oGS>SMLv8hhD(QEO_^FmmtQ9<_nAwn)pYf z7beMcWQB?4Qg7!N^ng_?KVw${S+sp{0t_}~4+GKuLPGs~3GcmJ747>3)VX^K82*>i zbP-mzY6F$XB%KT5&)%3Hkt+On)QC)OLbJ*MI+UeYaWK7wUe`}q^(zJ=k=zffY={xS zK%emd-pCf`F;{UjWO~EZ*2Y-aN_@!Uf`qyT>pic2@3!z=a;;j5hjjkW-;|Ts9F$vd z!y*wRljQg!Z@fs)KN=b=E>rA020|g;>0h!UY$udf%JDF(lO9{HGl!|m;Q80rS`>(*`seRoqS@Llf zjp{3}bwWBUp=xRH^028jHl46G&Ddnx`V)4L=#ex8gD|r-tPJK7{sLCeOwfH#YcgcH z&HOfo64q`6F^*u~Be;DzUVpkrehO);ak41pE7SM#g!{$)3~R7$e!0Ce0rv{ymCjvD z)$#VG7bd$lV|X*gtge8Mj79Q}JKV4Y6O{(!!b;jYme&!eN?-1Xfn`7%%MW=*{b^Bo zzyiK+V#c2?{$mJlb{2I~ODNG)V23%mq2#1Aa>od>t29c%t;xTeOckQ(ZQx%4&AukB zn@ttaAL1XDWMwkEOyzj16f)geeSW_PGvgO+$fe(P6iZD7K+U@)1+x^3;5mxw&{vl1rK#<&T zvGzQ*pCuax6d%b|9#!g%pgbt>Q0Xz{T(gZ{Xy4EQ7sIyX3fH3f;HXrq`}hTddiKUc zn)pQ(R4GBHA#mvr?L7x&vTM^E=qrdrlcCju-+k3k&C$^+ebwn>%9dTDC(J1*IL|AG zTVKf=ZQ$*uxZI9RfO72?s`zcZH_wVFWWhw6NF~p!)@*^8J#O~f_)oT1boaxEA2N!@ z5w_7(p_0fW6)asW;9$xLaAp#*d268%rB6S~={1*G=bY1aGS5cx@w;@4) zRzE0Gz&K)V3cOg5_l;7rF3PzaZE4sR1~Jf2m`>>BFmj+Vkm@|6kCM--HNSfL0>DCN z7U{kRwVhnZIlW))xF(b2vG#nRB)3^xD67cM67 z%ND;W;xfqM^k(uZf3F)O?&T2Rbg_#5fS+KVr|~4O5cDDJTmIiWKTYhl=uKJN&v5Y_ z|E~~>l8!9zUFtpgTB2;30+l#Qb50Bb(@M^2_7Y@Xk0oZOUZ|X7jSxFu2stYrAuQ8G zP1C+wW<^x8NL^>mViV?7ua}H5qYHi~iH)*0UuWKqcpvF*IcpwC1isQ`?sw2?6LcDW zfzo>xUy9l6K-aOQ*g{fbm;J7wlMRw&Avi4trR>qveCa)R7lXo{ukDSV(y&ph^&py9 zU+Bt<{T;5zJZLyI(4d`py^XT3Gs8sBRTmZU(fV?5W-?!3{0td3?dXs_L2~bnsTjY0 zS9w9a+-K)g^s|swNRW{!{0!+0I#%KmtU5}{Tvqlpq6;`$FhhYH3cbbyMTD=%?7{uKS7vp$1}j9m|p4>}+aqe0!^M2jE-7U=za zY`tp?viFu#SDvue*30AUo^+wxyR6MN;D6QmQDt9g-Xy73om;mh%ZG8k-~ZD3)7L@a zL}ltrh0RH@abvV6asltyiq4hQ3*_Jt6n-%N=Ub^_VinQ>*6%(B3UmEi*RpF7CkR4^AaO9|V)=(^z$PrzBp3OdZUwW#I4H1Z>JhREFl$!LGi zKUBixv)oO~@oM|*Q=}q`NY@wpoCX%p0hx!%zUhX};kLvv7Vt9QN zWZ14Bz71=^)cn_Jb)D!8Dj3x~5lA2F>htLK5|}BsJ|$JL*BUw|LY{_T-z;9(YZER4 zWSk2&2GoP`OI{d48MMN~P_jScZ5Cg(Ki2YVJR9)Oh zNmAjk!WxEFr@9eMt~~ajM7GTK6o{cnE!DUfFRZ$wa+CsnxJwMIc(hfyJdtK*%Z3@# zG{8p?K1&Q**v#F`-PpsyGnHf@Um*nc^|43PUM(EA%_Z@N&v|MI;w?J1!nC+ z2dT`gukt`YU3Y#WtEetJiRp#OQ)uoGlhaBNzv;yjs$&@yyVa4-%!xdDTyNrhw_zfE z^5{dOE+@%;K8MC(cHe&l@GzW6f4<9Cnz-utF4{9qsf|*FXngmTubeFbGh5;clT z5+&Ng9KQ%w1O4Jk))+O?VX1*%Vv;ca1hLir$T^~K-kU4M#W4#>KPhf3@#mJ(td7&~QmyO%+4 z$V9alQvUyM|7K(F37CElPexwuPWT~l7bs`a`>B0&`xRAkj0op08BGe83jm9Nhfmc9 z$OXHYOZELx6d9UfcJjHNqmI|ObAI55@+Kg=m-p8-ZQkzaV=x^QDD^E#$SY;$fT!_W z!;1f}9{CJl_5)m}OhPfTVGZA7sn3|(p~ zyBJv~Tj=&Bg(7Kk1-MB=tr`tF9!=w8nOumLub5kO6rc+FcAM0gGT5`e0=kU|#5e(! z+S76N>0YX8Ocr@7A2~Ym25p0eJ5W4@4(8u>0QO>J0354_v#9# zbw;1XRJIeXvVUwR`(B|N&d66O`3`+#ea`$=+xz{n@rmj~lY`pXHvEK^Z#f2Yw5r2; zRu7lg;(Y$pNuJsbOZ-<(y`fv@QND*JxxRalYlz{Vea8uQ{0*kPWpsn#SytQUJh@?& z5zFkI1R_$-pY^{Tk)+_jX9*z-oIsMfrcKy{jMo=jJx^kH>=Vs*4AcoL%*7DSn1k$F zyM^tJ$FaxxM8uziMRDK0C#^}SyLFYbAJU*Ef5ZHv4=xf;l?7@;LDM>+#sG zWBAhDj}pt&8O!m97lU>EdBepS_OJ0A&@`e`>70%cc#(D}gTef->Rh)Z(t1D0gvkc*IPDV63#fIwHO2JWC=HG5i&A z-{Rg~mi{msppS{-87O%qE5xdXC2RUd2qpU<;r@H|)xiuFLgb_du_^{v3^MXQd;{}Q zz%OXZf4gBLdS;zPaQZv9FSP3@-E0G1sFSD`9OF8>sDsx5-GWSju9_^^*Y`hb*vY%E ztjvD!$KU@zI`)e;JfRs*`c0gii2K9Yd%w5$RZjE*2x+rUttd7@hFS%mtaowxZGHlJ!}?J?lF+`RNf3cCzSb&FHdr+PUY6s5{SO z7LjL}c5ZM5E@CP!T~AjamedSf?tXZ8Zo-e*HU+l8_+jd=>G=%ra@)8Urr^gPj9|a; z#9>Xh2J|AsJOU#C3BFr}yIqx?K2C;hYuI7?M4rufLxYtTAr+Le9{WK)jSZeC(`=E} zE_ZLd-b~Fj<6?BhTx5AiI5Qr$Tk0q+otP_c`s^-0%~GkO<3+_^-;X;N+3~XsT#rXE^XM-^?beb|0+zy<%f7f)P^i&k?5piHEg5NHrwtDw=RZv zXd$f83T&*BW+kZ@*T9Y-{6jpU%sBey!alC{hD&>o$cQ|W2tBt(o(@S{`Lrk!G6&f@ zj-iYm7;AgzX|%MMRDH|3cD+4DrQVJ%3FF9AP4vSgJy-x~&amBfH!l8KdaK zCPe|>{RT~R4e5!=8p*=A9TtvxI}7ng<1zm{AB1{++)$J7-#--?C0lge`9E^Q5<_SI zrj0{BF&e=XtAmq|*BOiNbd1+mm185;`+1!!%>V+;9v0#olwydUy__ zqA=1N!g5f~rv=hc1b!l}BWC$$L&9gS0-Wdw^LSg==hWy$xKt5*UHZVztx?GD^gioj zaAS4!C7uCy5+we(=`4@mG75$91Upxp`Hw?SFCs<@!L+r7e0=zGDiemZO2u;9QFj|9 z8e%jW^%$Xj&fB@g`9f3DJ785r)JQvkI+O^#k-r@_mfSGgS^Ddpg^#C!Xi}#qQ6=e_m`IK zkx?)Z5wgk?n;`*SnG}h8uS<>TL{~P6^(IB&|7x1^Ly=lEsH zpG?(Ifth(fY1p3US-?{i;b|Tb=kvPHo2D9(K@n?+VLuo6o zgY0Rxuj_gf>yh|Oj<3d0_NV$7{(bTNuOH{BeW^~~dvC!Hp%oc<2hh%K(b~~cPWB}` z3{#tN{yqOv-|QiAH?Xog01MaADqHrF@_8wJvk+VUJ-mVeHgE4dlsf&V&)uwuQMe@D z=#Qv$XA?^7D$TeTC!AsR9GALAYaCc){(o@~(MgDYWTYW9jEw3RlsWPc*PK^kw#Oc{ z3mD|Z?dciC$1g(FnTWwVI{?PyDJg!lR!g8_nf~LR*Wzt|z|R;C zzCd_yE z@df$c!PsG5?@jA@g89!16jkL*#9hMc#R$6AM)sWj$2b24KqrZkjH>S>H1uGdVb8|0 z&-m@m#-;DQbicx?#zGkr&DQG9S^K!SGpG6o+8PR|TGsF%-BEmU#!##zig2PZ#rhky z&w`cZ@#I)Ez{$Z?)RYB|0xuAB0Z!M??U5iR(GVh0CD6RFz_qimkW9s%$T$Cqyf6f^ zAFIvhQ+IDXFtV~{g`_zN)CCJ_PrOamkNTdK`hrxOPt^VU4#lZOeo z2ItS&N~1*_eb)}|N#-lV>p@NS)f8B4qPp8aOi8N)Q|WD z*`d8Bk=ym&yOy-Jcb8x&ntbcwneIcf%8W|~2RsfidmWNAS~2AjG2de~g9YkBe?d+~ z9S_EHnUh~EPwr*ujAJsYO6yzuxp_-M)VGx+7X_o$emcsgsPb1`KZq$3x@0|>1lEpQ zZzYhKyj-w9bZ5vs{@F{YcTlLb?>)h)y*koW_{6GT;>3#bWKc~kmaC{AP~|iL`y>uJ zm01ikt$-5~TkONt@y26zrWHEV&9~beV09yRG?Ol|=6mBKWyY1ijzW_!nLt=`U_yb`~$#bz=?Q8qrYHc`AX1e?hla ze+Key>hqu4eLK#85+g;Q$&BB|v^MW-&l$^lrVg{?uP9F5!s@2~A6~2Hr@mY#1?j~C zm^NUoaf&N&5sM$rg$$gd61KL){0$n_s%2Z0GhM$!g~qBQ1H_Z^qgEr15>gjB(o{Y> zY(r0B-tHCEqKr}AYsK$Ty&|n~yn~rt7;Dw6`OK=qfg%9@6tI-^iY1GJxnnjh zZ-`D*fg)$&(yk6mS|y$GQ6wXWo0JRDwy=C7x1Rtg-1>gH%YcDXj29TLEh)2-$vU^( z*y-Ak!i!r5Ve@0VSu;9X$Li!ko=GpuW!YBsVA^_Hg5UzRKMC|88LHrqclM0voJnb7#m0Fj$k8o<^OLJMc)amBj zL9qURnE`3$=S9Km)t)A8L}RhrY0G?u;p0!jF+0v7H(l;^;0Y4%;5i169Bsyo>_4t7 z!r&&%88-H?^$c(9hwaE6=)YTjG&{^(tQd?Bkvb%$ObO?*{Yvs5C7;zu#2Pq-z6z{+ z*ZdOjqff3svziV<7nCv{iEP`~h(k06GfN>_z1vnY>!})H#fP?ii9XS?u-!s;V;kue zlfeJxy}3k*p?c~<6?t_NHTe7{#b0O>ZehaGPu*Fauu1v3V-HR?3&oH8jS{xqqA&`+ zPZWC1v9z(c16p5X)BYs_bSUv{X@V7*M8@tB_;KPi)36XHPLZ-zbZUd0)+sgeJG8c0sQ{^D%cYl=_dcNb*HGWkHg(}k{MPtcdZ2CVhGkU=lY zOU{d&zoaj;=R^YxJ7JH+HfQL{Y-+-$Y47Cdcn`igi0bxEU5f6k7^w;|)hW|2^WDmM z60ZZY&e&IqM%L<a)kVG%|PF6V`xmnFxJfD*n+z;)}N-tLEBd_%QEICFCI3jzd3~q6?_xRfTv3Wu} zFDx^N!i;6jK|qar;tnR4In_`o1YQw8|ECQ7=d*4G1SOF!A{F~?d(guZ?%ab}fPLW1 zcd~S4GkqMBywZUsf zLzPLe12{7}t()(&v5^X2+q@8L{g+TeC=;qep#AH(Z!LRzo*5cn@>t#h_tubtiAQM; zV)(G$*>&xP*v~dMbDl%z%E#j75T-1^AQnREzXmJ7gbti({KTOGA=x&sGFk>!P z24Ym36sWS!7o2PyI%nFU3^_{-JS(vJ9*?!r$1D#O?D|PxLeZlT@J`jVQ0^RC1*kBRGB_PTw#mwJg2p%+5N$cPi72t$hLI6)^5L4|!dT+i@wcXL z+|W;2dtK2lnZr_b8b3M>nmYA5w;)rEdgj1k+sM+^?a8%s4~!@0pT4WfT0NTeB%exo zCZn1o?VmMrqyLrb>CvdrHY1hqUhAYBd~0+$*s9-}#L@jYomtf(R@|Oz-XQ2nPOwX% zPGpSByW+5c!5`7o%KW&7J;!?{p_Ml~v`?@$-TLz1Wg-^O+SSv{+&@>nO%JmWNE_l4 zh2CzZ4#uw{%OYT?JxP0PZTqh<#8zqS`8EARqPLQsPN3WK5G3Rm{tX)kR6(wbXZ|R) zi8iVC{o9XqgzJx-e#>i$7(X1MnZ!z;5-|9V2w!oY+aK^{KHvGTgG--+m#IOj3xyEkI>1-)oo#zXcyP(hSe`9*Ew^fr8 z)9*n+Sn0m@vj;Ej%#o5dQ`#Ru`HVJop@#^Dj|O|9)%=zG4|7El9bYeTx3DO^9p~Oo z>viu+2_hE@iV!-07|k=@eAj|3{P`cv?_JoKZ$(|_qBzRR{@-35F>MYgH5je1nZ``; zkEw^61he^#$^LX9lsdy32l)q$*9q6ZWBFdGZg$;sA~L1bYUDY(_=}7-WAKY%J?H3b z+ZYq_p7c)>2!%?0!6sWuw#iJ{x-lm`9ytIS#smH}-r&=D#LwfikY+bLiVL-vi!FdI z)fks!^iomjwpxe`tPqX9cj3rya!gII+hu3em}y$N=gTTwzYv7<3ijvfY%*jCTtSS%qU{eBp2L~PK~*j>7zKwd^OiGH3x^7y0s5?#dq(tiJ`+(PJg zhIY5FGx*r{<4>Kq%~0y{Hv=X8YiQuht(Bw84xPiBSqNlld}P|44tYNq(> zA}_jB9-zt-K)0SX>I;rNmI>8`m0!5NyTfDV|ZT-j|5C{yEmfKP~nKH{h5`o z>iQ-@HfDO>6;x0FHuzcR#-sBF3`V$vt|M0%<(=joO7+9T%O#+HxxaI1!kVBs+Tthh z={Ow_2=ad5!`En3b7YkMrbOKhZEg`INsi%SBnPA=snWl7@YaKmKTlZCUFkp+_#(H$ zXX3m%oQ+N^$bC00;rg~0vK)M!?kDm@wp5b`XCXHUxHyL1dUJ>@d?Bdspnom_fi3!y zt(%DbM)X2&wy1hQi_Y{V6)O2ob&y^^k!U@VRdXV9%{dgLg!P#Nu#Xxou_RFz=HJTy zH~sRDif+Ml<>k^>e~7SZxS0NsoKIggIvAFu-cE`twOikN!_aC_?{dvFje!Z{kh%*0 ziItS+)zq^&4og%Mj$L0UG*z|G=DDrPz#b-!O5<_`1`SsExcvLo+xfRzQT*G7&Djh% z^Q@67UB(o%5@>+5nzn%heTNWsxffpnd28oDj<7v#dCa)aI;Xn%WJ_Y@v+HUHn> zc|NMXpMn5B{}Ud>n{mlH8MtQ;*hOmksU8$IW0ek>*Bjthr8Z&3-HoRl-04KYTqUsJ zqQhHvuh~2J`6f}~5$XtpopR5azDC^3p*LDg@%NsyodOf1^3U|o;gBjM`{ot+T(5;e z#$#~8I?r*!!J$(R?kU$H?$vfuPr6+?%HMOT31z6r)xVo2MYG@cM(AU~slMg9q`+dpdMe~!OQE3o<+BsTk$)12~vSc=TisW;&zLtL_iwWI$Xr0E=ch?>js z=fYZ5I^kYLA<)0wy#)J<7&e2ri&u~Vk1&gr`2amWb{E~Xal~(##tR?wM3O7!`&qpsnx|W$K&1N7Sr2e;f~BODHphIv zj9!8MzhZ}F;7GS^hotGId}bv?wTo`TZW~j5#~sx2)nQJVp!1=hBthEP;ZmT}9KkSJq)tWp_`oi%81QjqR|Y{dq$Td7x2^waOF@~cVNWQOSqm_0`)Z^i(38V5gvoDKgLmnJag1$IWfBtt+s z*E&eYtmyZGaILnyAJE$}yg^UyP5T@l<}5L%LD<6znSedNVw1+* z5~h&o1dHc|%Rk{Qe5RvUIz`W7ozH(@Wqd(zwAol6va;#T(Fw)O&^@=FF(P|u!q-#5 z#C?NP$W}e$qO*wf?FNz>myi<1eSgKZ{`GP@9U@xJ7w~{N|C`K7nboZyeWUcWw`Wh) zhL@xMn)T+xc|2v;t)U=2Mb=fR&#eKBl6>!Xp0#UM-Pu+Jho1s&O)+{J7>R^CU@d}D z*|J~ZU)Dnu-ar)TEJ*R_z6Yo1y14!oGKTSw&kJ_qr*WsF=X=@2+m=7(E6h<=b zarj(5dmeOBj?{le_J5WIO~V?s&lvu>iVCUo{~4~9XL_Ye^DuMiI8V;g_034w&NF>a zDbV|_ANWxiT+hJ{zUC)FnfRI$SF~H%((hGtW69D>OW`Qe#{9S=fAi3q|GRFSdfFI> zuP27;>^PAr!n3)MjJ&&QT@Muf% zetMm=00V_iDFTEJ)E#%_-?Z)0aPay@9$W=x+?*KbDS!I_x7erljITXrj!?oZ3EPF! zO|A}b)>JQm50CwU+dG5Z6eunwlE~*kIafonWVTb|N7jffrV>S*eYXR=WhbulbJC|- zkLT&X!-fAOHN)mk=>06`7h01%X0rBHP`eew_gIQSpfSYSFHOu4g7d|^`C!QazyOwwN>>cH6E1wnyUuKNmPh_lf7Na1*~5sjn%7NY zd8XVPtJ_ROS!d6Ti4%_!(_ztO8L&x-t(hS|I_-(aHFzbkG-LuD5$rkx?6v>Xd{itV zla3m59VurnLv=@BotyYSJYg%C^Qm5(2jsQUrB0W9g?Fl(Zh;l*bHqWd(_kfFQ|2(UrUq13^H zeipVzxv; z4Vlu9NlDhAOUj1~)tIPonK%?5ZnuhYO!JzICRsQ?#_M&tDo>kl>B*jQ^)8}Sb4uob zIE#@d{A|8u73yK4JeVFD}gpo+MWfTpE{uel-tcy4*T>W!V1+RX3AjiHu(fgn^| z`urXs7D)4x&B%TQVc!L9PD5beM|4*sp6`=3Bd8N-*-E`jqCmRZNd7qLu&3~zj+{2DH3 zzJosnA1=N1xkuJ6#$&AmQqy~pM<&CxP%ttTo)m$2@ypX)awYRGynl11NRM#9XSL}Y zo>U&LuGQZL8t=Ah_t~JjyQ%Lx2b38k%|;I>Nt@}l&Y%LegA$PtM7O2rUU}TTdCi}V zZ+y8m8FK~(QMrhX%pm_jcC~o7CUHncre2xKABdD1n?qZWHyGEqfANW=v(~czr}O>) z%R}+-%xl)bqdewX3>$o6 z+B(&;uCf4zzXl}VXBOaGkm$HEhX?Xp*}u*fQypih52(yv)YV5{Fsbs8;Z#h5@f5Xb zVI-TxcxKc={2C}3G7Atvxt=DWU5OMxVT%-79{!f48S(^l@&nCV{eBMVa~9$p&ZGet zJ!!i?*2mJIid@cIn3SPUP6lvRL#TelEBE>>IEzM1{^NuH>!{A^tqKZVj#6{78(1k4 zd|#ytw_=Q|-TNzfK-BGnr(d{r95InD5m0B%!jI!aN431p0i@@#8MT5vv28-)8H-^p3joW7mft{EHe!Eh1#)&8weiFg2-*>|!mAi28YX zaZm_b4v$`&-ze^A?*_0R(DN0vPBV7+?4iAXP%O8oZNJ*{gd|J$ESE)p($JtPCCYD4 z*wcb~Ndi}!R76pYHn6Whr>wA!<7JLRmEz_@ z9p+C*AJeN$^ykAufU;d_34=f)B}GsXk@Ip4*g3e%22ZUmgepMoK(i{z#3_~MWOhBY z*dUa54Nn?Hw{q0($2F#RH`LbN)*ijg%ZJ-F+MOEkm;V30IYBp~D24ez>&Q!VSw!aw ziO-Y-A8c*sltD0o-Cqm(&>{hTST)t!BZL0NBULZPc;k3RkEMtxikxR zfmFZH5wvnb329QAl-5p8G%>ypLsfY1xe-u)XL#FQ*nl;`yXB@_;rYg~$`l%Sj^1ef zz6>(sSM<`DSgMl}cBiarV4`H&KN&Zm6(SNE_&tKEC02ZZ$RuhvGxl9IOBKeudkqFWN0b3`Oy_ zO4ZLB7$iqr{eu7MgdKSo*81EphA({QHnyj|NnRSKRKxXrGo#@jtyA`0(u(R&vn3Ch$JMoBISj# zx`6TFuOH9-aYQ-zER4)UxQ~u-%dhg_qoTJ=Si$A&=4qlj?cfMmX;;9P4tV}FT@TIY zCHTXOL-%)45$WsIQ_;5WU_smmbZ7_FxvU=H@`XMaLfls5&RzI|W6z{EjxW_}BkJR>SP-{xr>$b7xp#fK;mM-ui{1dk0fqPBLmBy}$6-oL0F_tk z8YhY!6z?5n+&Z$Wj8XoEeQZv_DCNTo9b+|dZETh( z+!ah2-x(rU9l@EM5UXIT&bjerwV;vIb9!QXzYFdA83J;rGr8Q6WpvE408H^wskX4# zwN~nb@QC8ph)~N`Q?>vWKg~^SccODLWG~jMI3#&}VKWsB%xhbJ9K0$qH}3RDo2?CC zY53bRAI$D^5XLZJtT4^8SByM=1{I`mX0o7%{Fo2T$V3SuxO?9O&hdJc*~`1z!jm_= z{LH?;BUQ4qX^)mU~TFyJ=}1T1+B&RA~aO zEaPuhvr#*5yxQn%d+2)5uUC_a4m+iY5PV9*&%u$trU@|xj%8j{p*(DAD`@uZqJ6?& zW)Z^_W~WCgJhqvVQ39f?5pGPxn)^=I3pnPx6>PD;7hgAuoD8q%XLtj-o1X&(&Sf-C z&3a-Lks2>X!egaVu911Y5dt1bez3LhhC`uxgiSPAk!?t0E1Lf7@3hFen3#qZ-B{ z-yO&ZwR&)k;(vbNlFuw8Lb^kOvTr^>*m}=E>d8#{(*BAaPagHKEdxg#N9Us-zGp2d z?XlKAjKdx)ZPF~oAFSvt*#Ontl4FzCEIWc!+wH;(wzZ%zhsSG(||NqTkiiwWTqpJvmze)-I+VJoAf z*sGL4cF&3DYZHldI{AKTo18=l}}={mg+Dr2IfXuK7Vaol#jKfKsLmFpVlO^YeG-7c?-SqtKhP4$M(}a9GsO?$Mq0aw(oE{0jhjO%$2F{nI3}$RWx%xHY)( zHI}*^dkBm?wP^9nrAcJM65$s}Y`+GK8H}Mm(NKjoLpCtAlo;hNPCggP*er^HaocE4 zvN)!Q#j}Wtq`ypd;c!U7%ewV`yiFqREy4#qG=E{Oh)sd%yFr|ufw4-q%!#MFcKARF zPzCneFAy61Q(hLIHg*&QNVT+&Pe%W8V|;~HQwx%%^Qvqm#}|+;0p+EECf#|~kegiy{Wpbx7pu}_8strJ62;m~bYdkEG>k^{6 z)IpZ5X*J+zW5_n{OG!mLWW+AvzXy=y1gbzOrtIV zUEs9op1oV>d0|l`TjU`=zb(VE6_~$QXx<5ZTbg=182Um(-_9*5I)OTp^2YzoLC+*Z zh{FQ#8?|zk!iV_Xc5u%*auvIYTJ{sUgX~Fc^%N#VjF~ob)h?U!+|{h5iCk$x+wzD~ z5`x}nhq!YE;W?nR(?mKfPDw=OTj$)b!3b<}7yI=~;#OkZL;V&rZ{le5S7@f~|8z2CnS!;MZ22#B4C*z|XZ={3Mw zKz;DTo7SV6BhhNo5Tn|!)lDrRck&+wQQ6>Sy0gbTJtpLzx93lQ0o7~zyIGFbP)WkY z(DxL|>C_4JUJ^;yV+Cent0rhevJTDt|MyqDyMh}ZW9B0RAf6W zsVOMgAimaAM~Q=YmOmyn@&b#x%o2r!;b+~2(W4}IP`{#V-lCV?I#f#F@scHJc1%&5 zdcr#8D`|Vr@0R+)R2NCHJjse!fz6M-#o6S&%ysg=D{_CsaOW zq;X%j$;+pY*ND6-TO*KUYzaB&!w%Qr@!WmQ0|E{C<2Jkh3 z@etq!)S=lX9$&tSar`>}JM9avG)75g_W)WY#h2K2AwA3764t~rR6U!L{V<Qu58(P_%PyTw>4Q@FV zNM@bAC{75IuuXggh@E&^}`p=-{;+^{2jVvg& zPT|*DyyBddSjG?03+*sebLvN{gNiD!Nrz7D`vdRN7vvt2ew(Wb9$ZjXGYAa&WhM6v zoIvKX(BVGEjOpT!`9vh5^CB1}Ei5!3B8KEu9o`5Bx2y%72FY}nO=I%lbIDpm^*#$S zmJz_Yi9EC1V&eCZz&k^VE@F}6cvLhAx&L)Rl$)*|;w4ciQHXy5Ths+ML0K}_a)Rhyyd&dPquSbJ2lS*u~k(PnRv_? z)7h@rsPxOzgP?gkQ|;#>j=Ach6KBwH`MmL9%$^G5$y$Xn-YLza`p0!751CWH?6+vl z1Z+kt{7Ycx$#6?9t`Q}xcO!>%p?9(%mboe}n~~EYIVon;|K0qM+-NpWD{$4NNe6nzImJt}oCF8h0VjF@es=V!gZhm|crG5Ehs;T(_7+qUGmV(H2~qJu zh1i44zg)X^_g=OwLNh`An( z*W)d7yC73psCX%&Sb9%S1ev zth@}%pH|Wgk~rW?zhTiwBO}fS7uvkJ5$fC;CnH<9O0!D2xx7jhcRlmY&M=&!Tm2wG zY8*rEL*IR7qb>-q$@U_Wt4AN^vsS{eUs5I$Yi;+1{q1opm9kzPBew5x5qjM*kAd+` z?#s4y6e@-c^Pv@~6i@#8U4>eufS$O4ox5%Sz*l^1Rt*6}qFNKPyP5Sl#;0g|A}^p= z*09EnwfEY{%sYvtn>v>#w;bU`Pgn}%u9z<)Cm^;gm7xC4;AN=VNZe?;)9dwip)y9l zIoe%$+!(NrGEzzsEO${07apUpUm?tU*f9`eE*a|4`CjKSE=1b-EIVjw%lE`bRmtX5 zy)~-3eBsK9;-xara23mT(j(R6hu83&?4v+lc>tA7B^T^|}ZCGz@+dVG&# zNbZMKKF8YlLN#D0#;Y77TVVnhk!Dkdq)C>S8z&`xRQ9}-IJ*XII_RnZ7-J>vs zBgY{>T8Fvpa2iFFfOF}HIu9;IT>m1XSi_=|vC1|0`|ID}1gA>UE>zyuOx2ni-z)n! zXboz&zUYR@a%V91Ew&?U&Ky4E`z!RZo`x>h^9SSgamjp%qQ0}7Gws-gI>4V#_x#+A z!)<4fsweF=5`yK2ax(*R*mT0Dx%ib92yxmfnz)*QWT2}7;HAdC{&xu3ce#3qB1tAD zE|q7eZE}vZDd%?adx`#42~|Y&=A@jMn)A08Skb;dF8-*s*`J3VUW^J={M8|QW3OM; z@PmXw=_{~ZeL&B+wsOS`g|$|N3F&g{Jud8Yyq(chRA0X&noG5yEvb)1$`&y3!+<7* ztBoTY0?EXwRv&v#WNSTCBLl>%$!BFbw4TDEza`pS^-<|(g%y-{CgKr_vXRC`opGv# z)e($?!SWqXjA+rZb+da#RU$gvq)K#wN+EI%uL`QQE@K@@wx$zkq(ryZVpZ5vPTx0c zqce?>;vO--hTZE+y5nl1l@L-H@)gN4A|Ty*iwg>>{hJmRDpYcYiW7Y%a@i!)O(nJ*r7Em1s zAJ!i99lT$Yce;9(SUW5}b+Pd`HXioN{#JjumSchq5$^Rfz|k_cpE;L(@@m_1O}niW zTDo`_Y>8@Csr`dH<3|7>(vBpBE`PH8jxdHvA?rGD2-*9dW;K(EsdV{q-r(y142dn> z9hs7NLmHP~|4g?FrbznKCFIg0KzUQgES7Mie{&8@1c79SKpW*Pugwfo-q5)72Tzix zbKj~8RoD!bQ}d`5Qe;0czmSLPIV~=XX>|;X9e;=%Me0@Wv7W4=y{N$Ywc9_c_k^SJ zeFJ<_!AVTm4)?{Bm+R1#baH0_8u@7`Gq4XMN+yTxjkpxT33K-8_72v+FE0z0B)gs{ zrt<_qWAU<2fNyufU+vqw*;bg0NsU>XpKxg6N)QW78AE^hE-BRg~6m!c%*?0wZGqN}j#CoV38F+u!jKH{U)O&H>CPbU4D zoh?y>5Y>L0Q+mXA8WdMDO|pxoz?3&?C7f(+iDc=KMA3U=>*vJ}Ye0Uys^u9>zY@V$ z3GwkS*>M9CnM~>8R_Lt0vyHKjX9_raGTPuhfs3$V1>=%4io3YJM3U%b!i|HMNg>eP zYFzd;>Dm2;jvjOmL}AOj;xq&K&e+zhOv3m+Sc)iROusZI?W(}@4YlC0s8}+_lLuBc zE5${iCTz5An4nYMgAf;eDI|n={4PW&zRod2*B)oZq%ZI#sgd2|ipkX9qzI`&JM8*2 zO!sg%tB@TBv>ERsmMv{zb|dAp>0@U>ORw=0xJIL(mcJA`!*5)@kw&V<-0Z+!q(jqE z`{WxktKD!;Cgkt~I-_v(y1FiDCm7xbb4>dT)keuvSXch^yo)3VDzlBS!TAsh2Hg@P zV}R{OyBi*pt63%~wW0EQ zY+|iWe%ne3aXCsXslp#S?p(j{cmj%Z=-^pH(=%drut%^a9?({U!bXJiG#0N>Luw~Q zC)(kEZX&v##9+la(y;Rb3?Hz^mU~ybv>XkYl9)96`~U79x$pdiWnO(1cM6LPBv+6z zE2I_2${%KqY`;d)R<*VhdEO^#g=i(t#abHVK@s7?mN%XxN%u9&GQ7lZNp`o5sm4&A6hQfJv^I_xKEZB z^U;d+BoBk}lzFSOIZG;P;IC=+-sQ*_Y1NwlDx3?Q#)!Snds%!=--6J+23<$eh-fUR zA|~}zK@&@+4A7IVj2Wqv>Rj}4fI&2Jv`SUI6xc#6ts^j7DUaIst^$pjhf^ zI_W`|`fPfx!`sCQB|d4O2%*^FCUH6V*mzX}5~TFig^)~MQ^<4)7k7`8TW!bUP@t2o z>aO4x4y| zF#mD%S?F*8l|tg!CI%&aX6bXZkx>F;vfT={Ei=?ZU7fL#7(sYgn9{p$dzygGHsk^; z^5$GQr=&BK5Hnfb#1vcPNs{CJl4^#5la!82S#%@X%h|Z=B_L&&a(G+&MN8tFayMX` z5N5G}0583VzowPwvuM_$9_}2k0#5d)8@d#t#AB?ki{>PvvKU`O%|qid;XZv{jbSty z%z$?hru1CWQ|5y^oe<=n>ecWQ3u^QU6fg~+*MWj%dK*ROEVIJ55kUJ=HDK?&1C6Je zcHz;K`xs_Sisiw22(T;j>*aJ+@8L(fKbjc&oYxp?eLztya`{L6mlakth1W^;Iik=h zdRy`8-%j9B2Zkh0Gp60V*Vv*bi1lb$oxiTiZkf*zjIg5E7|Ph?_^?|7`fkED)uWXQ z8PtFrZ6IshohmC@jB=;%2J}l^NXz<^?&CNn)ZHPlJNBu;4|flW`)y*4-6lw`(b3E< z?QJ04EWEybVn6<(u@Lp^WP9M#b82X_*Y^Og*O4~Ls(LiCDmbi-J6f6g&n;z`B^-In zW{yB*P6oMaAQX22x)`ND2nuq`lhI3~Uqq+7Q`H$_w%)B1!j>lXY=ece$u*seDJ70% zWwF;wyKt{Az~>Wj{7(nL@RRXf@~6o~m_gDHZYINv4*j3wR0wRtps(58_rHxQhb+*X z(vLt>Y6Y$#GF?f<^>ZWxXDO43f||k~xJiex9RmQ?0=l*PcBU1_?N{@4p?k>HGg)kR zuu8X|q&*5v_eIrsq%d+uHdw*=m+2P3s^ydL3gWpO!n*0svsikCe?VW2ryO%gLAkgu zGz$UIe#PGC!LWaa=l$ZVA6Yhl`j;Ghcj13|Gztu_o4|?zutE1P4`pA|QP=hLA@iTY zzrl2lOAQ;jDdeE-LG{hyZxQJ2KOMLRPhSxw#m<16VR0G}oaq|NmN9M!nx#o0fm2*Bp{Ein ziV%5_^!+3VU74UvRf{N-5Fhdtk6x8R`K# zh4XRjLWvD}wn8duS9ZxQMTa2j*rQ`(FdE7guFd8JSasi>n^R_w!+TNf#>+Db*o?ZP zP|(#8Z0@}YI>OZAd=vU>rDaZA9s#&H90t#BtJph+PHrfFND!QLY8GX@q?JyhpwTn> z$g}u{byt(z^yv6`IEFpL-h|8S`PG3oS)p%hDKBc0IMSh(u%pZ6IZOd;=c<;5p|X(T zZ<1n8Wbd2putVV0+t176H1VK9K3G@Vj)eysTMbF*_(d@3G!ik`mk#s?FcaTsQ9c^n zY~h`I);o{ku>^zdjJX}n2~(~;guLl}-SlRoq>{VIgXx93et9{jGxDAlL08h@0z)lS zS$BIV2aj6+c8#k0rfoS{p8nzGBX+5b5IJCT#b2ipma$k<=W)mf=0q2=uZskEv?)il z{)i#b{HqhS0Pp1B*+8FMLf@`u>ZQb^r=|d&0HXT%m?tjKzb$alY2SKdGylX$YiaSU`4~3PFBBFUShR&>D!etI@{6cj z7c#9?5q}IDljZa^H%EpI=Y@?69KDm4aQchnzp&&P0H(RLao-5US5Q`z2dYsEJ;8s1 zaOz8#5a;L+9?9w#Efi538nC{f=wwFYXNGYZi2oUc;m2Anr}QzXXPsZTW2kz$;=)N4 z=Xuj$i7)wpJ@F)$tQ%;Uc+#Be8XG8l=8JB5bwHHjqGg;AL>wf6)^vvY&( zaj66%ijdJ*aV60J$`wn}Pde}Z#$Qe(O9*L?ZE&nOyiVuYEVdzTOUk9oh%;5s@@-#+XcAy^mF$7m0U?{oFK6lP zi0Wo3&Fur8_vi32O@XSK>ombUcx`ui`A<1Sr%7rMOr|t)TdcZkzk~+jA&X0ZHbbEA zeb1rJeGr;GB#DP+J&-U#rdOHoBq!U1Kb20rp zT_-}h4h}&9gJ{+6J4^2P7!1okMZ%KwhfOr+K}P#bN903MboraLn}Fn>k9>j9Cv89U zvSz_(Yp`|}NU&V>XK;sC?|Cjpc3DXmKfK!x*q<(hcq_ugqg|F%4T|fxMGC_qS8zx3 zJ6~(piX=GUVcy1J)rFaXfP{=L*E=wISVEr%l$Sn9YSN=XmeXcqlN>exwzC<=NBHBM z7ROZOiM`J{J?GNtb24Uru7C>KT`mI zC3i>>b87cRrRV4E_V#MMwLxo|(S{Y*b7-YDnc$YGxvQ=8WH*`LPD28@mb2IcMeX(( z=^%lMMlA^gL)c=bQa9#+xzv(7C2q7qx~Z@#?qwxobP}VOcv4I{vBujSmEYD@7+ihd zE0YL(I_92Ia6TVVBAP5i-ApWC+hd|%8!wgEyzAj;P=rQSChJxm|MiJ!114yY+i+=c z;k<^PMDl8bZu=gAzh$$hS1(m^)p(X0*NRj&q6UwceV1-f&`SIb?)0(aK_MFUmx_7G z2l1#B_Zm62DRF_51Ul|oFrK@B#}5&u*JB}jVcc)o(u3nP1CEcdr+asaCNBbF|Xo1IqN!pUkwfNn-8uS#=5O%h|bdeFCJy`^gio}^W zdFTMF0Jh^=gNN5J%tRfa+u*qz_6=jLUVf_h{K@7KAARu66~#k zQme68xh86L5P}Hg2pL~#jXpTO4(GomG(T9pVwOL-2Gb`96u#4C~{3^e04)&jy%TPFxxzX5NGpIO;K}st4clnoj zRKkzq6+4-fF_^M~s~T9w9)gmVPeZtuXq6$)RXX~5&G#NA4P=X4{6V1}+!<#VE4+9o zDBQyOSbU@M#$k`hf<)iOnSb+Ferx%op7wPfu9by&BG_V=>2XfUf`o1nb6~W>&}5Mn zdtY7Ae9=SOUa>xgkMIRD`JX$w6Ji;@E_UvMmY@s$7ThJbVo;lk#tU2DaFxL)T^N#3 zUQt?pIAl}L`~?dv8UCi$$~*J&#UTvA5_VUCNH%9V@>k!3VJ(6!jYnaR|$H z+vRl51MxKty6c|@Z5o<5t&Yhk)w9RMZjd7SQalB#=B{WHe868V&w_LzcSG~Vjx%Gb zD)O{=2^RH8P*$Ed?I;_@@R`oy+}kjnzGTu-d=NP;?=rFw|9x6OYb&+X)XPRc-Jmg; z(#AvP=Aj`JS`2Nz8SrGT0NM^ATSX@~Nw0Kgr#$3=hL!$_6f97fM!vT>U`Y9y%G8r( zl$eCg&$Rb<_3({i4rRi$^y0{-*8W`c$ocK}cbIA_Q}v-)vVmY%fe~+YpGkf*wz)Zw zd~vF@Jftde3K_ju-332d>T9s%P^_78G0UfX^>Sv8hQ}bLaBeytZ?~@a^C2 z#FvSut*A{c51kDED<0zPx_Y&Ipe^!)v?la^*1av_1%fGu7@efP5NN>RX%|=p*2I%v zf;HY6=lI47-=7!T{dv=#QUty{A-hnd!9aIIh>%mT3z6S+X z75g%QF-?{@B;19o)|xM#sG(E7EVS~@f7 zNfZhuqI*fPv;V8Je*Ue#wEz%HP^jltm)yLfNQWsx$$*!0s{hl{B<^@E%1iyO2g66t zG;l2pt*Gr#qm`Ew)vY6Q0y)IU$a;xJrvQ$u_3e)q*cGQOnf8POSi4|2vok6TUq_*B zEtH91gIr%93Cl1m>1u+tyNDJ1T$3u& zWjPmp5qWp;B&=liuf>3Fo!xLpnmi%*pO8hvMG2?|MfnKg=S|Knj!?T_9mu5i**h)I zfxDw}8?T|wam~Qqcus}Bsi{pPdc^{$eEv~AHldGNqJfyICevOA1*s@4=&~_in~@0a z&C0e!KIMV8+kIn6AFW9$fpzRm%^Ey5Z8oYBpI6$kjzpA@E`<32j9reEi!s;&qYIPc4bppR84&_JTdCBxC9jqH$c^zdN3Ber#prpZzO6yH ziwDjTgUm-c+sZkar1DS?V9^1iC-&mA{~rK3K*qmZ0P@fQ!)O`)atgqmIQ$=p?&s1r zEGgb|ZMITKZ4ZOFRwZO8C;|B;j|a)}t+vIZ_z!<)4$;za-tpG$O()tcZkG_)Us2Y5 zwVfy}tuD0Jtnquq)5Vy_2`zF-ZQ66R$wbXa2U_r{E>BYMh|>WoHn{l)G;grN9r7(I zz)P-*%}VE<&`Fxvz-$T6;Qy!K7TxrSsRvf4VZr( zCJH^A@H*76$(vpLT#V)?r;wApRA;lwSk%qm7(qK@@Lh=Zl=Y9GT}4(9c|1Z!Y8YV@ zA?3vvj952v5(frXD1oXplC}zQUx_zqN|}{T+>Ro~YToV#9n1=Db2s11DnC(u!xiNk4p2-<-tJqn3sf8I`e(Szl!irGb+VUQbO;ksRxkjac43s zCVKX|1N{rr?~?T)5U)o8mAJlA6jol8V$*Je{X7cyxtfbU$dC@`7Nd<#CmgXLQR5tA zBNXZaadS~HJ5}g?QlKq1-4vY{9L5?y^)m;p+;vVO^~qT-eV&IX7wTJQ;{!{@XAYZm zgv~(Di|c_)Kh)?4zG{mwn`G!flA1buW8A@BajxP4)Rp_EF15Vb43F?eDfK46$ZGl1pgdar;SvNNn~|!V9dEVLY?cK-J^LG_}r?J>57r##^=3_Ngf0_LqS<<;B&25i) z%%KLFr`?|TA|l1rN~Dic-eBeEau{U`0g>!gus@p&BqVui2* zdKFf~J%Y)w)K;BuIRy3SznCu!8kkaA3BBj*WAG$G7-C! z*b!4T{dANxh-YF022XQ07_eA7+x zG7dD((5MB!Q-N)%yj-_xMd3r0@f#QoD2?`nuo1`4HBZ45aJvo4X{ASjvh&%)#TAm* zzQjmpN@~d3gz5c>YEkK)wY5z4WU9+LK6MX#EpBE7+N)B0)k_7RmXxO9&IB`#L~ zX3)M2+T>KdqH^L+L4z+#X}&b+cXyxUUAnLF2u7?;tAY|PV9qUx> zmoXN0HgK1=I#t!8^Or!F=jP1n(~tN3hM(}*U$|7u;@MVA%J0^!@Txj|>b%}Q(3 zXmbxHY2EPvCFb3amq0V#d`0Hyp<>pJ*kCDYC(cB$$_u1JbYJh#-eo>}9om zi4B<$3bq9NiwwWSFa^4;&EDXBTF@!W{J$aG0=j1Dn#YJITM{t3%9Dy7G8coTJ9TZ~ zg+PiC3b5CF2i$WCO(e*H_Zg*fh#hK6HDUC1#EXCm%|R>_>&;d}QS3KT4}~${#DpHt%??dnZ{$9j-UrB;?_DW?L|iuN2nCUwx?B%WqnoWFx(8&$~_sJ*}b zU;2}pCVOd9hiE>Y<-E_lBml(!qZ@-aB4g&kzaVpu042h=$tr)(Xw$>(B~SxFl!^lK zNZ2CEBq1v57YiNMtVswmIU;P>E2KDOKya$Qbd#6XWhdlZK{3`^X%1wMwXi&4q)9GW zS^LuK8oD6n!xEKig2pTVT&j1IB6EMnDDTog-&mYv0;c>%&^T`)11FbA03fE$^Y7|l zZIQ`+>%ZxUmo>EtCsgvalwXsZd=jo2N?|&2QNyAuTofj?!SbtC6jig;Htqs?y?Ljh z#zC`AfShA{Rc)QB8|P{gp6+INXDUCTsUxGD<8Thp0cvV& zM<>!l$8lXv@9EpDL(D9z4EbO$aLrILgl*w{s=@r_&l;(lVACl!{`Z@bnh$TfUZR=$ zd+}kp`@E>W_A+GAr6rnACNvO|y%99@_4bTOx26bF1zK zW@VF>be&0rb0D+g zwvG+aIS_N~)!|`Tkx#{@r7BaAL$H1kA;B)T9a$d~f66aV8W%5dc>>S7OU(30 zBgJpiw*BG7hJ2aEe8))r{kgTMyk-JQlAPX}WkzVb|DOjR(Ms2$DvPw%ZElKA1Rflf z>N?W0kPHkBc>q>^Bo%cQgf|;SgkswS>*WXEeKb zqL^`h@>TWSzUD}BthZgJu`AH!k;_xF@br*NfBzRP(AGrF1#;Z&C4~tJC_1ory#&?` z*CssuhT3wNAMi^{yun8>5XJMdvWp|uL|*=tAZm2%hOdvUZVyrqH;sqgqC(T*R-2UC zVc2*0`X6gH?8!^s*X*+NCAtk%q?8>bSdJPs=1)8!v`@_n#omTfa+6?v#yRTQSTO(C z?{A|tpHOqI&^A)%wgl-k0rlfyG1Km^m}LqseF+9W?qg70Ul#*80%#HX>M>xw*t#6I zsaRQ=m7R(jZG?1-ADXc!!YmC}(9=0mngD-t4olI=uW%PW*EjNaIM?{#Y2ucV3jWzWY9tVk~)>uX}2>umg5Co zw`VBLh;W+Z`uf&gO?*E#1jPHgqBH`=~JCnipr3kQjaE zsgA(Bi3>D(Kh7CW0!o`ckO0Sr#FR*`ZRhM=Y8udb1sEyM*goA%1aGAr{RAIMi#hZJ zFTVJo+I3E0LLu1Jog0&*(RTE#i3n+(L=7=?Ip<}y8J?A;^eGYE!KY(GI0cxD@O$>X z=(3=xNk@t}m3Ee7k;D-I`?ql^V`AS+z`%jMKxCa83I)DJ1ujmA&&=3B1$TYE#sKwq z&jA`kK^{1dME)xk%ru1I9yKwg!X{X344aOwQsRe4C?p;Iq90lPY7%&Jb&H#hfWTZR zy$ttaU8cxqMZ+~E^PT^=!!~gE(sc1=BqKk>=N(^GWA|g2l=O-qu4`)Ba31yM0KzkDd)EIQjxMODLp0 zNPpTQ>N(JCqRO#XpcT{hb*J0cD#L`%=BZ!!7>9v0H?LGwROwu=?9)#8F&D+i{|peUp#;z&bMbFGT(W~A$XTN*z5%Gj0iG@IFBIH0 zz!u#Bk|qd=9N=ow?)Vrr`E6mod*v-;?TSiz3L0&xT|#0G0>rdUEj+U^@Bozl>)hnU{SP_ z!=f|gP}=}P^R#1yJO2`U_BCs<;!RziZOm@rk=CuE$rokYvBvaPEtYre{k_36KNYMK zF^>=aHU+0U9_zBg26Tlj_$@WFE>zW=9SxR>a^PE=frF21v?V(aNcBQ)>XQfamvr0w zcAPgs2mrnyFLPfpo037WIB?!3V z=fZb~+n_F(FlT;AZDO@sWIU#2Te?48h$TPStP-mxFAfCvXJk%CcFNMcfiL4NXye5r zRd%;xe#Iz!-DM?;3bsy#_VQSJTAS~T7dOH1(B8F(miUV5r^+NYT6>k2i&jrs?4Oz^ zQ15MuEZeGM3)+%D1Berk5EyNf$Wal%fDwMJJ_Jo_(oE9PPw6M{GZQAyZ)9Q}r=}m&}Ze;vytMqs_m0Y>$x$;l=vRIGg2h zL+`Cwv?|dUX$7Etmk~hovrrS=@V$TIg!HVoOw2&n*!JGg8aQr6w={ zjgM?L3TI|bEguTyM+J>+up?0L&#V(u0bNbB6^>%SYH-?^mR}7As*B02iNo9S(g;-C z;WaCdeRStAQ8{T}Z}OTEUFb0`RAld*tdnXq$QFr%fd0IFI})}Lar)O@eqR(Ik?<53 zo&r!$8OZ*sdOD4L$Hk>gRmw&WnaVGZcHim>)s1duIE=5oKN+TRmOvzR<=LQ%WW@c< z{F7Jh#;57>nzx}2rv@vsata9A@&}~~(PExK$&B{A5K-Our60ZvNT)z}YJbNbDam$7 z?I~E_J-Lr*k3@sP%tqN6?FRe4yHAb~hw2A%oXT3D&m}?${n`vjLP|lMAOlgqT{O$* z#)yOw=LSj|noUU|YVv1sbVc^~N`r)OD%zsBRPAb(IjdI}zG?TzLynkT7m&D1yS-JGF7pGnLRbL*3({xtCrVLZB%*UQ? z2)Ap7?js2Qg~SrXQd29);hJNuqG)K<~ zL(5H6Jh-fgNh@wNoVJ5i57m5g371vW;;}3?OzI1YArZI&=5V3nfFAKs@v3QkkGGLC z=bT)D8$ffJbg7<9V9S$~`pBIi+L>P58Nz7gog1KMhAuOZk!s_M5{91}!y@egG#vKz z)aPXHh0fV)GDNB9TZ)R|#5-@!c`<2wf1AZPs9hRwU)CG$_Odpe4{1&s-Dwt@-Py0E zG3zNS@&rlCN5aS?K0Z;n`NCR&uteNplU@9eQgzDP@i0ZP>zLX6Vw}aoO;2I*&MH#- zq6a-eDFYKghc$odF2?2e^k+L%QH3+FozATdLoK6;Y@zotInBqfwFeP~ex&B&{7ijV zol$&a-LFQ-4@B1l{@o(w6)nUlHQ-|k{nbeEtyU#Y;g$*Jn{Pa{GdM?_Ji+Auw>t)p zbPZ8;h_cipYRvfPpJmS_3rQ@#Yae2h9CCJnKm?jOZk4j4AowfeU*()Bwjxb@-r z+fH|rP2cgoS}2qN;$rtL8T($vG>tQdG>&T#x_Vx9?U)JvdpezMX5Fw7zL#nH6taig z$Ey36yFgqZI_Ul|P+V5@*<)u9*U?@f--FYc5(N@(SNx*|Ziy$D5hkr+S#2Zhep|Fr zfda1>wxUTW`;#1$vzg|wp8w&D3L^$qG=-YWiGiUdGv2S)`+R- z@fe}Y8H|9&7Cu~=!89|_C=gxy_srZqF+p|9n6Ae#;k<-o#~6G@TjAR8pU^i(^LI?a zAi*rkLf#F%)&axOK#$D{^)RuAs8q&u^FB0TAOc5Wm}HWXJ?2jYooDX)uD0gqqmMWL zvv z?o3Km^Jbx-C%*wniRyPxH6rw5CyGc^VW#y=Ii0Z7u`L6&G$gqJ7E}QT-w3*l6D8y> zO~HXPT&6u~pKSb%Dl9w>qHNqC`E`w+GrEs7*xFtlK=WH5w?NNZK#E8ez`9v z*+Ao{eRn02Y4;Z-Xj}N;acQrDC6jvm5Q2~MG015uCXrR^TGJ>g+Zy0S6=z&X(RW7< z@mbjuuQ)cpd(J>-!TZ=EwxWJAnt<^@x~1)~lq-b%>pa))J%*hF7++Y6!rmK+6-#;5 z4T&LaTkU()>r5}jlwH(2U0kT9`gS_MqVSu{_um#e%Ap@ZsSPWaW+a3Tfux}bhHR{0 zdP6Wm0^pjDPp6YG2aJi26~e1qb&99k;n~qlBQjlblN2^yh@AS_Z_l1JC;6lgDUjf* zb+VMd2T;PbWyqVma;WjtImbv`&$=+T^NAAr?G8e-Oq9#I%p>rEpo^*a#NN3e;B`N! zr#j_t3%>3@oK*=-t$mi{B0mV&JWMr`elW}^QEsAMOBwOWep=?hp@XTPXvyDD@I$b3 zBB9(m>*Q+!?vd%$0ArCjFq@VDzWQIM-iippdpL4;7(}JhH^{IxW^&S5nGukURqNHy$1#4)LhBDi`@M9YHH(sc~{|sk(Cq5R38oO6u^|AYL zUA(#}L!i+fW(|mA*88R0TA|=EgR()X=1)~g_MNvddA7}js&>?56>5Jk!daI&Jm)ah z*0QHrSv}7v00rG0SRH<29~h&^Buq`qB~q!pa@2Zf9E!35cA1O4t9ZfFg~s=WC1CZa z-}%{pFDX`}`o92_#{|L}rB7Vjf@hM0IXw5n&%MWqaI@y8HJmWM-1(HItr4sCM|zqe zq(+=_9rYZ6-z3-&V{jlDi;CtpaF^GGXD=q}krV^(oNd#hy*jLtb`x*csJrhl7{(ow zb0B8*E3sAyA$97^S*bfatC-axR!c$nDHeonQ^qEa=C2ZHA>!%IPF)Ki0&n(uOgbtS z)|`f6u1}Y^A&QDC+#evO{i1Z;pKYq8Y$&a<-Z9ZSKaN-kfTkY!vDe3a@bst%y!RkQ zt3tpNcB3L+R>B+Xm4mBhVMzYO&tNoO?YtvKUrRe-`ztFl!i1K(y_k@Bjcwy;)(Z`V zu=2(!5hIzY$JaT$Rk;%-_OreA)I48+WSyUXamrrB4eold=0Ag3Zt!FP#=At^sUbX~ zEr3Fm1!O}3q374iIYf-LMbn_K^zta!BExB!*Lu!KY1oCXr!simYn3wi&dpI&%Lqtm z`GC*p2+Zuo67J(Lo=VQ99^&4&#Rt4T%!05eR@HG3QQjoeTR!!DDP5&|%P`3EzI#o2yk<_`~qq)IAF=elW`(n+}91t_ zUuKL}f^*kTkGcng=)!8yqEHaR7DyAtaeapr*okf;c=Vyy+gOkZP!)O)-2#v@EGk%A z{c$sTho4zwt@2DKKPf1@cH?wAIn=qXtE9{w-5!E5_R>dlg`}orm=$pz?vtb1|kAEJ@`|;$FGi* z8X$^r8-c296=jp69mk2wOpdr3>D8&xcrOf~3#sNbx*#3yj3YF5?KMnzhLdE+J_I*~$^(EM!1S zk`0uOCfuy+$7l9t`9%aR@%NmW8Z?rXJGY9lp1{QhM%=gR zep~F;oV;O@UW*TL+ZOh}Ih9WnRW8M@Cht4{{D;#XlLnk9+uK5GOc-z3S9fu$Myk@t_!xx}P?w!AC#}!Uf_*B3X5D6_& ze*(fVz@=TO8Zi0Hh&~rbm~20{%n4}f;Rc+dJ`Z*f2}6ys+5PcekUZWJHGgDfoiv`b z%QWcZvsZ#;X3^+u8Y_eVT-86CTvGu>*6W_gqVa~mry_7#@yaNCL(d^dANfEd1;zP3 zM8|c9NxTDAxQtXiK7w0DyF&zrV%_6|7NISR7h+Pb0X}tDLInhP`sN_(^GzausJYyG zX5;Ku6I0VBCPAQOp|iO?KrRB6P)Y5(5``13*2yV@Z8@)16^o5Tpg_-%B?VtBECOG* znCCAY3)0Jc>tPIukz2G3sdCjPYum6t@hPu&k-;)#ppj| zS}LRj;ZEnC=?3D|t_K$e(uflPvnpWwfwBfI0NDq(4axT%??Sw!)(t(SozquW=N)7$>Hxe(M9dBAKF&U z^~3zw6B~~vDGc06G;bhQ=+01(&$8EnA-_(@$yi9tP{`TEl|I6OR+#qM1qP+XvV-Uc07thrfjm0hmPW<(*TRDIlMP#E{cYSb{r2;M8g(XWMv zCdY7~Qw&$hYcL`7K+&?0R7Ou!?WK#5(WPdPPqdkyk(+lyRLZgVs@aK&eJC?K>jxHW zl4j)Md_KQAI?F&V=Rq%RifRBJ$bt6-JysY9;+=GiDGb>%U>%DRpWHy>6YHcq3IY{5 z;YKW5?ER#6wNn1yaod^`C~_GENyz!Bm7E+uR3_7=)4FL(L!}=f;VqzD!5GP0>H8x> zo!=UJ`*v$u@Zlz;Ven;19;wf(G$&uGoO9wPdbCS%kU-py*=z36IjOsP;_72IP%&xB zS*eHY31vNV`^WZT+8=MfTA-?WwXUy&NX;GaD?9Z}r}C6a{)ncFYWSM2|7H>R&>+Z) zW|7b^I-MAf|L&c4Oof!)1WZS=fU2Pkv@bIex9eEI$GA>^oaWUFhfWIj**!=G7dTjt z-~+xJNtXmjEYS%EWs0juW-$Bn&Th~_sLykhbcxzmEGMuMr?NT~k^pq-0ayB?L@_X+ zU$7Nkf45q6%F|x())E5TUW8dcO{Gad)BX|2m6P9AONMmjDcN?nveV}IM7mi?ppmc> zQ5QH^G*Us8ld$py+}?~F`p{MVJND^rGkymYz@Kql`{D$JxGLAfxM^7449kfkh8^+y z1s$P-Dd&+x?9iFEGkTvsghB+qF@&wpHb*`bezP4G5Z?>S);#4B8(f~ng~jrb*uaL8 zB7JZV9irF*5dPcfd(@c}%r~Ym;GHw8qI0^W=Gcxf$(FTw#`|Ji%4c8OOEc=S=Y{|d z#fAi?XN5^AkKL4(>E|pAc}6x5kW&J2|AnuJL{Py5_RvEkvXUQj01x`{Wah6o4yiYJ za&+6kM5976VEV+EMVNV~;4)6JQVLy&YgU!>Ffw}D=@Za#S@_3>VhS*NcNe_!ND&-` zy$D@t?XTg;vB6e+#D;BCp(x^tDVkpuwH5(vbfTPbim=ly$ODitGT5 zl}cjD{obM5@JZshX~YHALv9~Lm-KfQ*k^MyjcsN&Nr75`u5|B;f&Ev)+5+X;Zc`5$ z3+wjcwR@lI$%76>WM$*^TR~r}_W~Y!S011@_J3l|;&=GzNvT8>{YKdagO@NetnlYj zOE9@TtkUC(QU zC_;M4F;)qse+sd1J{z%&D1Lj1BIw;@2}SwXBol`M%OZVjBjI0)OH|s@OE(TM`qOG2 z6wAZJE7eT7;Pk`C;g;UvW6i;NozkuI{Ov<~^ox4_@&444w3ePxjzBn*-K7A+Ar{zppag_Rb_Nf@&G~5zmt2Q2AlSN(X#oUYXqz1V1 za!r&C7SQCMuujFg-1DZ$V7{Zk|9k-YT}_rU%n9%7x*$Q}^$)ccL72_LYKxfk{Evw; zTVo%*i(@TjZ{GlynX(Vi&CN5`m7%`O^i&lI8QjynIk zc)w2JE$Ro9+A5MNU=ktDHEGx^I!g>t{z;pWjbe=<4x3wZ)~tR70y~3&1mW$ zsyUdy_`(EHZIga5jHD-d%8hoX+!lVkkZ1KsG3`zlf&H`FtjlW=McbWM$PVXL4R=7A zI-Q%(fxihJEP&kk~P;esa5JC?=uT%+HeEArIiYS;|S+upXT(?c3d zn)1$o;c+nP5|GAl+?%O#TFIp0+{;>swQG6WR+dMxmNj?_k0$x6ad8coj#Wa!t;$#y zU8&JwM5*B`{#2Edt3hk1MEz8qlWV^mD4lV}n}gc6SW~2ZhbGv{ zu*OK~{ADlW6k=NElV!vic*;Rb(jo(RnB10Aif9``I~7pjl*m;JYYrX!ABS@#LBzRE zrX)i6x8Jjb0=xt=i$E^PW7RxoE2DS+s#0iM7PwpUdD`sW)$Ke|FfhQ>;ylnBtJqvi z03fz&=D{O(OcT66`4+0V&$t;^xu0Wj5Gvf_{e#hV2tP#EDnmGF`&lDXf4$yEE?=UX zbfyBO%C2ad{Z7PXI6l1Jm;tWWIwr9jpRSgq+;h;H#mB%S{oA(O9Xk5)FLnp z^Vhf~?=u%j_;qKt=|V9;*QX&qPLYDvZ?}HBiwmmw^{e&lpJYm{v@0L=*zC5YbZU1a zT$+C%pbCpkn3Gs+9Dtae$D$FU-v$qE5B8%#)|N&X7_7T8m!9uwZx%ivHe|sk>(%y0 zknes%=Ho>PTluFP1IvcYkiO4K*E(x~-oFN6KN=dBj$_SJsVaF>Tn3BinS`(6_-V|N zD=zpIMn7n|*k&!O{IK)mhsc|`eW7qfk^$s`4F`?>!UU zr0`0IZ~XgI@tR@oPGMedMkbQdh1|o9 zdbx2#D^q-WNkDsC{4LzK?cTj=6$$*udFt9q?3RmqH@B;UV;i>3_C8x0dPGF1i04%@ zu?n9dKyXS-6tp03hwPkrC1E=aq1mroTSLDLRqu>^D5xif zDjg12`V`SiW!9*m?nnkqn-;Xgr1>p!o#epfVX{E%(pe6Ex4{KPqp)E=`8iXK+Tb60 z!&&?yQY5g%>6P7L%T$TAzc{|Z9w+Jm9r-+|V0T+ofye=pUZWO?F*_S4C8Mtjq{1+& zSX5*9Hhfw5Jf@x!!V_>Sm+f$Vy)UM8uzDk_R*Y1da^@sbZ%Ba|_UNM-2NgMZGL(SX@YY76;)g#PFgxkcLy?#V@Rro)IJ)%NEphgeuW@wg zC&z-8nsX4y5U?09p93SJs*6(Y(Gc93TTUmIxY~_{ZtXP@HlEj`=+)>jV`GQ#%gI(a zTTXl~TWli#oP@H?8gT+@xuunlqw*SIAr$#>&*v@4$nH%T07>qlvF2Os3I;@f!IO#! z8*o#y#{rW{dK_Au%Q0&%+>}6TQL`oHq9lm9*DQ18g6!iLVYFrl?ZX+$wq*^7CK$Mp z!OVosA*R}G7YTl0UEu|3z8ze|4}|hecPp5=Tp;S1d@$mUZ`44wG7|l;FqEVdy-Yk! zn=eXb8uCUlod-TAdT4)6a@nvtFzd%=Fw|z6;Y3>B>kjKA<#aoFl7|r zE~@BlMVr=CB|9V&(d~^+2Ofg3X`ri@h3Rvd2b5T>NuBnet8Y@389E*koV=!XLQSYh zEU5c?A{V&P^7-E*h&>a}bzlZ#d*sv|noahy8hHG@>{VCQQPsf`D%SU18i6RM zpz?tqyE<^5yx?fvt?>4tRO6^DSD)jq|) zm26VKQ?~9RWhT7qGI+odUMu4|CH>JMdPqi}|29;o+$kV%gZVp0qi){1@&h)1TjOtEZlSQWlX*Y8GdI`=cVD#GN zksfO0bFPEyPq2}~;eo;n%@cqqUu3tvQ*v4B3YkvRQOhi5G6I560CIxzdwqfcsz^lW{s+Ef( zXXNM-+?6gwhwj)pNY^He+FNuG2loB7VNcgKIC8+4A-0IiLL2pk_DgYV{;x znoBD;Ds2EjyO!w+LNI+|P~76g%^{Yv_eUu^7{+ax{jJ)<@#1kK>%GUh5%}Kpq|>w@ zDcjP-@Y#ZcjLon>^PVsp5$@)ESAPjCFq;$;+AO8UP{PmyvXUU* z>Y>9OuaeL~k@;7Ir?J^2Ivf9C*22uX%lMBN?;we}%X+VlG)_F3&R#Abhq>-5tEYCb z(=~oNHDZ&9nxl((Lx}#P9Y=Q2{Zi+?kBsWnKue1PtsnDOC6^QBOdIOkF^m_=_=pBE zjJ*JFyTP|8DQ`dsS&yV&KvFfw*_vyBkNG8Px(*!$kfuVOD8w+1JstX;LSFDV9zPXs z{nDKo{UL7)5bMxl7bPFH`B%+DD$wt>$6QV9Lf(KJ@`>2Ct%#{jsvpx)_9|#c{|Qs} zyedJ%>B!<+#AMyECjXx!(j7FOM)zP6&E45M**V-~P+`slCX#Cpnp@=Yi6k0`2{3sG zw$+d9igpcwF|*xU>d;~}Ti~-50Y~~z2rKWkic<%P-9^?kK~T*;qH6C}n!tJ&GXsvJ z95%;jr8DPu@a1vHy!Glj9Db8=s z+Qu0+?*!{{#+nrJ#EqH2gj9f&(%~(e{1%*E0eq`=aRxK-;5l{$T%P~o| z;8j8>hT|$fMgBCzA!}nueJy}p^DLhnP~Z@>Dx*<}CTczRLawY9UWkYKg8>X{>4f*HL zVVOdMdl0TISH{X@I@4((!DDq&U<1Xb%SHwoimJ8>B4FU&Zq$v|p!_O5@;-o4snpuUl?@8=tV|=f>C)BuXzm;@cxJo!jJjrOK+l~= z_XhPaY4z4>X-H5lSIsd=mBa#0F-(AY3Zl+LU>Vu%EgQ{p3nuaH!_G9_3O|FvVG?@x zh1G!4KI1am=Xa8cS{H20Uj(frROl-<0$iKrcbgy|>anaf1yZ*dh~lD?)(?i6HA1pG z1mNA7&|0LL2x}#b(2QrMxR*Zbhjtsw>>#72gPyhGIFbjov!&AEM96|J%>qu=9&(Wpr<`Y?sR>^{ki= z_oKJr^GbWffAAJ5==BVjsD|K{rdXGvU#32DDKYG=QGB_|Q9nPWtk-2+=|ky}AVS|3 zTokw&wE$x#2UK}*xsScF&*zVQJtTS#+|A%r!y55XACE*_StE(D)~M5$Sy|UYZL)~M zA~4B7>1#tBVBtmRyAlbHEIRCu8}$>!y~4HH9^eHf3TDxOaV4F6hIsL9SCev5p~)(yG*M8OY36^x=J|@ybgU9xv~n-sI&lWB2h5_GdTx2h z9wp>87oRy}^T-IWug!duq{NbjskOg}f&Q)pPWZK%GHEhRb-@Hy{f}{=z~9Bo3BS+} zN6zlTaNXNd_(TasSBPX7?-82V8kATXooDh%mYu-zrdgO{Gq$lT!vyg{s152EO2eJ? zk~j#}IR6$Ffr7eNKqN$H*QR2ugXiR<@TA0=Q!V=&aDqW!ml>c^`mF@{f0zvkPQ}p@ zb7a{5^2WYn<*|BfU9UhM(5d-HobJco71_&_uH{zR^hElPJAAT_2${;Pq@xwNBWSai zZQ(L{n?<{sMK|V^HQ(;5vb;y%CR^?l0TGHbx6A0bPqrEvAU@C5V&D;)N!*-6o*{y0 zbw#o?SawBte8UvqbMm>YTP7A(D(HUWn1z(s%GJg6B*Mbot3KY?*-`P=lC?qq=yh`; zqK}LNmNs>&7*9FXoPr28{yCObQK{8@U;qbS2Z9$<<@ag`e1nz5!{`!Zt;Rg243| z`+p@a;-pWwi#5IYilU?<%#Rrsi)t&Uy|y1UA&uj_%kyMfR1GzrePI5v5Luy^8c7vo z#vOE*C**S?9Y;V1+dIzx?%BsmvS!z5bzqw-r_wp@dpH#Mde4Qpg>EZLOW3mNL`DUM z8>rf(BbPDjIodIhyGCO{dDgu-2f48GH>gJ5mwm~FcFYEQTF}9MnA2+td2U@|w6d@R zactYmmOQ<{7zyZ$GQP{cFGUR7ttJ4rs5v6s-bzr|LDj(sUg-jJfD~wUHZ1R8o~Qp+ zYAI=1vPBfjALSi7vjJ+AO<#)^6JE)`pQ))q)PY9?pS4<%?|#^@tw8}C6(vWt$yp8B zsi=y3rAS$3%70%XD)oG_;xj8fjGBz2tTC(r@U|=|1gUn$h2E$o9zHQ1t_rg#E1nL8 z*!?QxN;odr?({X`xj%?m(tq!TsQQ+OVWqaM_!h`om+eHoDS7AzU20Nm8I5Nb^n>EP z&atj6dzwamZUY3eh%N&R&wY)60@%Sc=*XKqT`&+I92h!6~X%i>VV1p1e^n5Dz z_-E&EKe&!fJcrtrUbehHB#(}xq+I=@^%cxAlP0>!*tSY=;mb=DZulwknrk%nT)o^v zo3Qx7ZrWv5Ex!+{-%M@F!o+?X5&yN<;t8%t@EDW7*IDyJO*Z?H`XFi=Q?tj+)SJNG zFk5XaIK?9fI;6VJ5FXruyK%V#U&ga4Dri4vIS5l@MAK_`Eif?AV~o6`oD(P6Rm?@% zgd<@qe_232q*my`B|Ir2Z1`+8ZMw=igYlju`(zH~*Q*^jF{AF3kfJDOGcmb%tK}ui znVzVyQg5sXax8es31~v$B@(glGt`1%5&=AZ*L^=%*R?q12pLPLv=btDFQ~P5d?SQM zwuDrvDf9H26mu%CL<%VPRV59&B{j)%-Y@1Vh|vDIP|#CjGYq*H#IqM)XUc{w9OII@UwR}ObBarehTiWO1h=$D zg<;upUL^J-K{{Av$J|K5&vReGo3|oP6B>;-)s>nqs`yH@VoY0M@hAC4a1{~xkSEYm zTcz%5I^Raj{gh(u1e)nFZz4|41BWJEW7HuQ$!|ba!}OVEwK7!k{5Go+ms|y>eVL4x zxtDj($37RO)lz^4%zmPrb7f*m^6iHwPN`fKx^}sa%ch{6Zenl;w8_Q>H%6vXSf;fd zy_6*6n7#%_U*md%onC2@;`gg?;xdm0nUJ-dysBtS5B?UhOaJ-c-EtTcVa%!P2Pi+^ z5)sfqL~rdGPbwk2VOYMeOpvfuIZe&{`7B5ANWu#|oh6CWsIXiqTF=GNuqmd+N| zs|pCqn1?cQxPo37`#2<)X=u8h+uV+0qCl6jM=aHU`)qJ+yul&ZJ|M^f!K-5EwKPTM*MHW2@r@IKj|xn4yUi5pqCN8_QNY7-99~k#UMb?`v3f)6ma)XVNLs`d>d#iH=btB z&GLAaNe0zqC#wA++}SQ%$iubLib$F|rl;+QogDa-evy2{^D*dTT6xAsC#at=zQYrN zT=V}>V}C-VAw*i3N{(49NLr_6(55+HK1oBv5NYCk8@l;Q(w5TMib&=6@?isjO+PWH z+lWHSMJMQaAs*TT?u?e5R!MjeP^9(?E2lmzAOa&BJb z+#``n_+OI|c>-z!)^?4R-usjNI*u-vNfj-$CVko>D7%ME`q6)0x>l>W?c?VlXlTBj zgK?;r;b_4wknw3M0RH2U3@r#Ok1pmNnudm?htHnV*2xXxVQ{&5EgkvrMP~VIEt9wk zOn~rVeXGE7qwuhd1gjMpTFCqUnfQdC5i}RIWygCYO&UTu)CpEqxyRDTP&e|6WB@nG z+;>3B{&{G8#L=oMVIC02k{O@wGEYy)+QgeNzZPGQ2E5sbMo~v-3?rg*gE^fKg>3c> z9f1rAI1v{5wQ|>GEZvUMLRL2d?a3Nk-PLM7)u-B@&azH#@Dy*%1l>{VHpu>&yL~eV~l5Ta?m)?6A23<06{>$zXcY@sFQPj zV;%*90Q}=)g`)noMhIHl?t89k7JoD;$L^SVd4VcZC6iESCfY|!lniyqc{_y z@XD|7;oz~wvkfd#{L@LyK2TkfC>TwUu!`Y`eA7cQE+5pSb^;8vv=Y-*ULiJ~kRn_j z)T1(2OU*t|+SPuJXjxkfd9HVCm#tbEZcxBXxjQAEmE&{tVrdX@s7L=n)e2}&GtR?z zMsv1obI+~JxQUpq!m)K2*@3rTv-1UoN)_Cr>CalFf4t{*qHUSmUwbE?6`aTtYW|s6 z(Rtg>mfZG;6@-A4b<+b`<6ypOywbS4sEVc7IWzrdxLO6!psVl)pPofv&Y+l}3XoD3 z@!Wo$@IpjxoafN_Mqd{-5pP6)&H9XX8{K_gpDa9#U9p7Ir>Ug5Vc1uRVM0u{FBAlP z;o=}fKLd(s1TxFf*(#7t&#~7>gWU~|WuYM#3s3QW6P_>sh@-?1 zD$nVc*lFg0gO8zwS92#56QsY|H*;Xrzwl2}2QqdK8?A^qIA^PEO->T)zW)cVy<*#0zCcRR-*dR&EYAzw5eI|OB6b?P*5e=SY->fZ$a_y+Qah!D9$zcULe-#sj% zC3H6oY`$hUl`L-?B;!#O#cv^gC2cm$+=hEzPR_x|cQKkqDXJl93MY4IJBDUjNq;m+ zX9JlGOjYTIMB|>`x6USGQc1^z_MMfQF1~qdyFg{Cn5P1Jyiez-FYMUg*5w9jI_zn zws~(KL!=Z>DlbsPV4q_eccPIQ zTd;*zvVESVp1=v8U^xsq=Y&V%rn1?dS&)oODaF(PTDmC59kcEfN}+d4-CVd;Xoctg z0_*e0!inQ-MV)@9-;aZji8Z1=Q(hqWG@B*xR6I!Xk|b{hC)<)g`36gJq%1(s0+O5h zom3>P3N37$a7bAM`YI=Rj*3%7er|>PT3&$OVR8!hn*!yyxeaYaf z{kg}D(*v_5Zw@~`3wfu-aYNWhBiwRh9o(p$Lj77HfYN|rb-@muF!k$B+`e=JN|LT5 z6sa`m?*^%Slr)`?j=Ktm);3jWaT+pd=Z=~J!imW&H^G-wc$*%40?XXX)%ZmDN=<~0UC6Sw00=zEf+YX(Zl}^07r96Ny*6NTUhv4-L&M>c zt(rzJ*rLsPqm$~ZU2&~uMLLM_`}PPcyk8f8z?!o+HGM%!Y8zXNl)ekDKBMGq^nX*v zA}PcCxw=|)c4}V;n}ar@CNHNPaQVK;1Dl>QPBbIkvV~U?XmasG7pv$>R164)J|>l( zA0v^^_BUKGh+8xh42Q+^cPqNPw}F}%S5b!Fcw7l@aBK-J4&{=ae&`mXG>Y2Q*a6g( z^VG?Z$(8FGF2iX{FVO=z+}um9LlO7v$+23q2nZQKPrFWVK>)v+V(f3Ac}D1<6q%ko z0}Ym!KQagjPF;7Xmy|-kvpv^ta46}6%N{Wm?mn|+-hfIfI1LGn5bN8Q*2O(s5UGeo zo5y-;i80(Wp){1b2zD`~CIoTF%g82s)pO`O@N^0)Eq-O(#i~jSb=3e2jBv&P8HNyo ze7ka5p|=gYzlpUr0z7Z_TUSBEy{gAyp96Y$V*A(u#&tQ1t8S7xWAHn8n%HGX#|I{h%eBp1L+366w+1So7{^7f_B#;M?z$3XW@mp;F|f7WEk6nkWeEh@E{4_uH7C&~rb8H4SA6Zo z4%s6Ao6>O2#|*vc^QLF>l?L*E3aQez3?_FO+RF3-jES3> zzE>{%r!TPUpqFS^@{c>Z`&@#sX)RCYEp-PZOBF5>Bhyqba~&~J$!+{9!gC4Yso2m# z=YXP~9gIevg1OfVF((1PGG-JLV{*|Ik0@AyEvt!0y?ZdsA%CmOwesI59!;UGr zNT@@+W^TT%k-E96!!Y%jfES| zG>#jFlpC_hw0Y<9Tt3Up@B zqxY?{^)oeVkyD}Xs2{OE}2&#&geWK4?`_c7Vo6;w`eQl|uw-X-CMjSIB0~#Y@+xworBJ^ly7W8S ze>v_(KHxjt)(2mW7>2NaNW=dz`%Ba-HNs)ud{9S9hJ*sD^P&;W^1-E({61{$t9du0_a0onWwj2V@WqEnu-EY zDSG)(sW{xv8&%3j>eFvX+bhyivjiE#h9}@ru{j-J z&+;f@BnNiI-QcX1hW)$TQZtcoLkD-&9?Wj{&9nW{7zGy30CntD7P-mWQUN+4ry|9E z;I$t`ljNFrS!EP*@@Hs#$q0BFQw@<(@`h~q#(b~q84fTv5rl@6V;xWs5Kuk>gl7ez%>;My*zl*37WR%p z7{F}9Jgf>}il1_2wkJTZJ(@0P^qSXsc09-f9J|cknXXQo7aFiwTqWyadrU?1Do)o~ z%(!1%{6#!>EQCAF51tHM0*UGmy+o9Jw3&)AL=fPkX;s|Z89WW#VhSx3NbGMc_kzW< z<2Hz%yV3)(+!vp_ZQT3sL2_OslJQQ7gkeyrfVo%s zZpEyr3}MK%i|P|NMh+*3&fiW2v{_M!E`1K$z$(dFrnpH4_$UPKZsQ?5^a?n7(2F7f zVj6eE#%%S3UNSIORl>^=@573uE{3A|W*7v!9AGP=3*V^jO4ARI-uNdSZ;0eX*5`}MV+?MF#9MDEd zZ9Li;WNGPRU}#z#Js}rUcc3$!I5{-b&d*JNu(DekTa?8-cIZyNg^I4}^?jycG}QyFc~}DPX8Ql znUga~Ze78|zRh08p&#pi%R~KYU z9Sp#asK`Sr7b8gzBN*QU9`}A7kCB0%*&nj+*T2O5>!z|`HJ>qR-@fAPL zY7P|gFXjISt_4_&0F7K=ib9wqKTov2R1Qe-^ZmI-8+iW0eK0Y}LL^Acd`bK{FW~zX z%@ILw&eWzHszvcxDZHwPy=-RAlH+74(;bdEJwJ}i7Crw5JaM}#XMKb zeyiLwgzWl_uftSqhLI6{VyTLNG8sR`2ajWz?eZ`_bGSe4F!mJ3SgjbTw(OdZn7 zQC}l+U3bWCPNBobyuHqCwCz2R{UGFeE1|72nk(V6CChFqaBtOsARe(U4QC7B(E^m{u9sOgG{n&Rs8YI1_qWX;Zbn=bTILd*kg%cC zN=1u|VF@IC17;J&Y>~{$W9?38eWq(-TMU0%_Jg=G^nKU&UU=Ud73RR->W3>xKRx`V z3ZcSQS3qrK9xi8@lWZdir1C&ipu;D35viq^*Z(Gh|L+oTOAUp5pCWiOPjh>@-pDas z5f=e3vdj9a%|#NbP9z$(=ewv^psp+j;(U35-px3pYcQurz&dIaMGva1$hu|p%eXO?dRS9FZS)U|>7@&C&w@Krv7(Uvgs{`7iFpfqbBnKDqJ`NlUx1Vw#nkt3h$p1 z0i$*wM~X?$!$EKQeH5%>;we(5TlVbiYwy3gQ0(R3C}aut@#N&e$ay=}L8Af&ub$Cg zJ?gXjYSF>)MckASbo_sThu($ZkFbzR7>ZMFGW z?n>N;%%v~LkrsQaF9Wss50og#<;D&GnCH`%m!Q|@5pNCVBvLy5eK;0PA_3%-S8qVW3M7UcT$@gIVe7w0M{|FBvW#(j;_F}W1gc#Vx=d0Jk<#`G2>IfW-I`f>9N@Y-@6)R0 zbe!pS%2}le;Wc9XX-jNeU7A$;GZ^LvfnK3FI zw&o}n~#8(e&Ia%3xsCP1KDnQh0^B(070N_G*RyHL}!L z-z|%zBYI>0&H&4Py4+8k+1{B4bQPo+O(LxNSvGdCV*Qu_Y>)IaUmx# zk>Ia&UeW-@CqCXy8C{`7!65}S7J8g!RQMoyyvzZWgi;B@Om`-zkTyRy3r*fFQvp*4T zRNbofo{{*n#v@9XA=2c@g^aXpuj|<2@9ckqi;t5{=mvv=jdmRu0OrOeOrx@C{Pv{f z*XTFa5-tfbMT#GQUWkrh(`2;JkNjhh_8v6V`6#f zlHATA(pq&l#AJvv$^aX-U>+hOcdWjwP1=eYMeHY?P~L1d-W?K1P0K#7`;2dXWnpl5 z3;BfGaObf5v=*!odacsFDr8LO+^1@Xbh=9zha`Jgwp>znd@+qxx5;$1$Um=WyeU0p zc!*cP9%bOP6j>rW9C0pxVV}f9+M`$sI!0)h5Um=XwK+tuSLrNzC4 zQ%H-Yx@hxz8))c#0s;*vS6Tva;Ug74pT*6T6*Whe1U2==!En%B*%l;HjZ})r~xe^auBr} zq}Pk9i$5u!ldY|rd{)^ArQoc*c*a}TD)cW25RV%ijv zPW&dA`)--3?@C4T;jO0w?=Y0@lNAGR!nZ?WgCzYYTQL15m^HNYM7 z(_JJuKi7H(YA;syY9r4sB^=O#txEw8WbSpcz^|a6*q52!*-d6hGJySZUZ7_~yRld{ zOUF@r3vMhofR*~b%va3~t;1by$?YY*;mx#J@5wbx< zjvF)&>|6`_{n9RchWZuhNfB510zg*ItId|)k|E_}CZt6$@hATy%ntJ~iZZEg!|~ri zFftRM>~F0eQ1da}^IZifK*{5*%saS;wV|u0@5O}VRKMk!n5-?F#jT7iy8jB+frTwJ zaM@S?Ne{v58}bn20`axpT)$}>cAT|Rj|_`Z2P?j+EY^ctZr4(*vbybQ;Ja7{o5R2y zP6j)yqtHN1`Ub@=4UmiH&$W#U8 z8?Pbl$Qm|c(bnFKbB8Y42rful^Mx)TXU+1D0iwS5atyv{H@iO&!e>AE4b<8frxN3O zuibx;px6CtT15H38~_$t2Nm~?8Ji&j2uNfYyb3+=)@Jio-XFllA*OhVz|4s5=MU8l9h0mB@#vg@=|?mi zo(Q-LQ&>?)?X5#X3W`|Yxw_icQWfqb+M^dE9{AUz8wAWlm+APsvd^9ur5wg0j0sF+ z@mge+b7CZ!rO^pFvg(CSqkHJ2Bt95JL~=3CpQas8O6Gte!C;>sBo(8mNGSM=T-owN zoR+T}d5)dYTw>|*%ER#rMwIH9Zn#)s?v&$or2pIcV(xJxHx?d}ZSYx28@Pqd-vL-O z)Wh0LA1w(=*l2*J5l5(E|F(*X<6IcuRGb=rvQlRV)2U^ZJSqrdjLJ-PS$jePLp5(v zvVWwmztK*XfANp1HrTwkqb2S3lq{z!B>`2Kb6-NVuhIAYi)2?*#tY{wOB$i7*jb?4 zw*p2F=DzKdqlEndf1rusqNE2_mp85FA~o7G8g~TcaEKIjH@Sydfx`{f&G!ZWBiVH* z5z?hINsit|&@c|!fk8|CgkHo0%q-AWSCPIZHygnmwbcjW99DMrof8^#OH~c_btx_g$0y9GV zaqOTk%mKP$iw5WHzO}dEk*g+cVp2ZNN0{$8kRc zsC55UV!TIWMVJobUgXU{>=#3DJm8@wMn-W^D8uhj{6+*Nkn#3U>vL)B(e z6YJ@AccQ7`Vm+i_gvkRo;4xS6L#Scdt59on564+h)x9H>A-OILQRz>-)1`oxuO^>g ztN$%th4K_e>Y(MQJXLUG_XM+6$G31*IW#QD*VA~ z?j0Adv}k5UfyRM8LV^ShK3>`uV<<}OGF5kcC@FS2iUTIE39LU+0YC|VpQQ|5iEvWD zByLA{<}&P*tiGp#(1>3%z+Y0!p25;<&D+Y$j|e`Wzee)ONZpnKor;3Ny)>J;4D>i+)3@bhBq$es*V#Z{RY#qWkJ!lEPd;ie15ALW@yl3@K{`rUM&Cfit|<-o z6Xa0?j-mjNUhoqZ8}1@K>i~L^AzQ@hL~R;`L|IPbMSfQH@<_d^Qr# zny3i95y%FMG~vk&G3AjvoauNiXs@YbMDkih1?)PcR&1rH-4&PoCxE=7tc>;K*E|$9 z1fuT5V(G@aGIOfJq)-|n8aa#Ec-?we2BittrI+XQ@LDzFRUM{AX99sNK(IgQ z>?BG$!b4JStB)`lqa-a|jkCATqKF z<3VG}jeGf2i)nD%1Kqg8+pV3E)S-H*a}=&?Rm==E2$EOiOLbVS=n`-RS`zmFm2J}@ zlWQPj`f6{yzS{skQ+Q{Ed+)EERePfvSkT+7C=XK=wN3A_5`Dx}o{h4fsT+(`@}N_7 zHtkn1lu8nNThYpU9@3<7Y_m{_UcG^ms+K%G=ysL+u*qCWhE~^~@d_?~{3rCDIEqO?o|? z)Y)@G06K`m4#iXtI!Ax)j9TC`r$<y1bj@lRlj` zM$n%GT-;Iuyx!kRJKI`NV-&*6cKrSiKgtZW^t9{+Pe3HmDYwzKN{1}(ht{tEb80Ib zj98P>FsR{kCL;CiA(a#py_Y7?cqjzU2e61k+01?|L#EARFZ`kF#!T}DCQrRdrhHov zfnc>K8_8&r6yN?j>E3#~=?QBEZ88`e8A;cxM^*o+74A2$tu0_(^`Pjta{%kNg5alS zQ~YSVjY4b{QT(42e#mvB&{&rsP!2CcwXJd|Xdz}q_|;LI zy`OEaft4rMLBS>>;oOOSTX8|8ta6!#Bo!O^H^oa_;0r1WbMu@gC1sGlMD^cN`bkl5 zwf!={o0vY4KYkrIA+)9JUnj<{=+J6O7C6k>ZxZnuP!p2VS_S{)DQvHBDL#swrlCYm z*cS!bal!z+P>~7*i3-g7{Yo87syj-y#S{4+?XFNcfGAz9v=MG#28#VwJi#Qta_HOU zkUWM3KMa6cY~KsygIs2(vv7i56xx&TWdqz4|Ho?H5W zql((g@nk;~XVIur6k?t&f7~r?xat-(s1IPMRxD-BPH`0?$lk3j!bZS9p{>-fVKQai zi^24k4J!6_S|sCS$g26N2{fJ^S=y_yU&{OIUwJUd`BhyK2LMStbK+(F<+K}j)I4t_ zxvP;mmry$P#qJhEsQ;;`q(Uo&Gf2s{#175z9?i+Asi^UNAVO+OYD>iR86NvR14@jo zPW8ZexdQeV=eDJZv)*tNnak{SGW(2F2QKtLh8N9nmC}q7W;=KC-rvzzcW(g*R|%6l zWasB0LsX}n^R=f6s%X?{pw3}aCp2#U=G72$XYNzi16o7^iq3}KRsu{dRLGwY-fVbX z@=uY77%<2WE_ee)Z{iBKd4I1r z#gz$c#Lu2~OsmClzkWv&ud*8y^$~IhViNz6S-eLcGM`#(+e9Y+bYTsM+gq=uNcl?e zBZ=D19MM!%KV=)Ba|A9XR;dAL0DT~?F_qV7ICW1!o@nFs@Qyb#ZO!iZ zl1{K|Nqg;2dwrtvN;eC#Vc6oE$nh>fA*++O44zLC0jGovzY+mU={t& z;ylE%&0`?tF&d_R)Lnvm1qLH`GLaA>C<4V~DbWs>ZSw*Cb93vRwRC9I4*8w(vrwKU zbE?RGb2-Mu!dLcX%d%VG>qDNt0(MOuaB|6~P4Lj(yKf8Btiyv z*P8TUJkm%!ELlyja3IeL=zzgHoqeZfn;UKTPskaqsk5z>Z2PqqHQWpFBt>4J#V0~` zI{!sVZOElx*48F-f^}^_eh^2(_%Nc0CW&|SfT6#g)=!Li?E|!HrTbV5k7V^em#n7! z!!_0se1>fMb}>+RT(tRE7rGFmZ57U*N4hE%#gjiP@tsJQ9I57|{0a7wGX?o^QG+9B zm+}|;)7$GmDm}0!{<+>NyiDnVSj_){`T9*OM!GfaiwESKAW(vTbCW{W+F?|-d8kPW zON;oJ)c&mG?~(KLp)!~h-<$RZ?B+@c`+f1~D8c(W_38kh13C(T*p@~xHbvaVT06B{^PVrFZaXecUok8L zrKZNHp%tW0DiqRVSJH(c!VwRy< z9O7p~buN*ghNi;@KAe^Ojme_nL)XGt|B3 z>*V%g@?S0L{s4fPZ>VW6`KlqWdhH$x7U?iTnc_j;{#kM+yhtO2)bQ&Hra}2R0@3l- zCdTA$lw?Fg+H12t@-!(QAh)~3;sWSj4O6{&PlglnRsEo*Nm&2iWO-}jcD+IYmgIvd&e8|Tw6odt%K;@&6ME6|F&20QsaT*v4_$f{*nhx<4}@u znb!caW%fg_(o!0Nkr$xsVYTihp=9OY^y(>>UePD&pH2xI<63VACAm0^9rtDm3)t@V z#@<64OLR^w!z?%^2|v4)5_);$qIfoPF&Z6QAboQ~8cC4{CF_^1RmomIv|g4q1Q$MW zVvZ5tqRCK{bFm*OcS8lM;da{xnW7Tsv(atQ5V8028IC*{~)`@D#TQ z$gFkok`{&i!vF}E@eF-Pf10Cg6*qvn65o=G1)xHiU zPi>55B3;Wjx8$rMkEosq&RTIXd(^{teVK5I(FN3Ym5(I4zFQE#jZ zZH|QG6^;t%;^8J0=1uBc>*q=zZLg7PpH36aG#BweY6{ubL*w4_{Lwta z`*23=R}53zFh8LL3%<*8-$74HMjU%AdkS%q-7UvRTug!fYAoc%q(~d2>yTin!yXXM zfMJ46OYW(BEHx`v8CI==d5k{@)fIL;@&QU*SByc*MF_#KC9jU_7TYqGAL(noU6Tl> z(Ra7Ua>c1TO4Q>pu`~&-jBx-6ZBaQvrihr~z3SkQZh3n3o@cTMF>gTEF4;U9Ch5O+ zQr1aG{IQT|?L+UEClOPyX);isdWS8H&xA*&v}$B55T(fx7&QIN0Rd1%#=%kO6wl|hI!&}IC<7;|KB7Ax zfKg4!f63YvRqT%FE=F+NFr`00==Kc7JhASPGf8;A=~wW&KKNWPf1MXV6#E=*nqn#z z`DD=+1&fG8C2pg^k=+L#<8Ia)o%ZUE3T2u!h@~D^v!32387(t&yz;udBk#^D<)-b9pJk7mAp5p6}Wb8JPu9bU)wXtQJPI>Vzj2|v`*tbu37RnXZ} zlBL`s16GaXKV1?9_s8&snD0>@yFn`RpD>n~Nz7mUKX5f%C-`n?^sJe#D=Uje1dU3c zs{?KkgGTcao~c4U=AX<%-V@b*v4-HWsF;V3&_c4O2_%tt*tmaL<}R99g%q{@aG}Fl zyEigx2{E1!eoil1%;MYqizDOk3Q(-b@uev-pxz5#r>{%X_%SvdBXnYmyFK*AK^xVI zrdfcMFM@mKjz51ba^epj$~%g*gQdiUl>G4Oe>cuxibkZ5?4To*_E^2Hc(_xXj=Z;; z*gUZ({70nkT|&MoPYxJS{9~(Wmt3Tf*ZB;WAlNN}7-x-r!8#}acx@{hkb{&d9-<*} z@oS4&6qjQNC#m|UZ@6-Wg~d>a`)!@Y=W7Ps-O#CVJ~lyUs9>!u4Z=s){6?u|55lRI z#S9a*3&S_hZ(e=@NKVKiayAs7(nH7q^nRnpzr;yo&d(YMJnq`ZxfIOnKU_0zM z8{`8EMFA%&bZH9qccL}B<9IjKSlR7$(y|H#q-h06n}!=>5>?6bG2JjnR<%U!?UUtM z2`1JH$0efj;?YVtFCN*5ubz>(`g*|5Vhi~(+_N8fyMBti z{M;N2Ss*HF^A_4gv0e0(vbu8fuK*UmCb8}#&t$M;XE5Yadr9nmisHk1jCfUdJB&Y7f#M(Xs~d4gnIibB!N%dh_%Et46G7OkC2B;UOc)Si@UY zoCGWkjPYzAY*Q0$=S7RT}76MGRe#W5VT2 zI!Wb5Z8x!~Ek8>Orj-9$-0Xn%=DG%T&zX!7#*k{s5hJjt_#OvA3aZ1{ZzUlst(&J> z{_NkQTE5FAA{hjSwmExC*nML&Dbk^#>{E1_YZ5P;ue%a>@6x;>r$XkMs6Ns-5F`HR zM(MD0zOnTkA;Uz_VPl;~t%Ibv=ShbH%3iDsyy^~wk(OC9{8uc?Y_~q3rKl!7($V;7 zcbjdO#h1)QQ&|s|sjh04L ze=Rpf`l@dg?Vw_j1?~@B@8H3mTUGKAV4~VjCYH@qFWF7Pu_%A5^3QloO8!Dt!Ss~f zL;JjMRFLBVq@G8AjcX;Ub$O5vp-Iq;$XkjGIs_Ebw-4V!Ozo1}ai7tEJy4XmIeFnD z=$*nPrBz28Qtqd^Te(zG*jpG{-t-H;D@-2AW>4{NIr;=p@*{|PSaAKaYCMWI)s_xwcyn;@FfHy>d7F#Ty>)Sm0U9JgPG!Oyl;8VHzwd$J{@b z18w@6JqrEitas~-nALaCo*P}&ESxsq7-hfLQV?J?ZbX~s2#d2|um1b>N|6EYp3Uy} zIA)r)h{rs_7%^oZneTln=B}3!gm$?;^y5zLJ%}l*W)TZ-_r^w_4Jp@y&C(D6@+qa& z?-aW;*0s*LNH({MmT^5lCa*WvW8CBzS&cK4+L`Nfgm7;dP0>x<4H;9vvjrB|eBzhw zaSznB$FC0$CXMY#Q&ngcqU9;(xV9eYOvxijYBhg*Nkdy{j2EjC^a(H)LPQe8;CmVx zeHdV-Mcc!>S*;W2_lbD)gusu7llFUaFad5yclCvT?==*1z4bUvq44a`AIeNWJjDK@CKM)Q5UY>1n@q@v|yv0A>8M zB=;tXA8qTc@=6DMk?h3tD)35=+|i26UTbnB4 z`8p296HwO~dQlJ_Q2l7Mez;#iIukPxem#y8pVtgI=_Dl(@lYT6P())-*9Lj#VHcl! z9p@8>-M&l=Rp6$Mue2lyGUwhbN9PqbxVz{FHywo5K5I1C0;a;Jcsr4a{c8l0oK?P7 zu@g;FN*$*N?#4Z?H*e#f2Z?yTgkTqeLl6gXX>Dus1Z$Cz7|2V8iEc&*t5jk-kM2-6 zE+74hl)fJ)s_$^YfKNW`hC}tE%>{VAZJ{%~d=J*uXRO_S(@34y+2Y1~!NB7fD#JDw z%JEoM=qUeQVw#%OL*azqug=4qdtOax+(wdykI!xOrqEybs<$7vlBxc7TbvlFcP~6- z;(lgRa1p{0-36w62g?f($X_Lf9n_B7_-Yq?PSWfnYlI>9Y%g*itWBj1EqwiN%GRY}A0 zOmWkwnc`;BW6G0I{R^on8Q6y6PNn`v!FgV?;zo3?PDRPgeJpB!80&Fj zD+IA{e@b`z!QaZ4=m^@UD&cs-FtEnDnL0O^w-KtK<-Zq}6ARzP{6Ef&w#TKP$7yD+ z!)L{=ID>zmg3S|u4_{Zdj&;ckbo}edj6ZVP8US;)z~72?&?{rcQr~Mr@f^uKc?|i{ z)bt%8T@%n_I<0R;SDx`#BG@Y{B8VOtGRdBMlmfAi4Q83*6KHeipO9>C5Cdr6%(8ls zDgj-(zzp5$BSm>Sgnl3>5ea473Dtq7y^^0y$5`e-mcn45IxKbqS|)mJ8C<({!yM?4 zmnwXrHTq`}FC~w|bWXqLS@M#ObKpmc|C8jd6@>efgbkdnGt_X=c;!5zqS!Q$g%FS| zf3iDF-0`>0=qy-Gkl@9~DxfTc6Rf0*0SX*yVC+!5%BL>Pobez}uH?1L1I+ z*Vn4-bXp~VDj}t4TY$>_`ih1@Zk|6~Fuuc6ZWp9cV0-b?47a)O!QP^rEE;5cBQbr3 z;L8FbkdXXMrr7q;vdlAH)kY1SC~FczJmky&$4qRV&v%zr8w8C5&EJGiNR;fWZIL|I z-azQy9(y%inBX&qDK~RI{B77wkqGm2*ZAGF@lQdpitLx!v0lHMXKdU^nC4F;HCJOBBwR~*Pf=;_{zo%5r z6}LnAsc>I4gZr!LK_?Y`y0}a;8KZ#q*6f$iTfiVklYQjOlApGqi~L^UReJGXzWV!j zAKkeexcNGK2OVJ!E-M7ASjT$NomYQLM~lbpz4I+?Y1Du4EpaP*_O#6&swMW|Yj=2q z7#9~-1@M26i@>gzXmzZJT8T7_lU(IYcyW24ep85e-XtJSlIdE-PFuqN!MAr)a_$Nf zoa|0A&Fd(eRU2u*m%d`L)u!Q1jTFH|1JiJQ@sbyY2o~qKI@p#@Gzp1jAE@3kO_bUj$@MfYGhmf26P(;>}uL;JNdybq2ZrTUqi@OQ#*!w z*jQ1s(2p=`;$7cH9k`foy@#;E9iE=F_!sw>&hy~Mo14r}!*v`OFB8Afw*yi?Al2SD zYb|SX?EzUpj zoq|D!k!Z8ZGY`t@GRi0@d!q_9F5=&|#sihy9oyqa)B0sJ^uSg>im$i-J?X8n8~VnV zROpsSM0!Q~Y0tO@E62KsU22yQV{RA@z6*DOooO)NDv>(<#$`yfm4mob0mupnj9Fwa zy@_Bpsztuk(7o59tJg4fczMLNA<0P1$sWh{UnI#`t$ z)WtiyczPXrK>NGM;=lvz^D#V0^Rq{4m_Qh{x$ZGiH5f`toawe((bmA2SZ$eXDmsA* z1Olf@On8nb25Y4p4da5|%+$3^v6>^h00EfmZq(M&#|qyp-{h6_ZA8BbgtP)TN_OXO zj77iX5yI(_0#r$&QTqz#{MgvX%PGK6TrLI4 zSV)kd_=T?xpWH8N0~JM@5M1kxGZTmduCxXT7dLJRw`7ueJ%A23L*V|c6@)Y%?Ev`t ze_tG5u>CsmwuT9Ko!hFO$&=+hblovus^<%z-MhMw&JZALvwA}zzk%M7#lgSs6*eqC!iH*ZWTAA1NO)DTJI}DoO z?x(>QW6_%+^yptpdjwP>eFXN@bl=W7j6a2HcXw)O+6K9g(z>sTjSzuCvQLwpx7GRx zAUKJ*MxSdM=<`E2Cq_kMe6IfVFbtJg+H0ArYQ!WaXF8}pKBzh$Gr3RPmUybu0A1f; zjQ#{g5U}Wy6c6tyfdUbzi_NkU)=#ThG&W4U_yaf+4)RuOV9Q8cG+dzgS55*P{W-T2 z9{WeHDs89?ybaY=#!;Q*RxSogqE#lUE9>y{)*^x8phEnbfzXB z=e8oMmk2+g_QPS7@{cL2J&;@YR~+>{EvP-#YoznBQl%JU3VR7-seDQO>!Af(57igc z{oEN3?@h5|##v$)2ADy*&`qWn7K<8i|0Io*7zS-s=af)W*LbTK5U7Ui0z!(eKcnJ^ z{BG8Loe&lrxm<;jIvMp!KeMfCvid}!p8@hb{5SYV{`v?Zm6^FZpdTJMb!$JL1LFPE z!kyB9|DFwwLfGotnA-R#_SX`rnEvq{*A4F1E<2!i zE3cvn@2-7!_ouZUbyxR*`Em6oGDpvJF=h1fU^59>5BnVYKQ%MYe zEzZRHG0rw!^6*Ih+r;EFfH?Zf*~S2ibdR4G%M#A`;s6lig;4Gl4jSvEObO#AfE;xo2G-KC zT;mZN8VxB3Sr3+)y8Dtn&ret>zpnMaO_%p!7u%w6s!X|vEPCp4zo(LY8svlxCw}bi zcpQv$8I~voiqMjISb`I+EOfE^<@~0CR(6j+OvMOfR==3MG!&{R@wpWw=?=7c2q+y; zrz5H(8Tr>2>HC`?%Vjt7o!@~z+#jWsvQjY3HTb|15(dt@{e>RHCvx6V-A;eGRVV^k zE1VOL;3GE+4(8FcUR}!GO*+097!|r@I%7+yeh`C#Na)jA&-A0lausl!4?_g*m@KmU z>@oW|9Ui3KKrY#2$Dzv{Nt_&FF$8gLU*w1UWag3h){{DtmVUlK+wV@U0CS@t7hpHF z|I_Ap(QgY~iv7RYp)53Go-YpRn2qh!*a`GNbBw9GimyN6nbIqoHp!~C!!EDib`!Rf zB_V{O81Eve7cWD_{85cZ;wNwB`x+uc-nQvuDLSIeypmn2N)i;0MMQo~>&cF^R4b9~ zoK1;f7H2gweE-JPxJmHtHKTI(owEI98;7ksnFQ?#$29z$+W=vunkh20Ta4F2H4~gs zOwANLz<;YPAuFsP`7?pVj63P1q!xOs--e?yPGcZc;5K(60AE{E|A(IHeCBGJrFlRDa z$M8%R+Cz24Rq}oTmVV}2MvH6=fnkkR#m@HReGc?&U?{L)tYcjc_EpE0pPA*30pSyZ z3dUmrR<%XuEsRKbaH?&vcBjG4^vU*9hw}hV71qQ(&QwC_pi6xj0=xFoYpfl^438~I zA$;6c?wX}9*J0a|oe3;g8Qz~Pg0$Hr z+^3=TdIiYeq(@z=2>j&^Oaren$P#~7eP4kuOlo$>8raCLeloJyx_B055XW1&WehyX zbC3@V^DP3l8HVm)+m+@ePR5R$x{14dIX)IG_XC@;gQYD?U6zAba><-QO7o}FO9e_) z2&$K?b0h9Hp?EGo`s{y;k_BKgGq+e_fZ1aouw<*zyBAE1V{g8otYTNf3>{9+wL56- zdsv!*VS{%=S9!dH<_+U?4iB3UESt(MZ4+V8X}#;VQkQWnZ4GvG!n>p(1EHOmGU+w< znr^ngbU=PXS{yVVi|>3lG#ps3Z7DM(K0XEbG}BG*ibRB7xf~Dc3P_X5)5%;S1h#b) z<)fMxDf_#Z?NZx1#zB5$jIOo#o&7_UjuChGL`2Fshinp~IDb3>I>#!BnFyl~U@$_Y-C%g z-+?Yv%5RRQz!fDn`sv$6Sh_O#)4U`XKui5+bp&sb^$AmRO|8*V&@RYqzhs;2zz6G{ zvQyS!7?JXO`}XF0PVpm`xz~oK5F<<%Q}C{JkOrEeu}s+vQlEqRDq^0+ogp9HVY15_g; z1)=kp)h=2h^bKr=3pRvgGOpGY7cyywooo}GBLxKnfxB-C`f^c-0_Gsa0phG6QeCQN zawb|>2XY?KUACnzSKr3QL?ggjLo(-`l*JMYA*^l!fTb)$zzdaCwek8`MR^lC1_LR0 z9xRopPc2fBmLSo&+4s-R{JV|4&1UsPUOUu3Yk^Y1qI=x=flJE=8SQy;jIV;k9O`)H z5P%H@b6ha`*o`4RxH{Xv+X6yTWrCDiv}ey9L(uWE_gs#ga#wYRR<&AQvWVWryCP`q-Edub;3vaP2+eW_!r3rD z_{6}e^{Freo*2#!n1KdkLDU@884=#s?vK|ndK$+C9IY4YbJnjG5>>A3 z|HD#~*{Q;{T_R5E*<*T-OleZ_Gl@Fj!~nEv2EtLPV1i~0g}e4`{c*T&IDea9-6GJa zYNL(^R$6G>pzB*iTW~oBB$a#o2`I@$p@Z9SO#p`3ANJy7bDE*+WkUpgDzkRS_Bt1GrsRKhM`c!N^BG8I-^X4vDs zOj}~uy@MLnRQ?W0BV^M>SX&WSIPG-lBnUdPR^jX+mT+&Kx`mmX+av8DNX-m1#9rQx(r8>EGy$NoT+H#4V~QQ4 zb)#~w3?JI*jEsl8LW+Ep@ApZWdIA@ zYucMlf2K_m-O9%_X^)?!`ZzLeFB6^}3bHbe8KwtN3p$d4{^nKv#Ca}#oY;xHH{YOa1Y*BhF{%2Bm;(W z4Q`p${nzss+|8TQdcVsGRf#v(@yunf+Ofwy&Qlu^G)B9T>Q z0m#Ih&}BGNt|yBd%%A>{zl@0sTgRtWkopxb;30gO8Ul4i+Ziw+s`XyCdAj1&9v@gX zJ@%ajpdY7~Dfxy`$R3{+ZOq#VK5_6J{WsA-$CgsHOWe?v(?S#Wp_w;lFzWzMXTHlM;=od#lzExLtB4f31A zd(>Up*T4ZYA~(`aNX)=RuD+bIeQ|!I zA7}tq3leC%l-x0Sns9+&8hf{Xc9>kWV3#x3>&5kpiU= zH39hZeIPy(5@ytUZNA+$yu>V^M5VqE_=ZWBXt&YUUeWyHYwo4D#n&5UM|SJBZ^bS5 zFHiTDJ2ZW&?F33OT4UoJig5sl)9>D6hTH_IKsgYpy`>GA26oV$n^|hl%-2jVE5{f~ zh&8{Jv@b>|U5YWT{tGwVeEKh@hBApPDjL>vK4$F_gjDw`{1drkeNH>kW?*&piEhA? zATXYq?;4wf0`8EsXMuI%uz}BN2#i9r$7K{sS2Mj733B|`0muhZ%TAINZ=we`TmVws zYr_Do`8p#|6fqdEl9gsnu4=Bi!9ib$w-$;vXYkofwK`eTO13m-x3~?XWO0BPiNLtd zShWIs!!y=V2%&B*y%h{lD_-Ei&t3Q(%9eGocNR4xTG13)Q}j_!+~bdcc*DBOxZbpp zW~;X0e=xxE-$!Ny^x+zK%{2(1Fkr`N?AD@qbvqS zCmr$VBD3g7{MS}jl9S)Wpm-zh8q6%~bS-T3gKC)#+Q~j=VDFF3+%s)+C&0okI)`fNP9De@LsF&*xzIo4Kmq^P z==oGBc0uFg@)jjf|BL5J^+Iz=G~>Npb!sq!{$pnPtSUjZf;L>j0CWHYpoOr`O#0bV zoUe>5WVI}t*9k84vpxSRTwT*?{laAS#8KplTWScL{Vui-hxJpwPD(|b@$$0T2E8&z zHn#9I*pdv8kGpPp_TE4y<7k1H+O9)1;LL@-`^};D!US}Az2oAj0ulrN3 zmOd$n%plem7Kg}q)yJ)4tyjql|XTRO({J!K*}% z^V~*oVt*QXQOt84Imm;AxJ2ygBD=xX*VeBoTNh)K+P(}Eagn&*og|-7qbA>j@y-+9 zk-Ti%#ZGqplen6#853Xgw05%|`Ku+LbE!l`t6-ONrBsVnUANTahLOPnSHi7sqdj)T z1o#FuxJ{OoZKu4Y?8Q_lV4*Psa}NbvDipV)55Dgh5Y}W7LJ`;8)|8i|s7Knx!th3S zuG6O72$!eltvsKvRH-n}QEt0wDOZDXk?vi1WkHw%{?EwM%s|Uzz50p0z<49i5{$ey z7ti(p6N9*nuzB`D025T-{ZBILDPp%UNJa`YK3h``LES=r?(_%U#Kj_kVwc=qe`YY} zM#sU-Yg+U};cav%Jm6$vDvMo?9z(g@6>rQtc}(B{6Q06BW>m0$=2kY+Ux!#a@fhda zAQV+q`RYj{1*YQKFS)fH?tmEXy8G`15*PRVsP~J(Xq)?(HiCNzAL4f38q$6QVIyL(lwC}_&UA3 ziSRYOV=Idh0$01x7@gw|$bcdp@O%YbEBYvv*FEz9`ei$2Zzr?}xOgUO-JbBMpr zZYG&gUcvDPv0wH=!+;^=YQslHf1?)qm_+F5;9|Xr>M4qp!Q-v?jynf2&mGqqOI)pz zL#BQJaWAFT%83rOqp7XV{l$5acnVn&4S&E0zfya(rMqv(+j0!^Z3cS8<*)HYzE<*I zSVODg+O9xZL~xEK6TAAK2h~q2hNmf<5%iHVeeB=oHo*^vb4s+pWXhpvwb^s#?2fy_ zP{^Nja1z`C-oRW+mxua*&g$-O7bD4Wifs%IuC|^>Kr4%hf$8ooXbW%HY)z@z;u{VP ziWB4EmG2mkB+TuoV3uWSrJm_it}qg&Fu{$&R?AY&}U(dcq71K{(=QA zXEx5MYg9Y_Hq07$iihpzl+p&DSE_$y1-C}0RI9>qj$-<5?bKW_CEDnN@dd)|z`r&! z45;KRsg43nlD87|8f_ya>Smqtnt(5Gb2}n0jiA%ZkKDA#dcQz1+rOm4%{RSpHkTOX zH55K)*5L`WhNvrvF8W=D^FUwGhBD-zUOEQ7v*WN?&J|H71%gs;$&^h?0*lat&2$Pd zho*Hx@G8i(LMh$^TwoTe4__58l{%ZSthRUOwC5N9&230+Yk9HHYNMRWkYm#`pv_w^ zV7@9UhC%vwyV0IPkpTNwE6e-D+lSxZMF)3W9S)D|;|XDG3(2ds>KB-%c&S1<*Qq7^ zB{{JDMvU_Jp?GE~Rr96C`K=2w_~JNNElHk2>+q&CsIPMiv!*bei+5K~+Z2SyzdEne zZ7=D+v@YO8)d1H$+&|U=(<1bM-KBTnE7d@^st>?IAS%BQ8ZLhU1Z0Efgfhg%@$L(p zeCePGlbqcNx2juzLKq6Y7}YS9f%r$D$p<3Q(=14u>0wgNozeYt%Wz{qbe}em0TEC# zNO-V0M6`{QID@X)Y-;(V{T0@e^zy+}4oIUyWpx#OU}Jj2N@E`sW@YMPJ*+S30$Hql zdW_aX~vZUR(a5NsNQ}CT+Vkt!vszbt!2W0qX*wKw}$bF$PcM}GbEFm;mElF zOZTI8ik!UF2V6c}rAljMBdTN#s46zt_4WUu)%R<#1BpL%_4bfW_6b*np7OL(f_ycG zZ)o6!{c8aOotV%zVJTBAwu~y(&i;XVFX()Xa6rH8tu`QDX;f@U+Y42$%8fq)fi07F zhWZ0$voq-O$`K|imwb>z@9S83sDhRvA6NypI!;j3-dttW*XUucjc^p(AHYoEf?6=)cEW>Spys z$NB`ifR2~SZ2VVsh}+_q_yzIG3Skev_Bb%WA|GNnak9s92KRw4ST4E*WcjzRZlA5@s1q_Y`9iFsRbL~w zh>Tqx7Kb>ypip)<3$EgPG2^G`OOwqrmbn>74hfbXre?M~gbX^%5ZTwyshZ|k1-O^S z`FlEs#tvXpf}`{DolxQ%?Pr%F*uwuZSFQw4H55B)H}7KwUDAjY==WP1UqnIL^eEU! zro21;IL0G{-X1lplB!vwtkfHE{#|Z7lX>pH%BU)Z={KPAS&BP5+}%e?OM6mHRY&8E zljvD*1_|d$?5gDZKMNCdUw9>yvx|EN^%uV}(9UpudWe0y?arcup(s^YE4uEy_FF zJZ(pqqZKMP>k>Ku<#@X~I@}c`ku7I51_N(h*$3QRCL~xij5GDDFtP7O=_1k*~)$LG~u8wR>3!wl_rrWO$yuK`l&KD`IntWjxo&0G+yK+U zKuH#2CD642r4`Fb?=F;h1=;3`QJd|>P1b0Cs_so|a!4tyCxn@*%*l|Ve(7(YYZCkR z0QZcQFt$GZ2^i*VQ0e_{X1uFEbTWhY_Xrro&IS%8rS@|dY7XrMZvHso-BJ}a7@`;* zA(gHnx7!#oU^QUYkEv+s`6rpFVS3*)eAOvDS!FLf#>1QixB6&3K>#3M_k=N=wY|c~ z{ULG+f$hXSes|)if0bFb1pe5!tHA^>Cp~|X-q%8 zuS4$9)lho(O-xZ+f0!74!db_q^f~^Z8k+f5^O9;cMA*u+G_;*D2nZWS=Pt7W zkpm+KLS2GEd)Hv{05Il&;x&y$?w%{g=8W)$<1hC6Rq%5PG_ukzB2sW-=o&uN z3**S|s+Qt?Qvxff?MlQj08_8^|70#1D@$|DIFpX+y&#DQs+g<9vy)0M%rN{^6TZ@M z7w1wBd%)749;DSu1ycGTx57<^rC<*5G|CNAN*MXUSWq(#;!ietk_wGw% zabfyGM6*xpcRrH8$0)wteV3c=B(64?hLGD38naY*RyWROm0lq_}9 zad|HW-j9K|q-nOoc=s-uxSzOCyVGl%-J=trNJW(}YquhTlyI)LD{V8SiDTKO=4E>R zgV4YyeBgofOgAT({I_Ub1%5X{EXiFIMZn;7f=DcFT*km!4i`6G6kW5@PD!p~Q{x;;X1HvH6{`{(EJ>AAle_G1R1dD}N4 z>VT+i@}J%6ipS{rudb5_tr(l7MDty5wlS!KxfvW+)zHX@r z42HH~u6DzfT8)PS(Teh~Rbc8vH03LSYSVw?(nsk~M_+{UMkeFEV-FSF0_#s61qL#7 zSm}ix$bcv<#Xv|~aW<85O2=3KYliLJ_dL(H^ixO&$k}4o)YU;s1e2TtDDxE)?+*5fh z>YH0oAi;^PZh&~X0#yfj0xGY_G9D~r7dU3-A?`OVYMM5*GMrf&hh~1$vMH*0N`4qL zq2ZZ0BK7VRv3uTDbcZJ0=LLSExk~JR0m*>wWj#F`uopx{;sW~SOP9NpD*qDkh97K* z3xnMc(I3BcuZVq@=+EXImRE@@#YJQ9U_c(`wF^K5Oc&#MdJxIvkEvp}v?cnvWKL(vd8S^z*(2&IQ(NJTD8SB6HVYOW|brM@$Mdq2@A#Q?!e z@?r5M4s+@_rd6x13Dkl0oD3|FexCP!GPq{>4vK82+2qA)K<7H@Kn~Z+)3hwqlXS)6 zPLoS%Gle(F#sb5M6t=5FE9%YSA<^(h*)64wy4DM(!BFvs35p>{QhdX;SG+FrXT<~Z z0qUm33p}E?k^;E+coG43)wg|@8F^ED7X{m? zGxN0tPEAVJ&zbX!l;1M;zC?n@`*+dK_f~Hd3dj(E;1%%a)e1}BwR>sGr+E!%gwPIE z^DY6@3mu?b_R>hNz~PqAi~i5Hcv68gY1crK8mxq{q?M&Mi}ro(XI6y3Rs|oZ=D3u7vkRHcpvIO-x32MJq zWDw;%?Z{VN8cX;=lm2Zb!$A{+j8gA4cfyZ5~h%qkt_@pJK zV!L^dhIMy}jU%C;6c{Y98$O4ziAWT zA?X#`7MQaCFnR(>g2qk*2id`NoJ#^qYS;VZVOsd$eiMh$qbwrgz&!jj98!C_{Fp;vCr1TXQhpRtvo64zLdZ&^6(qNqR=Z5w*T*d69^C_Tx{JWpmE~H>LG>d#K4LU zm7Ggh2&8<;WPgFS8`M}diAMask?5=)wyakxbb#**F`i0mU{+uzQ)SG4*6sDZtWvLW z{IwTEI>(&6kOKRi*D`;LaTh30NhgjJK*t+rQXYoK;PI)TfI#FN(dDzrnuJeNWq&6B zmhz}$9qh*d0I@&cXMQ`5Eq`{x;kg`+iD*^;34p}Otw9xP*@*P9bR@u9uqq;k25uEI zJLx>Xtcq_Ebd$wtb}^@~-WPlI*Zf^}K+wQGsq*k3%|+ty(noOY5%GV-cJNIK5v#v> zxpf~{6Y4qaVDv3Dzq?%Yl-CP8(T#9^Hi}K2!08CLP!qsz5Di@VR}(iFu5dCuXF6FTn{Iom;)R(c!*`=|o3Lg&OAbthp z{g_SQiAk&;C37t3Ic7iD&K;_%8eB)ZDh81Gkc}hoQAR5=D|mDyMtH?!k6a0^SEfe9 z$7Oefr~YLWGyOV(4PU~l7>&o_s`=#bJI3A%N0t)Q%qqA~#aB}p5kv}&_ zgcdAKQW`=|D=K8@II|@jm}ly*bI75kB0=2``FTe&zS!){gqo>IwiNKBn-3GYo@b)ic?0@-l`6 z@7el1NXr8ALNI3sZjW%-%aB|_mHNlkEOT2Wz@Z0_8_XK#OZ`eedX%|367b-3thr|;?0DEq^>7`EHJlkc z$04YI8q_a?%SCKJ6n>)=k%8{6f5G&wCmBK9IMs#N#c^t2?E&w1XZ3~jxfZ%pm+oOw zj`1I9npzqcO`Jx~9qJA9LH*R|Ttd$`fBc8c`NKvEt&+_!J;I##%s(f_5wLX5`R9PIAksZj~zKjr59u4>`m z!Qh`dD3quIQe}pH0rby|?1z}V7e9YvQHzxww-3SoWDN-G1_(!=rfQ{g;fcI)8-($)?9V4DxM6b!HD6QpVrxkK(Jn_hn(4B2ao4-vaQ`A!8*8=-@6BL2Sy=Y_SiN!QfYm+AUN>3okl z#jd`XP{C7^bC2kYa(MDtq|D_xF34^ry4{`&-=|&Gn+QxQN>?b$;G8lniY5voV<_slQ1}Nz<15yxP8^#^y^w^EFGgmTiL<6MRg%p`0G}LMmrL1hR zPkSgY2IO84hO?ouyF0K>!gp(DnM4i1W0j!V6lM^Dks{*;JH>RF~tUu&Lsv*+Zp{)&@cYpZBDmI!cTWjZh#JJwsZQQCavL-tQwX$7~Wc zQh?usvM*HxXk|pG`Zge5p)s?+>FA* z*Q(GkNwwq*7hY1RoR|~`e~Lj}6~RGA8C;AzUnDe^eX%$SSU`d=hxJ_%Nr%Pe2=CZxqcZW{8Ciq9_>I z7o&$!p?j{a_v^oiXUfqt&PyTCDwRTm!+LuHfYXO5C3g@RS>><~a~dI&N|cw?sROK+ z?P*$f&%?_gJEVx(4}dOmp|zc;R2DS)9#Oj4shWx7S#bOH&%0j4SgMshFp|JN3c&F+0h zWlfJo;rNwE+&WYN-BjpbMj+wK0#&3(ay2Q05yR<#

hH{Bq);=G->C+VUA9R7hV0 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

I2YeX4{VB>lQ${f@nm{S8l| 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 K000000000b<`ne+ literal 0 HcmV?d00001 diff --git a/packages/examples/src/examples/aquarium/ExampleAquarium.tsx b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx index 6d0e775c0..02b02b57f 100644 --- a/packages/examples/src/examples/aquarium/ExampleAquarium.tsx +++ b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx @@ -3,25 +3,30 @@ * Copyright (C) 2011 - 2026 AltByte Pte Ltd — MIT License. * See `packages/examples/LICENSE.md` for full license + asset credits. * - * Fish swim across a planted tank; a full-screen water surface then refracts - * the LIVE scene rendered behind it. The surface renderable, drawn last, calls - * `renderer.toFrameTexture()` in its `draw()` to grab everything painted so far - * (background + fish + bubbles) as a GPU-resident `Texture2d`, hands it to a - * custom `ShaderEffect` as an extra sampler (`uScene`), and distorts it with a - * scrolling `NoiseTexture2d` flow map — so the fish shimmer as if seen through - * moving water, per-frame, with no `readPixels` round-trip. + * A user-contributed aquarium adapted to the 19.9 API: a seabed scene with + * swimming fish, seen through a rippling water surface. The surface renderable, + * drawn last, calls `renderer.toFrameTexture()` in its `draw()` to grab the + * frame rendered so far (seabed + fish) as a GPU-resident `Texture2d`, binds it + * to a custom `ShaderEffect` as an extra sampler (`uScene`), and refracts it + * through a scrolling `NoiseTexture2d` flow map — replacing the original's + * `readPixels` screen-capture with a zero-stall GPU copy. * * This is the industry-standard "screen texture" pattern (Godot * `hint_screen_texture` / Unity `_CameraOpaqueTexture` / Three.js - * `copyFramebufferToTexture`): capture the opaque scene, sample it from the - * refracting surface. `toFrameTexture()` returns the public `Texture2d`, so it - * plugs straight into `setTexture()` and is re-captured every frame into the - * same shared slot — the shader samples the latest frame with no re-bind. + * `copyFramebufferToTexture`). `toFrameTexture()` returns the public + * `Texture2d`, so it plugs straight into `setTexture()` and is re-captured every + * frame into the same shared slot — the shader samples the latest frame with no + * re-bind. + * + * Assets: TexturePacker atlas contributed with the original demo — a top-down + * seabed background, a 4-frame fish swim sheet, and a water texture. */ +import { DebugPanelPlugin } from "@melonjs/debug-plugin"; import { type Application, loader, NoiseTexture2d, + plugin, type ShaderEffect, Sprite, Stage, @@ -30,162 +35,65 @@ import { } from "melonjs"; import { createExampleComponent } from "../utils"; -// The refraction fragment (GLSL ES 1.00). It ignores the surface quad's own -// texture and re-samples the CAPTURED scene (uScene) in screen space, offset by -// a scrolling noise flow field — plus a caustic shimmer and a faint underwater -// tint. Sampling in screen space (gl_FragCoord / uResolution) matches the -// capture's coordinate system regardless of the quad's own UVs. +const base = `${import.meta.env.BASE_URL}assets/aquarium/`; + +// Atlas region rects (TexturePacker frames in aquarium.webp, 1024²). +const REGION = { + poolWater: { x: 1, y: 1, w: 740, h: 494 }, + seabed: { x: 1, y: 497, w: 720, h: 480 }, + swim: { x: 873, y: 228, w: 128, h: 128 }, // 2×2 grid of 64px frames +}; + +// crop a sub-region of the atlas image into its own canvas, optionally scaled +// to a target size (so a full-viewport sprite's framewidth matches its image +// exactly → clean [0,1] UVs) +const crop = ( + img: CanvasImageSource, + r: { x: number; y: number; w: number; h: number }, + dw = r.w, + dh = r.h, +) => { + const c = document.createElement("canvas"); + c.width = dw; + c.height = dh; + const ctx = c.getContext("2d") as CanvasRenderingContext2D; + ctx.drawImage(img, r.x, r.y, r.w, r.h, 0, 0, dw, dh); + return c; +}; + +// The refraction fragment (GLSL ES 1.00). The surface quad fills the viewport, +// so its UV (top-left origin) spans the screen. We re-sample the CAPTURED scene +// (uScene) at that UV, displaced by a scrolling noise flow field, then modulate +// it with the water texture (uSampler) for a wet, caustic sheen. The capture is +// a framebuffer copy (Y-up), so Y is flipped once into `s`. const WATER_FRAGMENT = ` -uniform sampler2D uScene; // the captured frame, bound each draw via setTexture +uniform sampler2D uScene; // captured frame, bound each draw via setTexture uniform sampler2D uNoise; // static seamless flow map uniform float uTime; // seconds (setTime) -uniform vec2 uResolution; // framebuffer size in pixels uniform float uStrength; // ripple strength (slider) vec4 apply(vec4 color, vec2 uv) { - vec2 s = gl_FragCoord.xy / uResolution; + vec2 s = vec2(uv.x, 1.0 - uv.y); - // two noise layers scrolling in different directions → a living flow field + // two noise layers scrolling apart → a living flow field vec2 f1 = texture2D(uNoise, s * 1.6 + vec2(uTime * 0.03, uTime * 0.05)).rg; vec2 f2 = texture2D(uNoise, s * 2.7 - vec2(uTime * 0.04, uTime * 0.02)).rg; - vec2 flow = (f1 + f2 - 1.0); + vec2 flow = f1 + f2 - 1.0; - // refract the captured scene: sample it at the displaced screen coord - vec2 rUV = clamp(s + flow * uStrength, 0.0, 1.0); - vec3 scene = texture2D(uScene, rUV).rgb; + // refract the captured scene at the displaced screen coord + vec3 scene = texture2D(uScene, clamp(s + flow * uStrength, 0.0, 1.0)).rgb; - // caustic sparkle where the flow layers pinch together, brighter up top - float caustic = pow(max(f1.r * f2.g, 0.0), 3.0) * (1.0 - s.y) * 1.5; - scene += vec3(0.10, 0.22, 0.26) * caustic; + // the water texture (uSampler), gently scrolled, as a wet sheen over it + vec3 water = texture2D(uSampler, uv * 0.6 + flow * 0.02).rgb; + vec3 outc = scene * (0.75 + 0.5 * water); - // gentle underwater colour grade - scene = mix(scene, scene * vec3(0.82, 0.96, 1.06), 0.35); - return vec4(scene, 1.0); + // caustic sparkle where the flow layers pinch together + float caustic = pow(max(f1.r * f2.g, 0.0), 3.0) * 1.2; + outc += vec3(0.10, 0.20, 0.24) * caustic; + return vec4(outc, 1.0); } `; -type Rgb = [number, number, number]; - -// paint a small side-on fish: an elliptical body, a tail fin and an eye -const fishCanvas = (w: number, h: number, body: Rgb, fin: Rgb) => { - const canvas = document.createElement("canvas"); - canvas.width = w; - canvas.height = h; - const ctx = canvas.getContext("2d") as CanvasRenderingContext2D; - const [br, bg, bb] = body; - const [fr, fg, fb] = fin; - - // tail fin (triangle at the back / left) - ctx.fillStyle = `rgb(${fr}, ${fg}, ${fb})`; - ctx.beginPath(); - ctx.moveTo(2, h / 2); - ctx.lineTo(w * 0.34, h * 0.2); - ctx.lineTo(w * 0.34, h * 0.8); - ctx.closePath(); - ctx.fill(); - - // body (ellipse), facing right - ctx.fillStyle = `rgb(${br}, ${bg}, ${bb})`; - ctx.beginPath(); - ctx.ellipse(w * 0.6, h / 2, w * 0.34, h * 0.32, 0, 0, Math.PI * 2); - ctx.fill(); - - // top fin - ctx.fillStyle = `rgba(${fr}, ${fg}, ${fb}, 0.9)`; - ctx.beginPath(); - ctx.moveTo(w * 0.5, h * 0.2); - ctx.lineTo(w * 0.72, h * 0.06); - ctx.lineTo(w * 0.72, h * 0.42); - ctx.closePath(); - ctx.fill(); - - // belly highlight - ctx.fillStyle = "rgba(255, 255, 255, 0.18)"; - ctx.beginPath(); - ctx.ellipse(w * 0.62, h * 0.62, w * 0.24, h * 0.12, 0, 0, Math.PI * 2); - ctx.fill(); - - // eye - ctx.fillStyle = "#fff"; - ctx.beginPath(); - ctx.arc(w * 0.82, h * 0.42, Math.max(2, w * 0.05), 0, Math.PI * 2); - ctx.fill(); - ctx.fillStyle = "#111"; - ctx.beginPath(); - ctx.arc(w * 0.84, h * 0.42, Math.max(1, w * 0.025), 0, Math.PI * 2); - ctx.fill(); - return canvas; -}; - -// the tank backdrop: a vertical water gradient, a sandy bottom, and a few -// simple plant fronds — a static, opaque scene for the surface to refract -const tankCanvas = (w: number, h: number) => { - const canvas = document.createElement("canvas"); - canvas.width = w; - canvas.height = h; - const ctx = canvas.getContext("2d") as CanvasRenderingContext2D; - - const grad = ctx.createLinearGradient(0, 0, 0, h); - grad.addColorStop(0, "#0a6b86"); - grad.addColorStop(0.6, "#064b63"); - grad.addColorStop(1, "#04303f"); - ctx.fillStyle = grad; - ctx.fillRect(0, 0, w, h); - - // soft light shafts from the top - ctx.globalAlpha = 0.08; - ctx.fillStyle = "#bfefff"; - for (let i = 0; i < 5; i++) { - const x = (i + 0.5) * (w / 5); - ctx.beginPath(); - ctx.moveTo(x - 10, 0); - ctx.lineTo(x + 10, 0); - ctx.lineTo(x + 60, h); - ctx.lineTo(x + 30, h); - ctx.closePath(); - ctx.fill(); - } - ctx.globalAlpha = 1; - - // plants (wavy fronds) rooted in the sand - const plant = (px: number, ph: number, hue: string) => { - ctx.strokeStyle = hue; - ctx.lineWidth = 5; - ctx.lineCap = "round"; - for (let b = -1; b <= 1; b++) { - ctx.beginPath(); - ctx.moveTo(px, h - 24); - ctx.quadraticCurveTo( - px + b * 22 + 14, - h - 24 - ph * 0.5, - px + b * 10, - h - 24 - ph, - ); - ctx.stroke(); - } - }; - plant(w * 0.12, h * 0.42, "#0c8a4a"); - plant(w * 0.2, h * 0.3, "#0aa15a"); - plant(w * 0.8, h * 0.5, "#0c8a4a"); - plant(w * 0.9, h * 0.34, "#0aa15a"); - - // sandy bottom - const sand = ctx.createLinearGradient(0, h - 28, 0, h); - sand.addColorStop(0, "#c9a86a"); - sand.addColorStop(1, "#a8874c"); - ctx.fillStyle = sand; - ctx.beginPath(); - ctx.moveTo(0, h - 20); - for (let x = 0; x <= w; x += 24) { - ctx.lineTo(x, h - 20 - Math.sin(x * 0.05) * 4); - } - ctx.lineTo(w, h); - ctx.lineTo(0, h); - ctx.closePath(); - ctx.fill(); - return canvas; -}; - // a fish that swims horizontally and turns around at the tank edges class Fish extends Sprite { private speed: number; @@ -198,18 +106,23 @@ class Fish extends Sprite { constructor( x: number, y: number, - image: HTMLCanvasElement, + sheet: HTMLCanvasElement, speed: number, + scale: number, bounds: { min: number; max: number }, ) { - super(x, y, { image, framewidth: image.width, frameheight: image.height }); + super(x, y, { image: sheet, framewidth: 64, frameheight: 64 }); + this.addAnimation("swim", [0, 1, 2, 3], 120); + this.setCurrentAnimation("swim"); + this.scale(scale, scale); this.speed = speed; this.minX = bounds.min; this.maxX = bounds.max; this.baseY = y; this.bobPhase = x * 0.05; - this.bobAmp = 6 + Math.abs(speed) * 0.2; - this.flipX(speed < 0); + this.bobAmp = 5 + Math.abs(speed) * 0.15; + // art faces left; flip when swimming right + this.flipX(speed > 0); } update(dt: number) { @@ -221,46 +134,37 @@ class Fish extends Sprite { if (this.pos.x < this.minX) { this.pos.x = this.minX; this.speed = Math.abs(this.speed); - this.flipX(false); + this.flipX(true); } else if (this.pos.x > this.maxX) { this.pos.x = this.maxX; this.speed = -Math.abs(this.speed); - this.flipX(true); + this.flipX(false); } return true; } } // the water surface: drawn LAST, it captures the frame rendered so far and -// re-draws it refracted. A full-viewport renderable whose draw() grabs the -// backdrop via toFrameTexture(), binds it to the shader (uScene), and blits a -// full-screen quad through that shader. +// re-draws it refracted. A full-viewport sprite (framewidth/frameheight = the +// viewport, so it covers every pixel) whose draw() grabs the backdrop via +// toFrameTexture() and hands it to the shader as uScene. class WaterSurface extends Sprite { private effect: ShaderEffect; constructor( - x: number, - y: number, w: number, h: number, + waterTex: HTMLCanvasElement, effect: ShaderEffect, ) { - // a 1x1 white image stretched to the viewport — the shader ignores it - // (it samples the captured scene instead), it just supplies the quad - const white = document.createElement("canvas"); - white.width = 1; - white.height = 1; - const ctx = white.getContext("2d") as CanvasRenderingContext2D; - ctx.fillStyle = "#fff"; - ctx.fillRect(0, 0, 1, 1); - super(x, y, { - image: white, - framewidth: 1, - frameheight: 1, + // the water texture stretched over the whole viewport is the quad's + // albedo (uSampler); the shader multiplies the captured scene by it + super(w / 2, h / 2, { + image: waterTex, + framewidth: w, + frameheight: h, anchorPoint: { x: 0.5, y: 0.5 }, }); - // stretch the 1x1 quad to fill the whole viewport - this.scale(w, h); this.effect = effect; this.shader = effect; } @@ -286,6 +190,13 @@ class PlayScreen extends Stage { const w = app.viewport.width; const h = app.viewport.height; + const atlas = loader.getImage("aquariumAtlas") as HTMLImageElement; + // seabed + water pre-scaled to the viewport so a full-screen sprite's + // framewidth/frameheight matches its image (clean UVs, exact cover) + const seabed = crop(atlas, REGION.seabed, w, h); + const water = crop(atlas, REGION.poolWater, w, h); + const swimSheet = crop(atlas, REGION.swim); // natural 128×128 (2×2 of 64) + // static seamless noise flow map (baked once; scrolled on the GPU) const noise = new NoiseTexture2d({ width: 256, @@ -303,41 +214,32 @@ class PlayScreen extends Stage { // the refraction effect, preloaded as a "shader" asset this.effect = loader.getShader("aquariumWater") as ShaderEffect; this.effect.setTexture("uNoise", noise.getTexture(), "repeat"); - this.effect.setUniform("uResolution", new Float32Array([w, h])); this.effect.setUniform("uStrength", 0.02); - // backdrop (z 0) - const tank = new Sprite(w / 2, h / 2, { - image: tankCanvas(w, h), + // seabed backdrop (z 0), stretched to the viewport + const bg = new Sprite(w / 2, h / 2, { + image: seabed, framewidth: w, frameheight: h, anchorPoint: { x: 0.5, y: 0.5 }, }); - app.world.addChild(tank, 0); + app.world.addChild(bg, 0); // a school of fish (z 1..) swimming at various depths and speeds - const palette: Array<{ body: Rgb; fin: Rgb; size: number }> = [ - { body: [244, 148, 42], fin: [214, 92, 20], size: 54 }, // clownfish - { body: [86, 170, 220], fin: [40, 120, 190], size: 44 }, // blue tang - { body: [230, 210, 90], fin: [200, 160, 40], size: 40 }, // yellow - { body: [220, 90, 120], fin: [170, 50, 90], size: 48 }, // pink - { body: [150, 210, 150], fin: [90, 160, 100], size: 36 }, // green - ]; - for (let i = 0; i < 7; i++) { - const p = palette[i % palette.length]; - const img = fishCanvas(p.size, p.size * 0.66, p.body, p.fin); + for (let i = 0; i < 6; i++) { const dir = i % 2 === 0 ? 1 : -1; - const speed = dir * (36 + (i % 3) * 22); - const y = 70 + ((i * 47) % (h - 150)); - const x = 60 + ((i * 97) % (w - 120)); + const speed = dir * (34 + (i % 3) * 20); + const scale = 0.7 + (i % 3) * 0.25; + const y = 60 + ((i * 53) % (h - 130)); + const x = 60 + ((i * 101) % (w - 120)); app.world.addChild( - new Fish(x, y, img, speed, { min: 40, max: w - 40 }), + new Fish(x, y, swimSheet, speed, scale, { min: 40, max: w - 40 }), 1 + i, ); } // the water surface post-pass (z 100, above everything it refracts) - app.world.addChild(new WaterSurface(w / 2, h / 2, w, h, this.effect), 100); + app.world.addChild(new WaterSurface(w, h, water, this.effect), 100); this.buildSlider(app); } @@ -390,17 +292,25 @@ class PlayScreen extends Stage { const createGame = () => { video.init(728, 410, { parent: "screen", - scaleMethod: "flex", + // fixed internal resolution scaled to fit (keeps viewport 728×410, so + // full-screen renderables cover it regardless of the container size) + scale: "auto", // toFrameTexture + ShaderEffect are WebGL features renderer: video.WEBGL, antiAlias: true, }); + // register the debug plugin and open it by default (toggle with the S key) + plugin.register(DebugPanelPlugin, "debugPanel"); + (plugin.get("debugPanel") as { show?: () => void })?.show?.(); + state.set(state.PLAY, new PlayScreen()); - // preload the refraction shader (compiled once at load time) loader.preload( - [{ name: "aquariumWater", type: "shader", data: WATER_FRAGMENT }], + [ + { name: "aquariumAtlas", type: "image", src: `${base}aquarium.webp` }, + { name: "aquariumWater", type: "shader", data: WATER_FRAGMENT }, + ], () => { state.change(state.PLAY); }, diff --git a/packages/examples/src/main.tsx b/packages/examples/src/main.tsx index 283f667ae..94c924557 100644 --- a/packages/examples/src/main.tsx +++ b/packages/examples/src/main.tsx @@ -367,7 +367,7 @@ const examples: { path: "aquarium", sourceDir: "aquarium", description: - "Screen-space water refraction via renderer.toFrameTexture(): fish swim across a planted tank, then a full-screen water surface captures the live frame on the GPU and re-samples it through a scrolling NoiseTexture2d flow map — the fish shimmer as if seen through moving water, with no readPixels round-trip. The industry-standard screen-texture pattern (Godot hint_screen_texture / Unity _CameraOpaqueTexture).", + "Screen-space refraction via renderer.toFrameTexture(): swimming fish rippled through the live scene, captured on the GPU and distorted by a scrolling NoiseTexture2d.", }, { component: , From 238b00d0a4d940a10294b39127017c1e7e6f88bc Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Wed, 8 Jul 2026 22:36:42 +0800 Subject: [PATCH 09/20] refactor(toFrameTexture): address PR #1543 review round 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review on the toFrameTexture addition: - Per-frame reallocation: copyTexImage2D ran every capture, re-specifying the texture storage each frame. Now copyTexImage2D allocates on the FIRST capture (or on a size change, which nulls the slot), then copyTexSubImage2D refreshes in place — no per-frame realloc. Verified the steady-state copyTexSubImage2D path still samples on ANGLE Metal (fish animate through the live capture). - GPUResidentTexture wasn't actually opaque: the optional `never` brand let any object satisfy it, defeating Texture2dSource's type safety. Rebranded with a `unique symbol` (a real nominal brand) — only engine-provided values match. - CanvasRenderer.toFrameTexture with `target` returned a DIFFERENT instance on a size change; now it resizes the same capture in place, matching the WebGL object-identity contract. Canvas JSDoc documents `target: null` (mint). - Doc/comment drift from the blit→copyTexImage2D switch: WebGL JSDoc (copyTexSubImage2D mention, "Color only (RGBA)" → RGB/opaque), texture2d.ts ("a future toFrameTexture" → implemented), and the spec header all corrected. - Aquarium example passes the NoiseTexture2d asset straight to setTexture (19.9 API) instead of noise.getTexture(). Full suite 4753 green, lint 0, build 8/8. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- .../src/examples/aquarium/ExampleAquarium.tsx | 3 +- .../src/video/canvas/canvas_renderer.js | 20 +++++--- .../melonjs/src/video/texture/texture2d.ts | 22 ++++++--- .../melonjs/src/video/webgl/webgl_renderer.js | 46 +++++++++++-------- packages/melonjs/tests/toframetexture.spec.js | 16 ++++--- 5 files changed, 65 insertions(+), 42 deletions(-) diff --git a/packages/examples/src/examples/aquarium/ExampleAquarium.tsx b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx index 02b02b57f..b36f506dd 100644 --- a/packages/examples/src/examples/aquarium/ExampleAquarium.tsx +++ b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx @@ -213,7 +213,8 @@ class PlayScreen extends Stage { // the refraction effect, preloaded as a "shader" asset this.effect = loader.getShader("aquariumWater") as ShaderEffect; - this.effect.setTexture("uNoise", noise.getTexture(), "repeat"); + // pass the NoiseTexture2d asset directly — setTexture resolves it (19.9) + this.effect.setTexture("uNoise", noise, "repeat"); this.effect.setUniform("uStrength", 0.02); // seabed backdrop (z 0), stretched to the viewport diff --git a/packages/melonjs/src/video/canvas/canvas_renderer.js b/packages/melonjs/src/video/canvas/canvas_renderer.js index d81ec8236..1fa8c65bb 100644 --- a/packages/melonjs/src/video/canvas/canvas_renderer.js +++ b/packages/melonjs/src/video/canvas/canvas_renderer.js @@ -97,8 +97,10 @@ export default class CanvasRenderer extends Renderer { * Canvas renderer, so the result is a plain drawable for CPU-side reuse * rather than a live shader input. * @param {object} [options] - * @param {Texture2d} [options.target] - a capture previously returned here to - * refresh in place, instead of the shared renderer slot + * @param {Texture2d|null} [options.target] - controls the destination: omit + * for the shared renderer slot (default); pass a capture previously + * returned by this method to refresh it in place; pass `null` to mint a + * fresh, caller-owned capture (mirrors {@link WebGLRenderer#toFrameTexture}) * @param {Rect|Bounds|{x: number, y: number, width: number, height: number}} [options.region] - capture * only this sub-region (canvas pixels, top-left origin) * @returns {Texture2d} a texture holding the captured frame @@ -127,11 +129,8 @@ export default class CanvasRenderer extends Renderer { : options.target === null ? undefined : options.target; - if ( - typeof frame === "undefined" || - frame.width !== w || - frame.height !== h - ) { + if (typeof frame === "undefined") { + // mint a fresh capture (empty shared slot, or target: null) const canvas = globalThis.document ? globalThis.document.createElement("canvas") : new OffscreenCanvas(w, h); @@ -141,6 +140,13 @@ export default class CanvasRenderer extends Renderer { if (shared) { this._frameTexture = frame; } + } else if (frame.width !== w || frame.height !== h) { + // resize the SAME capture in place (keep the object identity, like + // the WebGL variant) rather than returning a different instance + frame.canvas.width = w; + frame.canvas.height = h; + frame.width = w; + frame.height = h; } const ctx = frame.canvas.getContext("2d"); diff --git a/packages/melonjs/src/video/texture/texture2d.ts b/packages/melonjs/src/video/texture/texture2d.ts index 3b300db9f..89ec29511 100644 --- a/packages/melonjs/src/video/texture/texture2d.ts +++ b/packages/melonjs/src/video/texture/texture2d.ts @@ -1,20 +1,28 @@ +/** + * Compile-time-only brand key; no runtime value can hold it. + * @ignore + */ +declare const gpuResidentBrand: unique symbol; + /** * An opaque, GPU-resident texture backing — the source a GPU-resident - * {@link Texture2d} resolves to (for example the framebuffer copy a future - * `renderer.toFrameTexture()` produces). Such a backing uploads/binds itself - * through the texture cache instead of being read back to the CPU. Its + * {@link Texture2d} resolves to (for example the framebuffer copy + * {@link WebGLRenderer#toFrameTexture} produces). Such a backing uploads/binds + * itself through the texture cache instead of being read back to the CPU. Its * concrete shape is an engine internal that may change between releases: * obtain values from the engine and pass them back to the engine — do not * construct or inspect them. + * + * Nominally branded with a `unique symbol`, so only engine-provided values + * satisfy the type — a plain object is NOT assignable, keeping + * {@link Texture2dSource} (and {@link Texture2d#getTexture}) honestly typed. */ export interface GPUResidentTexture { /** - * Structural brand only — GPU-resident backings are opaque and expose no - * public runtime member. Present so this stays distinct from a plain - * object without naming any internal class in the public type surface. + * Phantom nominal brand — never present at runtime. * @ignore */ - readonly __gpuResidentTexture?: never; + readonly [gpuResidentBrand]: true; } /** diff --git a/packages/melonjs/src/video/webgl/webgl_renderer.js b/packages/melonjs/src/video/webgl/webgl_renderer.js index b9c2b8fb9..8979c6c45 100644 --- a/packages/melonjs/src/video/webgl/webgl_renderer.js +++ b/packages/melonjs/src/video/webgl/webgl_renderer.js @@ -814,9 +814,10 @@ export default class WebGLRenderer extends Renderer { /** * Capture the current frame — everything drawn to the active framebuffer so - * far — into a {@link Texture2d}, entirely on the GPU (a single - * `copyTexSubImage2D`, no `readPixels` round-trip or pipeline stall). The - * fourth member of the {@link Renderer#toDataURL} / {@link Renderer#toBlob} / + * far — into a {@link Texture2d}, entirely on the GPU (a `copyTexImage2D` on + * the first capture, then `copyTexSubImage2D` to refresh in place; no + * `readPixels` round-trip or pipeline stall). The fourth member of the + * {@link Renderer#toDataURL} / {@link Renderer#toBlob} / * {@link Renderer#toImageBitmap} family — "the current frame as X" — and the * only one whose result never leaves the GPU. * @@ -838,10 +839,11 @@ export default class WebGLRenderer extends Renderer { * each frame into the shared slot refreshes what the shader samples without * any re-bind. * - * Color only (RGBA); the framebuffer's depth is not captured. By default the - * result is a shared, renderer-owned texture valid until the next - * parameterless call — pass `options.target` for an independent, caller-owned - * capture (e.g. two captures alive in the same frame). + * Color only, captured into an RGB texture (opaque — samples with alpha = 1); + * the framebuffer's depth is not captured. By default the result is a shared, + * renderer-owned texture valid until the next parameterless call — pass + * `options.target` for an independent, caller-owned capture (e.g. two + * captures alive in the same frame). * @param {object} [options] * @param {Texture2d|null} [options.target] - controls the destination: omit * for the shared renderer slot (default); pass a capture previously @@ -917,31 +919,35 @@ export default class WebGLRenderer extends Renderer { } } - // Copy the bound framebuffer into the capture texture with - // copyTexImage2D — the standard framebuffer→texture path (Three.js - // copyFramebufferToTexture uses it). An RGB internalformat is a valid - // subset of BOTH an alpha-less default framebuffer (melonJS creates the - // context with `alpha: transparent`, usually false) AND an RGBA camera - // FBO, so the copy never trips INVALID_OPERATION on a format mismatch, - // and the capture samples as an opaque backdrop (alpha = 1). Unlike a - // blitFramebuffer destination — which some drivers won't sample in the - // same frame — a copyTexImage2D texture samples reliably everywhere. + // Copy the bound framebuffer into the capture texture — the standard + // framebuffer→texture path (Three.js copyFramebufferToTexture uses it). + // An RGB internalformat is a valid subset of BOTH an alpha-less default + // framebuffer (melonJS creates the context with `alpha: transparent`, + // usually false) AND an RGBA camera FBO, so the copy never trips + // INVALID_OPERATION on a format mismatch, and the capture samples as an + // opaque backdrop (alpha = 1). Unlike a blitFramebuffer destination — + // which some drivers won't sample in the same frame — an RGB texture + // copied this way samples reliably everywhere. const batcher = this.setBatcher("quad"); const unit = batcher.maxBatchTextures - 1; if (frame.glTexture === null) { + // first capture into this slot: copyTexImage2D allocates the RGB + // storage AND copies in one call frame.glTexture = gl.createTexture(); - // bind through the batcher so boundTextures stays in lockstep; the - // sampler params only need setting once (copyTexImage2D re-specifies - // the storage, not the parameters) batcher.bindTexture2D(frame.glTexture, unit, false); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGB, x, y, w, h, 0); } else { + // steady state: refresh the existing storage in place — no per-frame + // reallocation. Same size is guaranteed (a size change nulls + // glTexture above via the shared-slot realloc), so copyTexSubImage2D + // is valid and copies RGB from either an RGB or RGBA source. batcher.bindTexture2D(frame.glTexture, unit, false); + gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, x, y, w, h); } - gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGB, x, y, w, h, 0); return frame; } diff --git a/packages/melonjs/tests/toframetexture.spec.js b/packages/melonjs/tests/toframetexture.spec.js index c3017cb7b..5678ad0da 100644 --- a/packages/melonjs/tests/toframetexture.spec.js +++ b/packages/melonjs/tests/toframetexture.spec.js @@ -10,17 +10,19 @@ import { /** * `renderer.toFrameTexture()` captures the current framebuffer into a - * {@link Texture2d} entirely on the GPU (WebGL2 blitFramebuffer / WebGL1 - * copyTexImage2D — no readPixels round-trip), for screen-space effects (water - * refraction, heat haze, glass). The fourth member of the toDataURL / toBlob / - * toImageBitmap family; the only one that never leaves the GPU. Ticket #1544. + * {@link Texture2d} entirely on the GPU (copyTexImage2D to allocate, then + * copyTexSubImage2D to refresh — no readPixels round-trip), for screen-space + * effects (water refraction, heat haze, glass). The fourth member of the + * toDataURL / toBlob / toImageBitmap family; the only one that never leaves the + * GPU. Ticket #1544. * * Capture CONTENT is verified by attaching the returned texture to a scratch * framebuffer and reading it back. The live-bind SAMPLING path (a shader * reading the capture) is verified separately with a normally-uploaded texture: - * the headless software rasterizer used in CI cannot *sample* a blit-destination - * texture (it captures + reads back fine, and real GPUs sample it fine), so the - * two concerns are checked independently. + * the headless software rasterizer used in CI does not reliably *sample* the + * RGB capture texture (it captures + reads back fine, and real GPUs sample it + * fine — see the Aquarium example), so the two concerns are checked + * independently. */ const SIZE = 32; From ce66be30544dcb3b82ad9712c51d527396a82dad Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Thu, 9 Jul 2026 07:33:38 +0800 Subject: [PATCH 10/20] refactor(toFrameTexture): address PR #1543 review round 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review: - CanvasRenderer.toFrameTexture allocated its backing canvas via a manual document/OffscreenCanvas branch, bypassing the engine's capability gating. Now routes through Renderer.createCanvas(), and guards a null 2D context with a clear error (public-API surface). - WebGLRenderer.toFrameTexture now validates a non-null `target` is a FrameTexture from this method — a foreign Texture2d (no glTexture) would mis-drive the isTexture/reallocation logic; throws a clear error instead. - Aquarium example: debug panel is now registered hidden (toggle with S), matching every other example, instead of opened by default (maintainer decision). Full suite 4753 green, lint 0, build 8/8. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- .../src/examples/aquarium/ExampleAquarium.tsx | 3 +-- .../melonjs/src/video/canvas/canvas_renderer.js | 16 +++++++++------- .../melonjs/src/video/webgl/webgl_renderer.js | 13 +++++++++++++ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/examples/src/examples/aquarium/ExampleAquarium.tsx b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx index b36f506dd..52111b168 100644 --- a/packages/examples/src/examples/aquarium/ExampleAquarium.tsx +++ b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx @@ -301,9 +301,8 @@ const createGame = () => { antiAlias: true, }); - // register the debug plugin and open it by default (toggle with the S key) + // register the debug plugin (hidden by default; press S to toggle) plugin.register(DebugPanelPlugin, "debugPanel"); - (plugin.get("debugPanel") as { show?: () => void })?.show?.(); state.set(state.PLAY, new PlayScreen()); diff --git a/packages/melonjs/src/video/canvas/canvas_renderer.js b/packages/melonjs/src/video/canvas/canvas_renderer.js index 1fa8c65bb..cf4547fab 100644 --- a/packages/melonjs/src/video/canvas/canvas_renderer.js +++ b/packages/melonjs/src/video/canvas/canvas_renderer.js @@ -130,13 +130,10 @@ export default class CanvasRenderer extends Renderer { ? undefined : options.target; if (typeof frame === "undefined") { - // mint a fresh capture (empty shared slot, or target: null) - const canvas = globalThis.document - ? globalThis.document.createElement("canvas") - : new OffscreenCanvas(w, h); - canvas.width = w; - canvas.height = h; - frame = new CanvasFrameTexture(canvas); + // mint a fresh capture (empty shared slot, or target: null). Route + // through the engine's canvas factory so it honors the same + // OffscreenCanvas capability gating as everything else. + frame = new CanvasFrameTexture(Renderer.createCanvas(w, h)); if (shared) { this._frameTexture = frame; } @@ -150,6 +147,11 @@ export default class CanvasRenderer extends Renderer { } const ctx = frame.canvas.getContext("2d"); + if (ctx === null) { + throw new Error( + "CanvasRenderer.toFrameTexture: 2D context unavailable on the capture canvas", + ); + } ctx.clearRect(0, 0, w, h); ctx.drawImage(src, x, y, w, h, 0, 0, w, h); return frame; diff --git a/packages/melonjs/src/video/webgl/webgl_renderer.js b/packages/melonjs/src/video/webgl/webgl_renderer.js index 8979c6c45..e0b87074f 100644 --- a/packages/melonjs/src/video/webgl/webgl_renderer.js +++ b/packages/melonjs/src/video/webgl/webgl_renderer.js @@ -885,6 +885,19 @@ export default class WebGLRenderer extends Renderer { // flush pending geometry so the capture holds everything drawn so far this.flush(); + // a non-null `target` must be a capture this method previously returned + // (a FrameTexture) — any other Texture2d has no `glTexture` and would + // mis-drive the isTexture/reallocation logic below + if ( + typeof options.target !== "undefined" && + options.target !== null && + !(options.target instanceof FrameTexture) + ) { + throw new Error( + "WebGLRenderer.toFrameTexture: `target` must be a capture returned by this method", + ); + } + // destination: // - no `target` → the shared, renderer-owned slot (default; // overwritten by the next parameterless call) From c3b71c1192343d716e2247f21e806b6f979201e2 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Thu, 9 Jul 2026 07:36:43 +0800 Subject: [PATCH 11/20] fix(examples): smooth fish movement in aquarium (subPixel rendering) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fish swim at ~0.5–1.2 px/frame; with the default subPixel:false the renderer floors dx/dy to integers, so slow motion snapped pixel-to-pixel and looked choppy. Enable subPixel:true so fractional positions render smoothly. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- packages/examples/src/examples/aquarium/ExampleAquarium.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/examples/src/examples/aquarium/ExampleAquarium.tsx b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx index 52111b168..8e689bbc9 100644 --- a/packages/examples/src/examples/aquarium/ExampleAquarium.tsx +++ b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx @@ -299,6 +299,9 @@ const createGame = () => { // toFrameTexture + ShaderEffect are WebGL features renderer: video.WEBGL, antiAlias: true, + // render at sub-pixel positions so the slow-swimming fish glide smoothly + // instead of snapping pixel-to-pixel (default floors dx/dy to integers) + subPixel: true, }); // register the debug plugin (hidden by default; press S to toggle) From 36d70e5efe999f70387ba806d7893868b9cae4e4 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Thu, 9 Jul 2026 07:37:43 +0800 Subject: [PATCH 12/20] fix(examples): soften the default water ripple strength in aquarium MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lower uStrength default (and the slider's initial value) 0.02 → 0.013 so the refraction is subtler on load; the slider still reaches the stronger setting. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- packages/examples/src/examples/aquarium/ExampleAquarium.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/examples/src/examples/aquarium/ExampleAquarium.tsx b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx index 8e689bbc9..b45ac378f 100644 --- a/packages/examples/src/examples/aquarium/ExampleAquarium.tsx +++ b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx @@ -215,7 +215,7 @@ class PlayScreen extends Stage { this.effect = loader.getShader("aquariumWater") as ShaderEffect; // pass the NoiseTexture2d asset directly — setTexture resolves it (19.9) this.effect.setTexture("uNoise", noise, "repeat"); - this.effect.setUniform("uStrength", 0.02); + this.effect.setUniform("uStrength", 0.013); // seabed backdrop (z 0), stretched to the viewport const bg = new Sprite(w / 2, h / 2, { @@ -258,7 +258,7 @@ class PlayScreen extends Stage { slider.min = "0"; slider.max = "0.06"; slider.step = "0.002"; - slider.value = "0.02"; + slider.value = "0.013"; slider.style.cssText = "width:190px;display:block;"; slider.addEventListener("input", () => { this.effect.setUniform("uStrength", Number.parseFloat(slider.value)); From 0f190bda2f5c7d6a3e0fd9fde13d38b1a2c61296 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Thu, 9 Jul 2026 07:48:38 +0800 Subject: [PATCH 13/20] refactor(renderer): declare toFrameTexture on the base Renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toFrameTexture was implemented on both CanvasRenderer and WebGLRenderer but absent from the base Renderer — unlike the rest of the frame-capture family (toDataURL / toBlob / toImageBitmap, all on the base). Add it as a documented abstract method that throws "not implemented by this renderer", so the family is complete and discoverable on the Renderer contract (and a future backend that forgets to implement it fails loudly instead of silently missing the API). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- packages/melonjs/src/video/renderer.js | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/melonjs/src/video/renderer.js b/packages/melonjs/src/video/renderer.js index a37265fea..56267fb45 100644 --- a/packages/melonjs/src/video/renderer.js +++ b/packages/melonjs/src/video/renderer.js @@ -19,6 +19,7 @@ import CanvasRenderTarget from "./rendertarget/canvasrendertarget.js"; * @import {Line} from "./../geometries/line.ts"; * @import {Ellipse} from "./../geometries/ellipse.ts"; * @import {Bounds} from "./../physics/bounds.ts"; + * @import {default as Texture2d} from "./texture/texture2d.ts"; */ /** @@ -1117,6 +1118,36 @@ export default class Renderer { toDataURL(type = "image/png", quality) { return this.renderTarget.toDataURL(type, quality); } + + /** + * Capture the current frame — everything drawn to the active framebuffer / + * canvas so far — into a {@link Texture2d}, on the GPU where possible (no + * `readPixels` round-trip). The fourth member of the {@link Renderer#toDataURL} + * / {@link Renderer#toBlob} / {@link Renderer#toImageBitmap} family — "the + * current frame as X" — and the only one whose result can stay GPU-resident, + * ready to feed a shader as a screen texture (water refraction, heat haze, + * frosted glass). + * + * Abstract on the base renderer: the concrete backends implement it — + * {@link WebGLRenderer#toFrameTexture} (a `copyTexImage2D` into a live GPU + * texture, bound as an extra sampler via {@link ShaderEffect#setTexture}) and + * {@link CanvasRenderer#toFrameTexture} (an offscreen-canvas copy, for family + * parity — custom shaders don't run under Canvas). + * @param {object} [options] + * @param {Texture2d|null} [options.target] - controls the destination: omit + * for the shared renderer slot (default); pass a capture previously + * returned by this method to refresh it in place; pass `null` to mint a + * fresh, caller-owned capture + * @param {Rect|Bounds|{x: number, y: number, width: number, height: number}} [options.region] - capture + * only this sub-region of the frame + * @returns {Texture2d} a texture holding the captured frame + * @example + * effect.setTexture("uScene", renderer.toFrameTexture()); + */ + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + toFrameTexture(options) { + throw new Error("toFrameTexture() is not implemented by this renderer"); + } } // Backing field for `Renderer.getWhitePixel()` — declared outside the From 4e9a2c21c0be0a7f3079d0846d8843a309aa6c4d Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Thu, 9 Jul 2026 09:18:24 +0800 Subject: [PATCH 14/20] fix(webgl): toFrameTexture must not corrupt lit-batcher normal-map units MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lit quad batcher parks normal maps in the TOP half of the texture-unit range; toFrameTexture binds its copy to a scratch unit at the top of the quad batcher's range — the same units. That scratch bind goes DIRECTLY through the batcher (not the shared texture cache), so other batchers' unit caches don't learn it changed — a later lit draw could assume its normal map is still resident, skip re-binding, and sample the captured frame AS a normal map. Aligns with how Three.js/Pixi handle off-path binds (invalidate the cache), finer-grained than their full resetState()/reset() since toFrameTexture runs every frame: - new MaterialBatcher.invalidateUnit(unit); LitQuadBatcher overrides to also drop the paired normal-map slot. - toFrameTexture invalidates the scratch unit across every batcher after copy. - also fixes a PRE-EXISTING latent gap surfaced here: the base cache-reset handler (unit-pool-wrap path) cleared colors but not the lit normal-map cache. Refactored into an overridable _onTextureCacheReset; LitQuadBatcher clears normals too. Adversarial tests (3, red without the fix). New Heat Haze example: normal-mapped tiles under a moving Light2d distorted by a toFrameTexture heat-haze — the lit-scene + capture scenario the fix protects. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- .../src/examples/heatHaze/ExampleHeatHaze.tsx | 235 ++++++++++++++++++ packages/examples/src/main.tsx | 13 + .../video/webgl/batchers/lit_quad_batcher.js | 34 +++ .../video/webgl/batchers/material_batcher.js | 39 ++- .../melonjs/src/video/webgl/webgl_renderer.js | 10 + packages/melonjs/tests/toframetexture.spec.js | 96 +++++++ 6 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 packages/examples/src/examples/heatHaze/ExampleHeatHaze.tsx diff --git a/packages/examples/src/examples/heatHaze/ExampleHeatHaze.tsx b/packages/examples/src/examples/heatHaze/ExampleHeatHaze.tsx new file mode 100644 index 000000000..1bfbd89b7 --- /dev/null +++ b/packages/examples/src/examples/heatHaze/ExampleHeatHaze.tsx @@ -0,0 +1,235 @@ +/** + * melonJS — heat-haze example (renderer.toFrameTexture() over a lit scene). + * Copyright (C) 2011 - 2026 AltByte Pte Ltd — MIT License. + * See `packages/examples/LICENSE.md` for full license + asset credits. + * + * A grid of normal-mapped tiles lit by a moving `Light2d` (so the engine draws + * them through the LIT quad batcher), seen through a rising heat-haze that + * distorts the whole view. The haze surface, drawn last, captures the lit frame + * with `renderer.toFrameTexture()` and ripples it. + * + * This is the scene that stresses the lit-batcher / capture interaction: the + * lit batcher parks its normal maps in the TOP half of the texture-unit range, + * and `toFrameTexture` binds its capture to a scratch unit at the very top — + * the same units. The renderer invalidates that unit across every batcher after + * the copy, so the next lit frame re-binds the real normal maps; without that, + * a tile whose normal landed on the scratch unit would sample the captured + * frame AS its normal map and light up wrong. (See tests/toframetexture.spec.js + * "invalidates the scratch unit across all batchers ...".) + */ +import { DebugPanelPlugin } from "@melonjs/debug-plugin"; +import { + type Application, + Light2d, + loader, + NoiseTexture2d, + plugin, + type ShaderEffect, + Sprite, + Stage, + state, + video, +} from "melonjs"; +import { createExampleComponent } from "../utils"; + +// rising-heat distortion. Samples the captured scene (uScene) with a vertical +// wobble that scrolls upward and is stronger lower on screen. Screen space is +// the quad's own UV, Y-flipped for the Y-up framebuffer capture. +const HAZE_FRAGMENT = ` +uniform sampler2D uScene; // captured lit frame, bound each draw via setTexture +uniform sampler2D uNoise; // seamless flow map +uniform float uTime; +uniform float uStrength; + +vec4 apply(vec4 color, vec2 uv) { + vec2 s = vec2(uv.x, 1.0 - uv.y); + float n1 = texture2D(uNoise, vec2(s.x * 2.0, s.y * 1.6 - uTime * 0.18)).r; + float n2 = texture2D(uNoise, vec2(s.x * 3.3 + 0.5, s.y * 2.2 - uTime * 0.11)).r; + float wob = (n1 + n2 - 1.0) * uStrength * (1.15 - s.y); // stronger near the floor + vec2 d = clamp(s + vec2(wob, wob * 0.35), 0.0, 1.0); + return vec4(texture2D(uScene, d).rgb, 1.0); +} +`; + +// an embossed metal-ish tile: a base colour with a bevel highlight/shadow, so +// the normal-mapped relief reads clearly under the moving light +const tileAlbedo = (size: number, hue: number) => { + const c = document.createElement("canvas"); + c.width = size; + c.height = size; + const ctx = c.getContext("2d") as CanvasRenderingContext2D; + ctx.fillStyle = `hsl(${hue}, 30%, 42%)`; + ctx.fillRect(0, 0, size, size); + const b = size * 0.12; + ctx.fillStyle = "rgba(255,255,255,0.18)"; + ctx.fillRect(b, b, size - 2 * b, size * 0.06); // top bevel + ctx.fillStyle = "rgba(0,0,0,0.25)"; + ctx.fillRect(b, size - b - size * 0.06, size - 2 * b, size * 0.06); // bottom bevel + ctx.strokeStyle = "rgba(0,0,0,0.35)"; + ctx.lineWidth = 2; + ctx.strokeRect(1, 1, size - 2, size - 2); + return c; +}; + +// the haze surface: full-viewport, drawn last; captures the lit frame and +// refracts it. framewidth/frameheight = viewport so the quad covers everything. +class HazeSurface extends Sprite { + private effect: ShaderEffect; + + constructor( + w: number, + h: number, + tex: HTMLCanvasElement, + effect: ShaderEffect, + ) { + super(w / 2, h / 2, { + image: tex, + framewidth: w, + frameheight: h, + anchorPoint: { x: 0.5, y: 0.5 }, + }); + this.effect = effect; + this.shader = effect; + } + + draw(renderer: Parameters[0], viewport?: object) { + const scene = renderer.toFrameTexture(); + this.effect.setTexture("uScene", scene); + // biome-ignore lint/suspicious/noExplicitAny: viewport shape varies by call site + super.draw(renderer, viewport as any); + } +} + +class PlayScreen extends Stage { + private elapsed = 0; + private light!: Light2d; + private effect!: ShaderEffect; + private w = 0; + private h = 0; + + onResetEvent(app: Application) { + this.w = app.viewport.width; + this.h = app.viewport.height; + const w = this.w; + const h = this.h; + + // neutral ambient floor so unlit tile areas aren't pitch black + this.ambientLightingColor.setColor(70, 74, 90); + + // a viewport-sized white sheen texture for the haze quad's albedo + // (ignored by the shader, which samples the captured scene instead). + // Must match framewidth/frameheight so the quad's UVs span a clean [0,1] + // — a 1x1 image stretched via framewidth produces degenerate UVs. + const white = document.createElement("canvas"); + white.width = w; + white.height = h; + const wctx = white.getContext("2d") as CanvasRenderingContext2D; + wctx.fillStyle = "#fff"; + wctx.fillRect(0, 0, w, h); + + // Enough DISTINCT normal-mapped tiles to fill the lit batcher's normal-map + // units up to the top one — that top unit is exactly what toFrameTexture + // grabs as its scratch unit, so this is the case the invalidation guards. + const maxTextures = app.renderer.maxTextures ?? 16; + const distinctTiles = Math.min(24, Math.ceil(maxTextures / 2) + 2); + + const cols = 6; + const rows = 4; + const tileW = w / cols; + const tileH = h / rows; + let k = 0; + for (let r = 0; r < rows; r++) { + for (let cx = 0; cx < cols; cx++) { + const variant = k % distinctTiles; + // each variant gets its OWN albedo + procedural normal map, so the + // lit batcher assigns them to distinct (color, normal) unit pairs + const albedo = tileAlbedo(96, (variant * 37) % 360); + const normal = new NoiseTexture2d({ + width: 96, + height: 96, + type: "simplex", + seed: 3 + variant * 5, + frequency: 0.06 + (variant % 4) * 0.01, + octaves: 3, + asNormalMap: true, + bumpStrength: 2.2, + }); + const tile = new Sprite(cx * tileW + tileW / 2, r * tileH + tileH / 2, { + image: albedo, + normalMap: normal.getTexture(), + framewidth: 96, + frameheight: 96, + anchorPoint: { x: 0.5, y: 0.5 }, + }); + tile.scale((tileW / 96) * 0.98, (tileH / 96) * 0.98); + app.world.addChild(tile, 1); + k++; + } + } + + // the moving light that reveals the normal-mapped relief + this.light = new Light2d(w / 2, h / 2, w * 0.55, w * 0.55, "#fff2d8", 2.4); + this.light.illuminationOnly = true; + app.world.addChild(this.light, 2); + + // the heat-haze post-pass (z 100, above the lit tiles it distorts) + const noise = new NoiseTexture2d({ + width: 256, + height: 256, + type: "simplex", + seed: 99, + frequency: 0.03, + octaves: 3, + seamless: true, + }); + this.effect = loader.getShader("heatHaze") as ShaderEffect; + this.effect.setTexture("uNoise", noise, "repeat"); + this.effect.setUniform("uStrength", 0.02); + app.world.addChild(new HazeSurface(w, h, white, this.effect), 100); + } + + update(dt: number) { + this.elapsed += dt / 1000; + // orbit the light so the normal-mapped relief shifts every frame — makes + // any normal-map corruption from the capture obvious if the fix regressed + const t = this.elapsed; + this.light.pos.set( + this.w * (0.5 + 0.34 * Math.cos(t * 0.7)), + this.h * (0.5 + 0.3 * Math.sin(t * 0.9)), + 0, + ); + this.effect.setTime(this.elapsed); + super.update(dt); + return true; + } + + onDestroyEvent() { + loader.unload({ name: "heatHaze", type: "shader" }); + } +} + +const createGame = () => { + video.init(728, 410, { + parent: "screen", + scale: "auto", + // Light2d normal-map lighting + toFrameTexture are WebGL features + renderer: video.WEBGL, + antiAlias: true, + subPixel: true, + }); + + // register the debug plugin (hidden by default; press S to toggle) + plugin.register(DebugPanelPlugin, "debugPanel"); + + state.set(state.PLAY, new PlayScreen()); + + loader.preload( + [{ name: "heatHaze", type: "shader", data: HAZE_FRAGMENT }], + () => { + state.change(state.PLAY); + }, + false, + ); +}; + +export const ExampleHeatHaze = createExampleComponent(createGame); diff --git a/packages/examples/src/main.tsx b/packages/examples/src/main.tsx index 94c924557..799416d49 100644 --- a/packages/examples/src/main.tsx +++ b/packages/examples/src/main.tsx @@ -103,6 +103,11 @@ const ExampleAquarium = lazy(() => default: m.ExampleAquarium, })), ); +const ExampleHeatHaze = lazy(() => + import("./examples/heatHaze/ExampleHeatHaze").then((m) => ({ + default: m.ExampleHeatHaze, + })), +); const ExampleLineOfSight = lazy(() => import("./examples/lineOfSight/ExampleLineOfSight").then((m) => ({ default: m.ExampleLineOfSight, @@ -369,6 +374,14 @@ const examples: { description: "Screen-space refraction via renderer.toFrameTexture(): swimming fish rippled through the live scene, captured on the GPU and distorted by a scrolling NoiseTexture2d.", }, + { + component: , + label: "Heat Haze", + path: "heat-haze", + sourceDir: "heatHaze", + description: + "renderer.toFrameTexture() over a LIT scene: normal-mapped tiles under a moving Light2d, distorted by a rising heat-haze that captures the lit frame.", + }, { component: , label: "Line of Sight", diff --git a/packages/melonjs/src/video/webgl/batchers/lit_quad_batcher.js b/packages/melonjs/src/video/webgl/batchers/lit_quad_batcher.js index f3ecdc7d3..7888551c5 100644 --- a/packages/melonjs/src/video/webgl/batchers/lit_quad_batcher.js +++ b/packages/melonjs/src/video/webgl/batchers/lit_quad_batcher.js @@ -166,6 +166,40 @@ export default class LitQuadBatcher extends QuadBatcher { this.defaultShader.setUniform("uAmbient", [0, 0, 0]); } + /** + * Also drop the normal-map pairing when its paired unit is invalidated. + * Normal maps live at units `maxBatchTextures..2*maxBatchTextures-1` + * (indexed by the paired albedo unit), so a clobber of one of those GL + * units — e.g. {@link WebGLRenderer#toFrameTexture}'s scratch bind, which + * lands on the top unit — must forget the pairing or the next lit draw + * would assume the normal is still resident and skip re-binding it, + * sampling the clobbering texture as a normal map. + * @param {number} unit - the GL texture unit to invalidate + * @ignore + */ + invalidateUnit(unit) { + super.invalidateUnit(unit); + const n = unit - this.maxBatchTextures; + if (n >= 0 && n < this.maxBatchTextures) { + this.boundNormalMaps[n] = null; + this.boundNormalVersions[n] = -1; + } + } + + /** + * On a full texture-cache reset, also forget the paired normal-map + * bindings — the base handler only clears the color `boundTextures`, so a + * lit draw after a reset would otherwise assume a normal map is still bound + * (and skip re-binding it) while its GL unit may have been reused for a + * color texture. + * @ignore + */ + _onTextureCacheReset() { + super._onTextureCacheReset(); + this.boundNormalMaps.fill(null); + this.boundNormalVersions.fill(-1); + } + /** * Upload per-frame Light2d uniforms used by the lit fragment path. * Called once per camera per frame (before the world tree walk). diff --git a/packages/melonjs/src/video/webgl/batchers/material_batcher.js b/packages/melonjs/src/video/webgl/batchers/material_batcher.js index 460b7febf..a8360082c 100644 --- a/packages/melonjs/src/video/webgl/batchers/material_batcher.js +++ b/packages/melonjs/src/video/webgl/batchers/material_batcher.js @@ -52,15 +52,29 @@ export class MaterialBatcher extends Batcher { // `Batcher` directly and has no texture state, so it doesn't // need this. if (!this._onCacheReset) { + // delegate to an overridable method so subclasses that track extra + // per-unit bindings (lit normal maps) can drop those on a reset too this._onCacheReset = () => { - this.boundTextures.length = 0; - this.currentTextureUnit = -1; - this.currentSamplerUnit = -1; + this._onTextureCacheReset(); }; on(GPU_TEXTURE_CACHE_RESET, this._onCacheReset); } } + /** + * Drop every cached texture binding after a {@link GPU_TEXTURE_CACHE_RESET} + * (the shared texture cache reassigned units — our per-unit view is stale). + * Subclasses that pair extra samplers to units (lit normal maps) override to + * forget those too; without that they'd assume the extra texture is still + * resident and skip re-binding it after the reset. + * @ignore + */ + _onTextureCacheReset() { + this.boundTextures.length = 0; + this.currentTextureUnit = -1; + this.currentSamplerUnit = -1; + } + /** * Free resources used by the batcher. Currently unsubscribes the * texture-cache-reset listener so a discarded batcher doesn't keep @@ -349,6 +363,25 @@ export class MaterialBatcher extends Batcher { return unit; } + /** + * Forget whatever texture this batcher believes is bound to `unit`, so the + * next bind to it re-issues the GL bind. Used when a GL texture unit is + * clobbered OUTSIDE this batcher's own accounting — e.g. + * {@link WebGLRenderer#toFrameTexture}, which binds its capture to a scratch + * unit directly (not via the shared texture cache), so a different batcher's + * unit cache would otherwise skip a needed re-bind. Subclasses that pair + * extra samplers to the same unit (lit normal maps) override to drop those + * too. + * @param {number} unit - the GL texture unit to invalidate + * @ignore + */ + invalidateUnit(unit) { + delete this.boundTextures[unit]; + if (this.currentTextureUnit === unit) { + this.currentTextureUnit = -1; + } + } + /** * @ignore * @param {TextureAtlas|TextureResource} texture diff --git a/packages/melonjs/src/video/webgl/webgl_renderer.js b/packages/melonjs/src/video/webgl/webgl_renderer.js index e0b87074f..b742cebbd 100644 --- a/packages/melonjs/src/video/webgl/webgl_renderer.js +++ b/packages/melonjs/src/video/webgl/webgl_renderer.js @@ -962,6 +962,16 @@ export default class WebGLRenderer extends Renderer { gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, x, y, w, h); } + // the scratch bind clobbered GL `unit` OUTSIDE the shared texture-cache + // accounting (bound directly, not via allocateTextureUnit), so the OTHER + // batchers' unit caches don't know it changed. Invalidate it on each — + // critically the lit quad batcher, whose normal-map slots occupy the top + // half of the unit range and overlap this scratch unit; without this a + // later lit draw could sample the capture as a normal map. + for (const b of this.batchers.values()) { + b.invalidateUnit?.(unit); + } + return frame; } diff --git a/packages/melonjs/tests/toframetexture.spec.js b/packages/melonjs/tests/toframetexture.spec.js index 5678ad0da..32689b072 100644 --- a/packages/melonjs/tests/toframetexture.spec.js +++ b/packages/melonjs/tests/toframetexture.spec.js @@ -255,6 +255,102 @@ describe("WebGLRenderer.toFrameTexture", () => { effect.destroy(); uploader.destroy(); }); + + // toFrameTexture binds its capture to a scratch unit (the top of the quad + // batcher's range) directly, bypassing the shared texture-cache accounting. + // The LIT quad batcher parks normal maps in the top half of the unit range, + // so that scratch unit overlaps a normal-map slot — without invalidating it + // across batchers, a later lit draw would assume the normal is still resident + // and skip re-binding it, sampling the just-captured frame AS a normal map. + it("invalidates the scratch unit across all batchers so lighting survives a capture", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + const quad = renderer.batchers.get("quad"); + const lit = renderer.batchers.get("litQuad"); + if (!quad || !lit) { + ctx.skip(); + return; + } + + // the exact unit toFrameTexture uses, and its paired lit normal-map slot + const scratch = quad.maxBatchTextures - 1; + const normalIndex = scratch - lit.maxBatchTextures; + const normalInRange = + normalIndex >= 0 && normalIndex < lit.maxBatchTextures; + + // seed the caches as a prior lit draw would: a colour texture the quad + // batcher thinks is at the scratch unit, and a normal map the lit batcher + // thinks is paired to it + quad.boundTextures[scratch] = { fake: "color" }; + if (normalInRange) { + lit.boundNormalMaps[normalIndex] = { fake: "normal" }; + lit.boundNormalVersions[normalIndex] = 7; + } + + paintScene("#ff0000"); + const frame = renderer.toFrameTexture(); + + // the stale color view of the scratch unit is cleared… + expect(quad.boundTextures[scratch]).toBeUndefined(); + // …and (the crux) the lit batcher forgot its normal pairing for that unit, + // so the next lit draw re-binds the real normal instead of the capture + if (normalInRange) { + expect(lit.boundNormalMaps[normalIndex]).toBe(null); + expect(lit.boundNormalVersions[normalIndex]).toBe(-1); + } + // and the capture itself is intact — both features coexist + expect(gl.isTexture(frame.glTexture)).toBe(true); + }); + + // adversarial: invalidateUnit must ONLY touch the normal slot for GL units in + // the normal range (top half), never for a low colour/albedo unit + it("invalidateUnit only drops the normal pairing for units in the normal range", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + const lit = renderer.batchers.get("litQuad"); + if (!lit) { + ctx.skip(); + return; + } + // a LOW unit (0) is a colour/albedo unit — its clobber must NOT drop the + // normal at index 0 (which lives at GL unit maxBatchTextures + 0) + lit.boundTextures[0] = { fake: true }; + lit.boundNormalMaps[0] = { keep: true }; + lit.invalidateUnit(0); + expect(lit.boundTextures[0]).toBeUndefined(); // colour cleared + expect(lit.boundNormalMaps[0]).toEqual({ keep: true }); // normal untouched + + // the GL unit that DOES pair to normal index 0 must drop it + lit.invalidateUnit(lit.maxBatchTextures + 0); + expect(lit.boundNormalMaps[0]).toBe(null); + }); + + // the latent gap this surfaced: a full texture-cache reset (unit-pool wrap) + // must clear the lit normal-map cache too, not just colours + it("a texture-cache reset clears the lit normal-map cache, not just colors", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + const lit = renderer.batchers.get("litQuad"); + if (!lit) { + ctx.skip(); + return; + } + lit.boundTextures[0] = { fake: "color" }; + lit.boundNormalMaps[0] = { fake: "normal" }; + lit.boundNormalVersions[0] = 9; + + renderer.cache.resetUnitAssignments(); // emits GPU_TEXTURE_CACHE_RESET + + expect(lit.boundTextures.length).toBe(0); // colours cleared (base handler) + expect(lit.boundNormalMaps[0]).toBe(null); // normals cleared (the gap fix) + expect(lit.boundNormalVersions[0]).toBe(-1); + }); }); describe("CanvasRenderer.toFrameTexture", () => { From 01da107e32a9b5afee2075ed92f60ddffd3f427e Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Thu, 9 Jul 2026 09:54:06 +0800 Subject: [PATCH 15/20] fix(webgl): invalidate ALL directly-bound top units, not just the capture scratch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the code review's HIGH finding on the previous commit: the fix invalidated only toFrameTexture's single scratch unit, but ShaderEffect _prepareTextures binds its extra samplers counting DOWN from the top (maxBatchTextures-1, -2, ...), each a direct bind that bypasses the shared texture cache and each overlapping a lit normal-map unit. Only the top one was covered — the shipped Heat Haze example actually clobbers the SECOND unit (uScene) which was left un-invalidated. - new WebGLRenderer.invalidateTextureUnit(unit, except): the shared primitive, loops batchers (skipping the binder whose cache is already correct). - toFrameTexture and _prepareTextures both call it, so EVERY directly-bound reserved unit is invalidated across the other batchers — a complete fix, not just the scratch unit. Also from the review (lower severity): - _onTextureCacheReset guards boundNormalMaps?.fill against a reset firing before init allocates the arrays (latent; hardens against init reordering). - Heat Haze example: build each distinct NoiseTexture2d ONCE and reuse across grid cells (was one per sprite), and destroy() them on teardown. - JSDoc precision (samplers land on top AND lower units). Tests: new GPU-independent coverage — invalidateTextureUnit(unit, except) clears all-but-excluded; a spy test proving _prepareTextures invalidates EVERY reserved sampler unit (verified RED with the _prepareTextures call removed: "expected [] to include 15"). Full suite 4758 green, lint 0, build 8/8. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- .../src/examples/heatHaze/ExampleHeatHaze.tsx | 47 +++++++---- .../video/webgl/batchers/lit_quad_batcher.js | 19 +++-- .../melonjs/src/video/webgl/shadereffect.js | 41 +++++---- .../melonjs/src/video/webgl/webgl_renderer.js | 35 ++++++-- packages/melonjs/tests/toframetexture.spec.js | 84 ++++++++++++++++++- 5 files changed, 174 insertions(+), 52 deletions(-) diff --git a/packages/examples/src/examples/heatHaze/ExampleHeatHaze.tsx b/packages/examples/src/examples/heatHaze/ExampleHeatHaze.tsx index 1bfbd89b7..0c321799e 100644 --- a/packages/examples/src/examples/heatHaze/ExampleHeatHaze.tsx +++ b/packages/examples/src/examples/heatHaze/ExampleHeatHaze.tsx @@ -106,6 +106,7 @@ class PlayScreen extends Stage { private effect!: ShaderEffect; private w = 0; private h = 0; + private noiseTextures: NoiseTexture2d[] = []; onResetEvent(app: Application) { this.w = app.viewport.width; @@ -128,10 +129,29 @@ class PlayScreen extends Stage { wctx.fillRect(0, 0, w, h); // Enough DISTINCT normal-mapped tiles to fill the lit batcher's normal-map - // units up to the top one — that top unit is exactly what toFrameTexture - // grabs as its scratch unit, so this is the case the invalidation guards. + // units up to the top one — that top unit overlaps the units the capture + // and the haze effect's samplers bind, so this is the case the + // invalidation guards. Build each distinct (albedo, normal) ONCE and + // reuse it across repeated grid cells. const maxTextures = app.renderer.maxTextures ?? 16; const distinctTiles = Math.min(24, Math.ceil(maxTextures / 2) + 2); + const albedos: HTMLCanvasElement[] = []; + const normals: HTMLCanvasElement[] = []; + for (let v = 0; v < distinctTiles; v++) { + albedos.push(tileAlbedo(96, (v * 37) % 360)); + const n = new NoiseTexture2d({ + width: 96, + height: 96, + type: "simplex", + seed: 3 + v * 5, + frequency: 0.06 + (v % 4) * 0.01, + octaves: 3, + asNormalMap: true, + bumpStrength: 2.2, + }); + this.noiseTextures.push(n); + normals.push(n.getTexture() as HTMLCanvasElement); + } const cols = 6; const rows = 4; @@ -141,22 +161,9 @@ class PlayScreen extends Stage { for (let r = 0; r < rows; r++) { for (let cx = 0; cx < cols; cx++) { const variant = k % distinctTiles; - // each variant gets its OWN albedo + procedural normal map, so the - // lit batcher assigns them to distinct (color, normal) unit pairs - const albedo = tileAlbedo(96, (variant * 37) % 360); - const normal = new NoiseTexture2d({ - width: 96, - height: 96, - type: "simplex", - seed: 3 + variant * 5, - frequency: 0.06 + (variant % 4) * 0.01, - octaves: 3, - asNormalMap: true, - bumpStrength: 2.2, - }); const tile = new Sprite(cx * tileW + tileW / 2, r * tileH + tileH / 2, { - image: albedo, - normalMap: normal.getTexture(), + image: albedos[variant], + normalMap: normals[variant], framewidth: 96, frameheight: 96, anchorPoint: { x: 0.5, y: 0.5 }, @@ -182,6 +189,7 @@ class PlayScreen extends Stage { octaves: 3, seamless: true, }); + this.noiseTextures.push(noise); this.effect = loader.getShader("heatHaze") as ShaderEffect; this.effect.setTexture("uNoise", noise, "repeat"); this.effect.setUniform("uStrength", 0.02); @@ -205,6 +213,11 @@ class PlayScreen extends Stage { onDestroyEvent() { loader.unload({ name: "heatHaze", type: "shader" }); + // free the baked NoiseTexture2d canvases (tile normals + haze flow map) + for (const n of this.noiseTextures) { + n.destroy(); + } + this.noiseTextures = []; } } diff --git a/packages/melonjs/src/video/webgl/batchers/lit_quad_batcher.js b/packages/melonjs/src/video/webgl/batchers/lit_quad_batcher.js index 7888551c5..91f763891 100644 --- a/packages/melonjs/src/video/webgl/batchers/lit_quad_batcher.js +++ b/packages/melonjs/src/video/webgl/batchers/lit_quad_batcher.js @@ -169,11 +169,13 @@ export default class LitQuadBatcher extends QuadBatcher { /** * Also drop the normal-map pairing when its paired unit is invalidated. * Normal maps live at units `maxBatchTextures..2*maxBatchTextures-1` - * (indexed by the paired albedo unit), so a clobber of one of those GL - * units — e.g. {@link WebGLRenderer#toFrameTexture}'s scratch bind, which - * lands on the top unit — must forget the pairing or the next lit draw - * would assume the normal is still resident and skip re-binding it, - * sampling the clobbering texture as a normal map. + * (indexed by the paired albedo unit), which overlap the top units that + * {@link WebGLRenderer#toFrameTexture} (its scratch unit) and + * {@link ShaderEffect#_prepareTextures} (its reserved extra samplers, + * counting DOWN from the top) bind directly. When one of those GL units is + * clobbered we must forget the pairing, or the next lit draw would assume + * the normal is still resident and skip re-binding it, sampling the + * clobbering texture as a normal map. * @param {number} unit - the GL texture unit to invalidate * @ignore */ @@ -191,13 +193,14 @@ export default class LitQuadBatcher extends QuadBatcher { * bindings — the base handler only clears the color `boundTextures`, so a * lit draw after a reset would otherwise assume a normal map is still bound * (and skip re-binding it) while its GL unit may have been reused for a - * color texture. + * color texture. The `?.` guards a reset firing before `init` allocates the + * arrays (none does today; hardens against future init reordering). * @ignore */ _onTextureCacheReset() { super._onTextureCacheReset(); - this.boundNormalMaps.fill(null); - this.boundNormalVersions.fill(-1); + this.boundNormalMaps?.fill(null); + this.boundNormalVersions?.fill(-1); } /** diff --git a/packages/melonjs/src/video/webgl/shadereffect.js b/packages/melonjs/src/video/webgl/shadereffect.js index 0f71912d8..bcfc6cb06 100644 --- a/packages/melonjs/src/video/webgl/shadereffect.js +++ b/packages/melonjs/src/video/webgl/shadereffect.js @@ -352,26 +352,31 @@ export default class ShaderEffect { batcher.bindTexture2D(glTex, entry.unit, false); this._shader.setUniform(name, entry.unit); } - continue; - } - if (entry.tex === null) { - batcher.createTexture2D( - entry.unit, - entry.image, - filter, - entry.repeat, - entry.image.width, - entry.image.height, - false, // premultipliedAlpha — keep raw texel values - false, // mipmap — not needed, and NPOT-unsafe under WebGL 1 - undefined, - false, // flush — the following draw flushes with everything bound - ); - entry.tex = batcher.boundTextures[entry.unit]; } else { - batcher.bindTexture2D(entry.tex, entry.unit, false); + if (entry.tex === null) { + batcher.createTexture2D( + entry.unit, + entry.image, + filter, + entry.repeat, + entry.image.width, + entry.image.height, + false, // premultipliedAlpha — keep raw texel values + false, // mipmap — not needed, and NPOT-unsafe under WebGL 1 + undefined, + false, // flush — the following draw flushes with everything bound + ); + entry.tex = batcher.boundTextures[entry.unit]; + } else { + batcher.bindTexture2D(entry.tex, entry.unit, false); + } + this._shader.setUniform(name, entry.unit); } - this._shader.setUniform(name, entry.unit); + // this sampler was bound to a reserved high unit DIRECTLY (bypassing + // the shared texture cache), which overlaps the lit quad batcher's + // normal-map units — invalidate it on the OTHER batchers so a later + // lit draw re-binds its normal there instead of sampling this texture + batcher.renderer.invalidateTextureUnit(entry.unit, batcher); } } diff --git a/packages/melonjs/src/video/webgl/webgl_renderer.js b/packages/melonjs/src/video/webgl/webgl_renderer.js index b742cebbd..e032b4940 100644 --- a/packages/melonjs/src/video/webgl/webgl_renderer.js +++ b/packages/melonjs/src/video/webgl/webgl_renderer.js @@ -963,18 +963,37 @@ export default class WebGLRenderer extends Renderer { } // the scratch bind clobbered GL `unit` OUTSIDE the shared texture-cache - // accounting (bound directly, not via allocateTextureUnit), so the OTHER - // batchers' unit caches don't know it changed. Invalidate it on each — - // critically the lit quad batcher, whose normal-map slots occupy the top - // half of the unit range and overlap this scratch unit; without this a - // later lit draw could sample the capture as a normal map. - for (const b of this.batchers.values()) { - b.invalidateUnit?.(unit); - } + // accounting (bound directly, not via allocateTextureUnit), so other + // batchers' unit caches don't know it changed — invalidate it on all of + // them (see invalidateTextureUnit). + this.invalidateTextureUnit(unit); return frame; } + /** + * Forget the cached binding for GL texture `unit` on every batcher (except + * `except`). Call this whenever a texture is bound to a unit **directly** — + * bypassing the shared texture cache and its `GPU_TEXTURE_CACHE_RESET` + * coordination — as {@link WebGLRenderer#toFrameTexture}'s capture copy and + * {@link ShaderEffect#_prepareTextures}'s reserved extra samplers both do. + * Those binds land on the TOP of the unit range, which overlaps the lit quad + * batcher's normal-map units; without invalidation a later lit draw would + * assume its normal map is still resident there and skip re-binding it, + * sampling the directly-bound texture as a normal map (wrong lighting). + * @param {number} unit - the GL texture unit that was clobbered + * @param {Batcher} [except] - a batcher to skip (the one that just bound the + * texture — its own cache is already accurate) + * @ignore + */ + invalidateTextureUnit(unit, except) { + for (const b of this.batchers.values()) { + if (b !== except) { + b.invalidateUnit?.(unit); + } + } + } + /** * Begin capturing rendering to an offscreen FBO for post-effect processing. * @param {Renderable} renderable - the renderable requesting post-effect processing diff --git a/packages/melonjs/tests/toframetexture.spec.js b/packages/melonjs/tests/toframetexture.spec.js index 32689b072..e2e53a311 100644 --- a/packages/melonjs/tests/toframetexture.spec.js +++ b/packages/melonjs/tests/toframetexture.spec.js @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it } from "vitest"; +import { beforeAll, describe, expect, it, vi } from "vitest"; import { boot, CanvasRenderer, @@ -351,6 +351,88 @@ describe("WebGLRenderer.toFrameTexture", () => { expect(lit.boundNormalMaps[0]).toBe(null); // normals cleared (the gap fix) expect(lit.boundNormalVersions[0]).toBe(-1); }); + + // invalidateTextureUnit is the shared primitive: it must clear the unit on + // EVERY batcher except the one that just bound it. GPU-independent. + it("invalidateTextureUnit clears a unit on all batchers except the excluded one", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + const quad = renderer.batchers.get("quad"); + const lit = renderer.batchers.get("litQuad"); + if (!quad || !lit) { + ctx.skip(); + return; + } + const glUnit = lit.maxBatchTextures + 1; // a real lit normal-map GL unit + lit.boundNormalMaps[1] = { fake: "normal" }; + quad.boundTextures[glUnit] = { fake: "color" }; + + // exclude the quad batcher → its binding is kept, the lit one is dropped + renderer.invalidateTextureUnit(glUnit, quad); + expect(lit.boundNormalMaps[1]).toBe(null); // lit invalidated + expect(quad.boundTextures[glUnit]).toEqual({ fake: "color" }); // quad kept + }); + + // the crux of the completeness fix: an effect with MULTIPLE extra samplers + // binds them to several top units (counting down) — ALL of which overlap lit + // normal-map units — so _prepareTextures must invalidate EVERY one, not just + // the scratch. Verified GPU-independently by spying on the shared primitive. + it("_prepareTextures invalidates every reserved sampler unit an effect binds", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + const quad = renderer.batchers.get("quad"); + if (!quad) { + ctx.skip(); + return; + } + const mk = (col) => { + const c = document.createElement("canvas"); + c.width = SIZE; + c.height = SIZE; + const x = c.getContext("2d"); + x.fillStyle = col; + x.fillRect(0, 0, SIZE, SIZE); + return c; + }; + // TWO extra samplers → bound to units (maxBatchTextures-1) and (-2) + const effect = new ShaderEffect( + renderer, + "uniform sampler2D uA;\nuniform sampler2D uB;\nvec4 apply(vec4 c, vec2 uv) { return texture2D(uA, uv) + texture2D(uB, uv); }", + ); + effect.setTexture("uA", mk("#ff0000")); + effect.setTexture("uB", mk("#00ff00")); + + const spy = vi.spyOn(renderer, "invalidateTextureUnit"); + renderer.save(); + renderer.customShader = effect; + renderer.drawImage(mk("#000000"), 0, 0, SIZE, SIZE, 0, 0, SIZE, SIZE); + renderer.flush(); + renderer.customShader = undefined; + renderer.restore(); + + const u1 = quad.maxBatchTextures - 1; + const u2 = quad.maxBatchTextures - 2; + const calls = spy.mock.calls; + const units = calls.map((c) => { + return c[0]; + }); + // BOTH sampler units invalidated (the second one is the bug the review + // caught — the pre-fix code only invalidated the scratch unit) + expect(units).toContain(u1); + expect(units).toContain(u2); + // each excludes the drawing (quad) batcher, whose cache is already correct + const callForU2 = calls.find((c) => { + return c[0] === u2; + }); + expect(callForU2?.[1]).toBe(quad); + + spy.mockRestore(); + effect.destroy(); + }); }); describe("CanvasRenderer.toFrameTexture", () => { From d8ff81909073be30d68b980f2cac0552486627c8 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Thu, 9 Jul 2026 10:11:24 +0800 Subject: [PATCH 16/20] =?UTF-8?q?refactor(toFrameTexture):=20PR=20review?= =?UTF-8?q?=20=E2=80=94=20target/region=20validation,=20docs,=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review + the missing docs entry: - README: replaced the removed "Water Refraction" example link with the new Aquarium + Heat Haze examples (both showcase renderer.toFrameTexture()). - CanvasRenderer.toFrameTexture now validates `target` is a CanvasFrameTexture (mirrors the WebGL guard) instead of throwing a confusing frame.canvas error. - Both backends clamp the region ORIGIN (x/y) into the framebuffer/canvas before sizing, and default a missing width/height to the remaining extent — an out-of-range x/y could otherwise push x+w past the edge and trip copyTex(Sub)Image2D INVALID_VALUE. Fixed the stale `{x,y,w,h}` comment. - WebGLRenderer.toFrameTexture rejects a `target` from a DIFFERENT renderer (frame._renderer !== this) — would delete textures on the wrong GL context. - ShaderEffect.setTexture live-source detection now also requires a `glTexture` field; a Texture2d with isGPUResident=true but no handle falls back to the static getTexture() unwrap instead of silently never binding. - Texture2d docs: clarify a GPU-resident capture (FrameTexture) is a custom-shader sampler (setTexture), NOT a drawable assignable to Sprite#image. Base renderer JSDoc notes the region origin differs by backend (WebGL bottom-left / Canvas top-left). Tests: bad target throws (WebGL + Canvas); an out-of-bounds region clamps to a valid capture with no GL error. Full suite 4761 green, lint 0, build 8/8. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- README.md | 3 +- .../src/video/canvas/canvas_renderer.js | 30 +++++++++++-- packages/melonjs/src/video/renderer.js | 4 +- .../melonjs/src/video/texture/texture2d.ts | 25 ++++++----- .../melonjs/src/video/webgl/shadereffect.js | 12 +++-- .../melonjs/src/video/webgl/webgl_renderer.js | 45 ++++++++++++------- packages/melonjs/tests/toframetexture.spec.js | 43 ++++++++++++++++++ 7 files changed, 126 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index bd967cd04..a40496a7f 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,8 @@ Examples * [Hello World](https://melonjs.github.io/melonJS/examples/#/hello-world) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/helloWorld)) * [Whac-A-Mole](https://melonjs.github.io/melonJS/examples/#/whac-a-mole) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/whac-a-mole)) * [Compressed Textures](https://melonjs.github.io/melonJS/examples/#/compressed-textures) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/compressedTextures)) -* [Water Refraction](https://melonjs.github.io/melonJS/examples/#/water-refraction) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/waterRefraction)) — a perspective pool floor rippling under the water: a static seamless `NoiseTexture2d` bound as an extra shader sampler via `ShaderEffect.setTexture` and scrolled on the GPU with `setTime`, with pointer swells, depth fog and animated caustics +* [Aquarium](https://melonjs.github.io/melonJS/examples/#/aquarium) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/aquarium)) — screen-space water refraction with `renderer.toFrameTexture()`: fish swim across a seabed, then a water surface captures the live frame on the GPU and re-samples it through a scrolling `NoiseTexture2d` flow map (the Godot `hint_screen_texture` / Unity `_CameraOpaqueTexture` pattern) +* [Heat Haze](https://melonjs.github.io/melonJS/examples/#/heat-haze) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/heatHaze)) — `renderer.toFrameTexture()` over a lit scene: normal-mapped tiles under a moving `Light2d`, distorted by a rising heat-haze that captures and ripples the lit frame * [3D Mesh](https://melonjs.github.io/melonJS/examples/#/mesh-3d) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/mesh3d)) * [3D Mesh Material](https://melonjs.github.io/melonJS/examples/#/mesh-3d-material) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/mesh3dMaterial)) * [AfterBurner Clone](https://melonjs.github.io/melonJS/examples/#/after-burner) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/afterBurner)) — `Camera3d` + 3D Mesh arcade shooter diff --git a/packages/melonjs/src/video/canvas/canvas_renderer.js b/packages/melonjs/src/video/canvas/canvas_renderer.js index cf4547fab..ba5ade51e 100644 --- a/packages/melonjs/src/video/canvas/canvas_renderer.js +++ b/packages/melonjs/src/video/canvas/canvas_renderer.js @@ -106,6 +106,19 @@ export default class CanvasRenderer extends Renderer { * @returns {Texture2d} a texture holding the captured frame */ toFrameTexture(options = {}) { + // a non-null `target` must be a capture this method previously returned + // (a CanvasFrameTexture), or the frame.canvas dereference below throws a + // confusing error — mirror the WebGLRenderer guard + if ( + typeof options.target !== "undefined" && + options.target !== null && + !(options.target instanceof CanvasFrameTexture) + ) { + throw new Error( + "CanvasRenderer.toFrameTexture: `target` must be a capture returned by this method", + ); + } + const src = this.getCanvas(); let x = 0; let y = 0; @@ -115,10 +128,19 @@ export default class CanvasRenderer extends Renderer { if (typeof region !== "undefined") { const rx = typeof region.pos !== "undefined" ? region.pos.x : region.x; const ry = typeof region.pos !== "undefined" ? region.pos.y : region.y; - x = Math.max(0, Math.floor(rx || 0)); - y = Math.max(0, Math.floor(ry || 0)); - w = Math.max(1, Math.min(src.width - x, Math.ceil(region.width))); - h = Math.max(1, Math.min(src.height - y, Math.ceil(region.height))); + // clamp the origin into the canvas first, then size to what remains + // (matches WebGLRenderer#toFrameTexture); missing width/height + // defaults to "the rest of the canvas from x/y" + x = Math.min(Math.max(0, Math.floor(rx || 0)), src.width - 1); + y = Math.min(Math.max(0, Math.floor(ry || 0)), src.height - 1); + const rw = Number.isFinite(region.width) + ? Math.ceil(region.width) + : src.width - x; + const rh = Number.isFinite(region.height) + ? Math.ceil(region.height) + : src.height - y; + w = Math.max(1, Math.min(src.width - x, rw)); + h = Math.max(1, Math.min(src.height - y, rh)); } // no target → shared slot; target: null → mint owned; target: diff --git a/packages/melonjs/src/video/renderer.js b/packages/melonjs/src/video/renderer.js index 56267fb45..c7dcfe7c8 100644 --- a/packages/melonjs/src/video/renderer.js +++ b/packages/melonjs/src/video/renderer.js @@ -1139,7 +1139,9 @@ export default class Renderer { * returned by this method to refresh it in place; pass `null` to mint a * fresh, caller-owned capture * @param {Rect|Bounds|{x: number, y: number, width: number, height: number}} [options.region] - capture - * only this sub-region of the frame + * only this sub-region of the frame. NOTE the origin differs by backend: + * WebGL uses framebuffer coords (**bottom-left** origin), Canvas uses + * **top-left** — relevant when a region is used under `video.AUTO`. * @returns {Texture2d} a texture holding the captured frame * @example * effect.setTexture("uScene", renderer.toFrameTexture()); diff --git a/packages/melonjs/src/video/texture/texture2d.ts b/packages/melonjs/src/video/texture/texture2d.ts index 89ec29511..bce214492 100644 --- a/packages/melonjs/src/video/texture/texture2d.ts +++ b/packages/melonjs/src/video/texture/texture2d.ts @@ -51,22 +51,27 @@ export type Texture2dSource = * `CompressedImage` data are accepted too, but are not part of this class * hierarchy. * - * Most assets are CPU-backed and resolve to a drawable canvas/image. - * Subclasses may also be **GPU-resident**, resolving to a renderer texture - * resource that never leaves the GPU — the contract deliberately admits both - * (a future WebGPU backend follows the same shape with a `GPUTexture`-backed - * resource). + * Most assets are **CPU-backed** and resolve to a drawable canvas/image — those + * are the ones assignable to {@link Sprite#image} / {@link Sprite#normalMap} / + * an {@link ImageLayer} (which upload the drawable through the texture cache). + * Subclasses may also be **GPU-resident**, resolving to an opaque renderer + * backing that never leaves the GPU (e.g. the capture returned by + * `renderer.toFrameTexture()`). A GPU-resident texture is **not** a drawable + * source: use it as a custom-shader sampler via {@link ShaderEffect#setTexture} + * (which binds its live GL handle), NOT as `Sprite#image`. The contract admits + * both (a future WebGPU backend follows the same shape with a `GPUTexture`). * * Concrete implementations: - * - {@link TextureAtlas} — packed multi-region sprite sheet + * - {@link TextureAtlas} — packed multi-region sprite sheet (CPU-backed) * @category Game Objects */ export default abstract class Texture2d { /** - * Return the backing source for this texture — a drawable canvas/image - * for CPU-backed assets, or a renderer texture resource for GPU-resident - * ones. Assignable to {@link Sprite#image}, {@link Sprite#normalMap}, an - * {@link ImageLayer}, or bound as a sampler uniform in a custom shader. + * Return the backing source for this texture — a drawable canvas/image for + * CPU-backed assets (assignable to {@link Sprite#image} / + * {@link Sprite#normalMap} / an {@link ImageLayer}), or an opaque + * GPU-resident backing for GPU-resident ones (bind those as a custom-shader + * sampler via {@link ShaderEffect#setTexture}; they are not drawable sources). * @returns the backing source */ abstract getTexture(): Texture2dSource; diff --git a/packages/melonjs/src/video/webgl/shadereffect.js b/packages/melonjs/src/video/webgl/shadereffect.js index bcfc6cb06..2a4b6f400 100644 --- a/packages/melonjs/src/video/webgl/shadereffect.js +++ b/packages/melonjs/src/video/webgl/shadereffect.js @@ -268,10 +268,14 @@ export default class ShaderEffect { // A GPU-resident LIVE source (a frame capture from // renderer.toFrameTexture): keep the wrapper and bind its live GL // handle every draw — never upload a static copy — so re-capturing - // into the same slot each frame is picked up with no re-bind. Any - // other Texture2d is a static asset: resolve it to its backing - // drawable source once. - const live = image instanceof Texture2d && image.isGPUResident === true; + // into the same slot each frame is picked up with no re-bind. The live + // path reads `.glTexture`, so require that handle to be present; + // otherwise fall through and unwrap like any static Texture2d asset, + // rather than take the live branch and silently never bind. + const live = + image instanceof Texture2d && + image.isGPUResident === true && + "glTexture" in image; if (!live && image instanceof Texture2d) { image = image.getTexture(); } diff --git a/packages/melonjs/src/video/webgl/webgl_renderer.js b/packages/melonjs/src/video/webgl/webgl_renderer.js index e032b4940..ce951478f 100644 --- a/packages/melonjs/src/video/webgl/webgl_renderer.js +++ b/packages/melonjs/src/video/webgl/webgl_renderer.js @@ -867,7 +867,7 @@ export default class WebGLRenderer extends Renderer { const canvas = this.getCanvas(); // resolve the capture rect in framebuffer pixels (default: everything). - // accepts a Rect (pos + width/height), a Bounds, or a plain {x,y,w,h}. + // accepts a Rect (pos + width/height), a Bounds, or {x, y, width, height}. let x = 0; let y = 0; let w = canvas.width; @@ -876,26 +876,39 @@ export default class WebGLRenderer extends Renderer { if (typeof region !== "undefined") { const rx = typeof region.pos !== "undefined" ? region.pos.x : region.x; const ry = typeof region.pos !== "undefined" ? region.pos.y : region.y; - x = Math.max(0, Math.floor(rx || 0)); - y = Math.max(0, Math.floor(ry || 0)); - w = Math.max(1, Math.min(canvas.width - x, Math.ceil(region.width))); - h = Math.max(1, Math.min(canvas.height - y, Math.ceil(region.height))); + // clamp the ORIGIN into the framebuffer first, then size to what + // remains — an out-of-range x/y must not push x+w past the edge, or + // copyTex(Sub)Image2D throws INVALID_VALUE. Missing width/height + // defaults to "the rest of the framebuffer from x/y". + x = Math.min(Math.max(0, Math.floor(rx || 0)), canvas.width - 1); + y = Math.min(Math.max(0, Math.floor(ry || 0)), canvas.height - 1); + const rw = Number.isFinite(region.width) + ? Math.ceil(region.width) + : canvas.width - x; + const rh = Number.isFinite(region.height) + ? Math.ceil(region.height) + : canvas.height - y; + w = Math.max(1, Math.min(canvas.width - x, rw)); + h = Math.max(1, Math.min(canvas.height - y, rh)); } // flush pending geometry so the capture holds everything drawn so far this.flush(); - // a non-null `target` must be a capture this method previously returned - // (a FrameTexture) — any other Texture2d has no `glTexture` and would - // mis-drive the isTexture/reallocation logic below - if ( - typeof options.target !== "undefined" && - options.target !== null && - !(options.target instanceof FrameTexture) - ) { - throw new Error( - "WebGLRenderer.toFrameTexture: `target` must be a capture returned by this method", - ); + // a non-null `target` must be a capture THIS renderer previously returned + // (a FrameTexture) — any other Texture2d has no `glTexture`, and a capture + // from a different renderer would delete textures on the wrong GL context + if (typeof options.target !== "undefined" && options.target !== null) { + if (!(options.target instanceof FrameTexture)) { + throw new Error( + "WebGLRenderer.toFrameTexture: `target` must be a capture returned by this method", + ); + } + if (options.target._renderer !== this) { + throw new Error( + "WebGLRenderer.toFrameTexture: `target` belongs to a different renderer", + ); + } } // destination: diff --git a/packages/melonjs/tests/toframetexture.spec.js b/packages/melonjs/tests/toframetexture.spec.js index e2e53a311..16e7c5858 100644 --- a/packages/melonjs/tests/toframetexture.spec.js +++ b/packages/melonjs/tests/toframetexture.spec.js @@ -433,6 +433,43 @@ describe("WebGLRenderer.toFrameTexture", () => { spy.mockRestore(); effect.destroy(); }); + + it("rejects a target that is not a capture from this renderer", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + // not a FrameTexture at all + expect(() => { + renderer.toFrameTexture({ target: {} }); + }).toThrow(); + // a Texture2d but not a capture + class NotACapture extends Texture2d { + getTexture() { + return document.createElement("canvas"); + } + } + expect(() => { + renderer.toFrameTexture({ target: new NotACapture() }); + }).toThrow(/capture returned by this method/); + }); + + it("clamps an out-of-bounds region instead of erroring", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + paintScene("#ff0000"); + // x/y past the framebuffer + a size that would overflow — must not throw + // (INVALID_VALUE) and must still yield a valid capture + const frame = renderer.toFrameTexture({ + region: { x: SIZE + 100, y: SIZE + 100, width: SIZE, height: SIZE }, + }); + expect(gl.isTexture(frame.glTexture)).toBe(true); + expect(frame.width).toBeGreaterThanOrEqual(1); + expect(frame.width).toBeLessThanOrEqual(SIZE); + expect(gl.getError()).toBe(gl.NO_ERROR); + }); }); describe("CanvasRenderer.toFrameTexture", () => { @@ -468,4 +505,10 @@ describe("CanvasRenderer.toFrameTexture", () => { const b = renderer.toFrameTexture(); expect(b).toBe(a); }); + + it("rejects a target that is not a canvas capture", () => { + expect(() => { + renderer.toFrameTexture({ target: {} }); + }).toThrow(/capture returned by this method/); + }); }); From abecc20506966e9aefdeac2bcf35cdc17560799a Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Thu, 9 Jul 2026 10:37:44 +0800 Subject: [PATCH 17/20] test(toFrameTexture): harden the added tests to be genuinely adversarial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited every test added in this PR for "would it catch a regression" and strengthened the weak ones: - region capture: the old test painted a UNIFORM scene, so it couldn't tell a correct offset from a full-frame capture. Now paints framebuffer halves (red/blue) at KNOWN pixel coords via scissored clears — bypassing the bare harness's centred projection — and asserts a LEFT-half region reads red and a RIGHT-half region reads blue (also catches an x-mirror). Verified it goes RED when the x offset is ignored. - Rect-style region ({pos} + width/height) now covered (the region.pos branch). - new guards get adversarial coverage: a foreign-renderer target is rejected; a Texture2d claiming isGPUResident but with no glTexture falls back to the static path (entry.live === false) instead of silently never binding; invalidateUnit on an out-of-range unit is a no-op that doesn't extend the normal-map array; an out-of-bounds region clamps to a valid capture. - Canvas: correct sub-region offset + out-of-bounds clamp + refresh-in-place on resize (same object identity). Full suite 4767 green, lint 0, build 8/8. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- packages/melonjs/tests/toframetexture.spec.js | 181 +++++++++++++++++- 1 file changed, 173 insertions(+), 8 deletions(-) diff --git a/packages/melonjs/tests/toframetexture.spec.js b/packages/melonjs/tests/toframetexture.spec.js index 16e7c5858..0a9714145 100644 --- a/packages/melonjs/tests/toframetexture.spec.js +++ b/packages/melonjs/tests/toframetexture.spec.js @@ -160,20 +160,71 @@ describe("WebGLRenderer.toFrameTexture", () => { owned.destroy(); }); - it("captures only the requested sub-region", (ctx) => { + // paint the framebuffer LEFT half red, RIGHT half blue at KNOWN pixel coords + // via scissored clears — bypasses the bare harness's centred projection (an + // engine drawImage lands offset), so region x maps 1:1 to framebuffer x + const paintHalves = () => { + renderer.flush(); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.viewport(0, 0, SIZE, SIZE); + gl.enable(gl.SCISSOR_TEST); + gl.scissor(0, 0, SIZE / 2, SIZE); + gl.clearColor(1, 0, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.scissor(SIZE / 2, 0, SIZE / 2, SIZE); + gl.clearColor(0, 0, 1, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.disable(gl.SCISSOR_TEST); + }; + + it("captures the CORRECT sub-region offset (not just any painted pixels)", (ctx) => { if (!isWebGL) { ctx.skip(); return; } - paintScene("#ff0000"); - // capture a 16x16 region around the centre (known painted) + paintHalves(); + // a region fully in the LEFT half (x < SIZE/2) must read red; one in the + // RIGHT half, blue. If the x offset were ignored (always captured from 0), + // BOTH would read red — this distinguishes a correct offset from a full + // capture (and would also catch an x-mirror). + const left = renderer.toFrameTexture({ + target: null, + region: { x: 2, y: 2, width: 8, height: 8 }, + }); + const lpx = readCapture(left, 4, 4); + expect(lpx[0]).toBeGreaterThan(200); // red + expect(lpx[2]).toBeLessThan(60); + + const right = renderer.toFrameTexture({ + target: null, + region: { x: SIZE / 2 + 4, y: 2, width: 8, height: 8 }, + }); + const rpx = readCapture(right, 4, 4); + expect(rpx[2]).toBeGreaterThan(200); // blue + expect(rpx[0]).toBeLessThan(60); + + expect(left.width).toBe(8); + expect(right.width).toBe(8); + left.destroy(); + right.destroy(); + }); + + it("accepts a Rect-style region (pos + width/height), not just {x,y,...}", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + paintHalves(); + // same LEFT-half region, expressed as a Rect (pos) — exercises the + // region.pos branch of the coordinate resolver const frame = renderer.toFrameTexture({ - region: { x: 8, y: 8, width: 16, height: 16 }, + target: null, + region: { pos: { x: 2, y: 2 }, width: 8, height: 8 }, }); - expect(frame.width).toBe(16); - expect(frame.height).toBe(16); - const px = readCapture(frame, 8, 8); - expect(px[0]).toBeGreaterThan(200); + expect(frame.width).toBe(8); + const px = readCapture(frame, 4, 4); + expect(px[0]).toBeGreaterThan(200); // red (left) + frame.destroy(); }); it("reallocates when the backing handle goes stale (context-loss self-heal)", (ctx) => { @@ -470,6 +521,72 @@ describe("WebGLRenderer.toFrameTexture", () => { expect(frame.width).toBeLessThanOrEqual(SIZE); expect(gl.getError()).toBe(gl.NO_ERROR); }); + + it("rejects a capture that belongs to a DIFFERENT renderer", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + // a real capture from THIS renderer, re-owned by a fake renderer — the + // instanceof check passes but the ownership check must reject it (else + // frame.destroy() would delete a texture on the wrong GL context) + const owned = renderer.toFrameTexture({ target: null }); + owned._renderer = { gl: renderer.gl }; // a different object, not `renderer` + expect(() => { + renderer.toFrameTexture({ target: owned }); + }).toThrow(/different renderer/); + owned._renderer = renderer; // restore so destroy hits the right context + owned.destroy(); + }); + + it("treats a GPU-resident Texture2d WITHOUT a glTexture as a static asset", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + // a Texture2d that CLAIMS GPU-residency but exposes no glTexture handle: + // the live path reads .glTexture, so taking it would silently never bind. + // It must fall back to the static getTexture() unwrap instead. + const backing = document.createElement("canvas"); + backing.width = SIZE; + backing.height = SIZE; + class FakeGPUResident extends Texture2d { + constructor() { + super(); + this.isGPUResident = true; // no glTexture field + } + getTexture() { + return backing; + } + } + const effect = new ShaderEffect( + renderer, + "uniform sampler2D uX;\nvec4 apply(vec4 c, vec2 uv) { return texture2D(uX, uv); }", + ); + effect.setTexture("uX", new FakeGPUResident()); + const entry = effect._extraTextures.get("uX"); + expect(entry.live).toBe(false); // NOT taken as a live source + expect(entry.image).toBe(backing); // unwrapped to its drawable instead + effect.destroy(); + }); + + it("invalidateUnit is a no-op (no throw) for an out-of-range unit", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + const lit = renderer.batchers.get("litQuad"); + if (!lit) { + ctx.skip(); + return; + } + // a unit far past the array bounds must neither throw nor corrupt state + expect(() => { + lit.invalidateUnit(9999); + renderer.invalidateTextureUnit(9999); + }).not.toThrow(); + expect(lit.boundNormalMaps.length).toBe(lit.maxBatchTextures); + }); }); describe("CanvasRenderer.toFrameTexture", () => { @@ -511,4 +628,52 @@ describe("CanvasRenderer.toFrameTexture", () => { renderer.toFrameTexture({ target: {} }); }).toThrow(/capture returned by this method/); }); + + it("captures the correct canvas sub-region and clamps out-of-bounds", () => { + const ctx2d = renderer.getContext(); + ctx2d.setTransform(1, 0, 0, 1, 0, 0); + ctx2d.fillStyle = "#ff0000"; // left half red + ctx2d.fillRect(0, 0, SIZE / 2, SIZE); + ctx2d.fillStyle = "#0000ff"; // right half blue + ctx2d.fillRect(SIZE / 2, 0, SIZE / 2, SIZE); + + // a RIGHT region must read blue — proves the x offset is honored + const right = renderer.toFrameTexture({ + target: null, + region: { x: SIZE - 8, y: 4, width: 6, height: 6 }, + }); + const px = right + .getTexture() + .getContext("2d") + .getImageData(3, 3, 1, 1).data; + expect(px[2]).toBeGreaterThan(200); // blue + expect(px[0]).toBeLessThan(60); + + // an out-of-bounds origin must clamp to a valid capture, not throw + expect(() => { + const oob = renderer.toFrameTexture({ + target: null, + region: { x: 9999, y: 9999, width: 50, height: 50 }, + }); + expect(oob.width).toBeGreaterThanOrEqual(1); + expect(oob.width).toBeLessThanOrEqual(SIZE); + }).not.toThrow(); + }); + + it("refreshes a canvas target IN PLACE on a size change (same object)", () => { + const a = renderer.toFrameTexture({ + target: null, + region: { x: 0, y: 0, width: 8, height: 8 }, + }); + expect(a.width).toBe(8); + // re-capture a DIFFERENT size into the same target — must keep the object + // identity (matches the WebGL variant), not return a new instance + const b = renderer.toFrameTexture({ + target: a, + region: { x: 0, y: 0, width: 16, height: 16 }, + }); + expect(b).toBe(a); + expect(a.width).toBe(16); + expect(a.canvas.width).toBe(16); + }); }); From e6574b593b08c207b8cca210e02835742ca85113 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Thu, 9 Jul 2026 10:44:27 +0800 Subject: [PATCH 18/20] refactor(toFrameTexture): region accepts a Bounds, not a Rect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the Rect (`.pos`) special case from the region resolver in both renderers — there's no reason to accept a Rect when a Bounds already covers it. `region` is now a Bounds (or any object exposing numeric x/y/width/height); a Rect caller passes `rect.getBounds()`. Simpler resolver, one documented shape. JSDoc (WebGL / Canvas / base Renderer) updated accordingly. Test now exercises a real Bounds AND the Rect.getBounds() migration path. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- .../src/video/canvas/canvas_renderer.js | 9 +++--- packages/melonjs/src/video/renderer.js | 10 ++++--- .../melonjs/src/video/webgl/webgl_renderer.js | 11 ++++--- packages/melonjs/tests/toframetexture.spec.js | 29 +++++++++++++------ 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/packages/melonjs/src/video/canvas/canvas_renderer.js b/packages/melonjs/src/video/canvas/canvas_renderer.js index ba5ade51e..49a5d1bcc 100644 --- a/packages/melonjs/src/video/canvas/canvas_renderer.js +++ b/packages/melonjs/src/video/canvas/canvas_renderer.js @@ -101,8 +101,9 @@ export default class CanvasRenderer extends Renderer { * for the shared renderer slot (default); pass a capture previously * returned by this method to refresh it in place; pass `null` to mint a * fresh, caller-owned capture (mirrors {@link WebGLRenderer#toFrameTexture}) - * @param {Rect|Bounds|{x: number, y: number, width: number, height: number}} [options.region] - capture - * only this sub-region (canvas pixels, top-left origin) + * @param {Bounds|{x: number, y: number, width: number, height: number}} [options.region] - capture + * only this sub-region (canvas pixels, top-left origin). A {@link Bounds} + * (or any `{x, y, width, height}`); from a {@link Rect} pass `rect.getBounds()`. * @returns {Texture2d} a texture holding the captured frame */ toFrameTexture(options = {}) { @@ -126,8 +127,8 @@ export default class CanvasRenderer extends Renderer { let h = src.height; const region = options.region; if (typeof region !== "undefined") { - const rx = typeof region.pos !== "undefined" ? region.pos.x : region.x; - const ry = typeof region.pos !== "undefined" ? region.pos.y : region.y; + const rx = region.x; + const ry = region.y; // clamp the origin into the canvas first, then size to what remains // (matches WebGLRenderer#toFrameTexture); missing width/height // defaults to "the rest of the canvas from x/y" diff --git a/packages/melonjs/src/video/renderer.js b/packages/melonjs/src/video/renderer.js index c7dcfe7c8..b988ac50e 100644 --- a/packages/melonjs/src/video/renderer.js +++ b/packages/melonjs/src/video/renderer.js @@ -1138,10 +1138,12 @@ export default class Renderer { * for the shared renderer slot (default); pass a capture previously * returned by this method to refresh it in place; pass `null` to mint a * fresh, caller-owned capture - * @param {Rect|Bounds|{x: number, y: number, width: number, height: number}} [options.region] - capture - * only this sub-region of the frame. NOTE the origin differs by backend: - * WebGL uses framebuffer coords (**bottom-left** origin), Canvas uses - * **top-left** — relevant when a region is used under `video.AUTO`. + * @param {Bounds|{x: number, y: number, width: number, height: number}} [options.region] - capture + * only this sub-region of the frame — a {@link Bounds} (or any + * `{x, y, width, height}`); from a {@link Rect} pass `rect.getBounds()`. + * NOTE the origin differs by backend: WebGL uses framebuffer coords + * (**bottom-left** origin), Canvas uses **top-left** — relevant under + * `video.AUTO`. * @returns {Texture2d} a texture holding the captured frame * @example * effect.setTexture("uScene", renderer.toFrameTexture()); diff --git a/packages/melonjs/src/video/webgl/webgl_renderer.js b/packages/melonjs/src/video/webgl/webgl_renderer.js index ce951478f..c9ac21cd8 100644 --- a/packages/melonjs/src/video/webgl/webgl_renderer.js +++ b/packages/melonjs/src/video/webgl/webgl_renderer.js @@ -850,9 +850,11 @@ export default class WebGLRenderer extends Renderer { * returned by this method to refresh it in place; pass `null` to mint a * fresh, caller-owned capture (for two independent captures in one frame — * `destroy()` it yourself when done) - * @param {Rect|Bounds|{x: number, y: number, width: number, height: number}} [options.region] - capture + * @param {Bounds|{x: number, y: number, width: number, height: number}} [options.region] - capture * only this sub-region (framebuffer pixels, bottom-left origin); a smaller * region is a proportionally cheaper copy. Defaults to the whole framebuffer. + * A {@link Bounds} (or any `{x, y, width, height}`); from a {@link Rect} + * pass `rect.getBounds()`. * @returns {Texture2d} a GPU-resident texture holding the captured frame * @example * // a water surface that refracts the scene rendered behind it @@ -867,15 +869,16 @@ export default class WebGLRenderer extends Renderer { const canvas = this.getCanvas(); // resolve the capture rect in framebuffer pixels (default: everything). - // accepts a Rect (pos + width/height), a Bounds, or {x, y, width, height}. + // a Bounds — or any object exposing numeric x/y/width/height (a Rect via + // `rect.getBounds()`). let x = 0; let y = 0; let w = canvas.width; let h = canvas.height; const region = options.region; if (typeof region !== "undefined") { - const rx = typeof region.pos !== "undefined" ? region.pos.x : region.x; - const ry = typeof region.pos !== "undefined" ? region.pos.y : region.y; + const rx = region.x; + const ry = region.y; // clamp the ORIGIN into the framebuffer first, then size to what // remains — an out-of-range x/y must not push x+w past the edge, or // copyTex(Sub)Image2D throws INVALID_VALUE. Missing width/height diff --git a/packages/melonjs/tests/toframetexture.spec.js b/packages/melonjs/tests/toframetexture.spec.js index 0a9714145..3493a195e 100644 --- a/packages/melonjs/tests/toframetexture.spec.js +++ b/packages/melonjs/tests/toframetexture.spec.js @@ -1,7 +1,9 @@ import { beforeAll, describe, expect, it, vi } from "vitest"; import { + Bounds, boot, CanvasRenderer, + Rect, ShaderEffect, Texture2d, video, @@ -209,22 +211,31 @@ describe("WebGLRenderer.toFrameTexture", () => { right.destroy(); }); - it("accepts a Rect-style region (pos + width/height), not just {x,y,...}", (ctx) => { + it("accepts a Bounds region — and a Rect via getBounds()", (ctx) => { if (!isWebGL) { ctx.skip(); return; } paintHalves(); - // same LEFT-half region, expressed as a Rect (pos) — exercises the - // region.pos branch of the coordinate resolver - const frame = renderer.toFrameTexture({ + // a real Bounds over the LEFT half (x2 y2 w8 h8) + const bounds = new Bounds([ + { x: 2, y: 2 }, + { x: 10, y: 10 }, + ]); + const a = renderer.toFrameTexture({ target: null, region: bounds }); + expect(a.width).toBe(8); + expect(readCapture(a, 4, 4)[0]).toBeGreaterThan(200); // red (left) + a.destroy(); + + // the documented migration path for a Rect: rect.getBounds() + const rect = new Rect(2, 2, 8, 8); + const b = renderer.toFrameTexture({ target: null, - region: { pos: { x: 2, y: 2 }, width: 8, height: 8 }, + region: rect.getBounds(), }); - expect(frame.width).toBe(8); - const px = readCapture(frame, 4, 4); - expect(px[0]).toBeGreaterThan(200); // red (left) - frame.destroy(); + expect(b.width).toBe(8); + expect(readCapture(b, 4, 4)[0]).toBeGreaterThan(200); // red (left) + b.destroy(); }); it("reallocates when the backing handle goes stale (context-loss self-heal)", (ctx) => { From e6838f7420a2064836dcc68438abdd15d6cdf5fb Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Thu, 9 Jul 2026 10:51:35 +0800 Subject: [PATCH 19/20] test(toFrameTexture): dedicated adversarial rect.getBounds() region test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the combined test: one for a plain Bounds, and a dedicated Rect migration-path test that proves the Rect's ACTUAL position and size drive the capture — getBounds() reflects x/y/width/height (non-square, to catch a swap); a right-half Rect reads blue, a left-half Rect reads red, each sized to the Rect. Verified RED when the region x offset is dropped. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- packages/melonjs/tests/toframetexture.spec.js | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/melonjs/tests/toframetexture.spec.js b/packages/melonjs/tests/toframetexture.spec.js index 3493a195e..8e48582b2 100644 --- a/packages/melonjs/tests/toframetexture.spec.js +++ b/packages/melonjs/tests/toframetexture.spec.js @@ -211,7 +211,7 @@ describe("WebGLRenderer.toFrameTexture", () => { right.destroy(); }); - it("accepts a Bounds region — and a Rect via getBounds()", (ctx) => { + it("accepts a Bounds region", (ctx) => { if (!isWebGL) { ctx.skip(); return; @@ -224,18 +224,54 @@ describe("WebGLRenderer.toFrameTexture", () => { ]); const a = renderer.toFrameTexture({ target: null, region: bounds }); expect(a.width).toBe(8); + expect(a.height).toBe(8); expect(readCapture(a, 4, 4)[0]).toBeGreaterThan(200); // red (left) a.destroy(); + }); - // the documented migration path for a Rect: rect.getBounds() - const rect = new Rect(2, 2, 8, 8); - const b = renderer.toFrameTexture({ + // the documented migration path for a Rect region: pass rect.getBounds(). + // Adversarial: prove the Rect's ACTUAL position + size drive the capture — + // a right-half Rect must read blue, a left-half Rect red, each sized to the + // Rect (so a dropped offset or a wrong size can't pass). + it("captures the exact region a Rect describes, via rect.getBounds()", (ctx) => { + if (!isWebGL) { + ctx.skip(); + return; + } + paintHalves(); // framebuffer: left half red, right half blue + + // sanity: getBounds() reflects the Rect's position + size (non-square, to + // catch a width/height swap) + const rightRect = new Rect(SIZE / 2 + 2, 4, 8, 6); + const rb = rightRect.getBounds(); + expect(rb.x).toBe(SIZE / 2 + 2); + expect(rb.y).toBe(4); + expect(rb.width).toBe(8); + expect(rb.height).toBe(6); + + // a Rect fully in the RIGHT half → blue, sized 8x6. If the x offset were + // dropped it would capture the left (red); a width/height swap → 6x8. + const right = renderer.toFrameTexture({ + target: null, + region: rightRect.getBounds(), + }); + expect(right.width).toBe(8); + expect(right.height).toBe(6); + const rpx = readCapture(right, 4, 3); + expect(rpx[2]).toBeGreaterThan(200); // blue + expect(rpx[0]).toBeLessThan(60); + right.destroy(); + + // the mirror case: a Rect fully in the LEFT half → red — proves the + // position (not a constant) selects what's captured + const left = renderer.toFrameTexture({ target: null, - region: rect.getBounds(), + region: new Rect(2, 4, 8, 6).getBounds(), }); - expect(b.width).toBe(8); - expect(readCapture(b, 4, 4)[0]).toBeGreaterThan(200); // red (left) - b.destroy(); + const lpx = readCapture(left, 4, 3); + expect(lpx[0]).toBeGreaterThan(200); // red + expect(lpx[2]).toBeLessThan(60); + left.destroy(); }); it("reallocates when the backing handle goes stale (context-loss self-heal)", (ctx) => { From f50a26e401d6ec9bb157ff9414c04541f3734748 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Thu, 9 Jul 2026 12:13:52 +0800 Subject: [PATCH 20/20] =?UTF-8?q?chore(toFrameTexture):=20final=20PR=20rev?= =?UTF-8?q?iew=20=E2=80=94=20CHANGELOG,=20RGB=20comment,=20invalidate=20gu?= =?UTF-8?q?ard,=20aquarium=20leak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review: - CHANGELOG: the setTexture + shader-preloading bullets still pointed at the removed "Water Refraction" example — updated to Aquarium / Heat Haze (matches the README). - test comment: the capture texture is RGB (opaque), not RGBA — corrected. - _prepareTextures: only invalidateTextureUnit when the sampler actually bound; a skipped live bind (capture not ready) clobbers nothing, so invalidating other batchers' caches was needless extra rebinds. - Aquarium example: destroy() its NoiseTexture2d on teardown (was leaking the baked canvas across stage reloads, like the Heat Haze fix). Full suite green, lint 0, build 8/8. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A --- .../src/examples/aquarium/ExampleAquarium.tsx | 5 +++++ packages/melonjs/CHANGELOG.md | 4 ++-- .../melonjs/src/video/webgl/shadereffect.js | 17 ++++++++++++----- packages/melonjs/tests/toframetexture.spec.js | 3 ++- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/examples/src/examples/aquarium/ExampleAquarium.tsx b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx index b45ac378f..3b33669a1 100644 --- a/packages/examples/src/examples/aquarium/ExampleAquarium.tsx +++ b/packages/examples/src/examples/aquarium/ExampleAquarium.tsx @@ -185,6 +185,7 @@ class PlayScreen extends Stage { private elapsed = 0; private effect!: ShaderEffect; private panel?: HTMLDivElement; + private noise?: NoiseTexture2d; onResetEvent(app: Application) { const w = app.viewport.width; @@ -210,6 +211,7 @@ class PlayScreen extends Stage { domainWarpAmp: 8, seamless: true, }); + this.noise = noise; // the refraction effect, preloaded as a "shader" asset this.effect = loader.getShader("aquariumWater") as ShaderEffect; @@ -286,6 +288,9 @@ class PlayScreen extends Stage { onDestroyEvent() { loader.unload({ name: "aquariumWater", type: "shader" }); + // free the baked NoiseTexture2d canvas (engine texture asset) + this.noise?.destroy(); + this.noise = undefined; this.panel?.remove(); } } diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index c3a374f76..a96f29ee2 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -10,9 +10,9 @@ - **Procedural noise: `Noise` + `NoiseTexture2d`** — `me.Noise` is a renderer-free coherent-noise generator (simplex / perlin / value / valueCubic / cellular, with fBm, ridged and ping-pong fractal layering, plus optional domain warp) that you can sample on the CPU via `getNoise2d` / `getNoise3d` for gameplay (heightmaps, terrain, spawn jitter) — fully deterministic per `seed`. `me.NoiseTexture2d` is a `Texture2d` that bakes a `Noise` field into a drawable canvas, with three output modes: grayscale, a `Gradient` **color ramp**, or a tangent-space **normal map** (`asNormalMap` + `bumpStrength`) for per-pixel lighting; optional `seamless` tiling (`seamlessBlendSkirt`); and **live animation** (`animated` + `speed` — sampled in 3D, advanced by `update(dt)`). Each re-bake bumps a content `version` stamped on the canvas; the WebGL lit pipeline re-uploads the normal-map texture only when that version changes (the three.js `Texture.needsUpdate` model — no renderer handle needed). Both work under the Canvas and WebGL backends. - **`shared` flag on `ShaderEffect` / `GLShader`** — set `effect.shared = true` on a post-processing shader that is reused across several renderables, so one renderable's cleanup (the `shader` setter, `removePostEffect`, `clearPostEffects`, or `destroy()`) doesn't free the GL program the others still use. Defaults to `false` (unchanged auto-destroy behavior); when set, you own the shader's lifecycle and call `destroy()` yourself. - **`ShaderEffect.setTime(seconds)`** — a convenience for the `uTime` shader-animation convention, now on the base class: call it once per frame to write a shader's `uniform float uTime` (e.g. to scroll a static texture's UVs, or pulse / wave), driven by whatever clock you choose. A no-op when the shader doesn't declare `uTime`, so it's safe on any effect. Manual by design — the engine never calls it, so animation stays opt-in. The built-in `Hologram` / `Shine` effects now inherit it (their identical `setTime` overrides were removed). -- **`ShaderEffect.setTexture(name, image[, repeat])`** (closes [#1532](https://github.com/melonjs/melonJS/issues/1532)) — bind an **extra** `sampler2D` to a custom post-effect, beyond the sprite/target it processes (`uSampler`) — a noise map, mask, gradient, or flow table. Declare `uniform sampler2D ;` in your fragment and pass any engine texture — a `Texture2d` asset (`NoiseTexture2d`, `TextureAtlas`, …) directly, or a raw drawable source; the engine uploads it once, caches it, and (re)binds it to a reserved texture unit each draw while pointing the sampler uniform at it — no raw WebGL texture-unit juggling. Works on both post-effect paths (single-effect sprite shader and the multi-effect / camera FBO blit). Enables UV-distortion water, dissolve masks, flow maps, palette lookups, etc. Purely additive — effects that never call it are unchanged. See the new **Water Refraction** example (a perspective pool floor refracted through a GPU-scrolled seamless `NoiseTexture2d`). +- **`ShaderEffect.setTexture(name, image[, repeat])`** (closes [#1532](https://github.com/melonjs/melonJS/issues/1532)) — bind an **extra** `sampler2D` to a custom post-effect, beyond the sprite/target it processes (`uSampler`) — a noise map, mask, gradient, or flow table. Declare `uniform sampler2D ;` in your fragment and pass any engine texture — a `Texture2d` asset (`NoiseTexture2d`, `TextureAtlas`, …) directly, or a raw drawable source; the engine uploads it once, caches it, and (re)binds it to a reserved texture unit each draw while pointing the sampler uniform at it — no raw WebGL texture-unit juggling. Works on both post-effect paths (single-effect sprite shader and the multi-effect / camera FBO blit). Enables UV-distortion water, dissolve masks, flow maps, palette lookups, etc. Purely additive — effects that never call it are unchanged. See the **Aquarium** example (a water surface refracting the live scene through a GPU-scrolled seamless `NoiseTexture2d`). - **Anchor-point presets + `Sprite3d` anchor support** (#1514) — `settings.anchorPoint` now accepts the named presets `"center"`, `"top"`, `"bottom"`, `"left"`, `"right"`, `"top-left"`, `"top-right"`, `"bottom-left"`, `"bottom-right"` in addition to an `{x, y}` object, uniformly across `Sprite`, `Entity`, `Collectable`, `ImageLayer`, `Text`, `BitmapText` (and their subclasses), including as a plain-string Tiled property. `Sprite3d` gains anchoring through the same `settings.anchorPoint` key (centered default, same convention as `Sprite`): the anchor is baked into the quad's local vertices so it composes with billboarding, flips and trimmed/rotated atlas frames, never leaks into the renderer transform on either camera path, is mutable at runtime via `sprite3d.anchorPoint.set(...)` (automatic re-bake + cull-bounds update), and grows the frustum-cull bounds exactly so an anchored sprite can't pop out while still on screen. On the 2D classes invalid values (unknown preset, malformed object) keep their historical silent `(0, 0)` outcome for full backward compatibility, but now log a console warning; `Sprite3d` (a new surface) throws. See the updated billboard example (`anchorPoint: "bottom"` — feet on the ground plane). -- **Shader preloading: `"shader"` asset type + `loader.getShader()` + `ShaderEffect.clone()`** — custom shaders can now be preloaded like any other asset and are **compiled at load time** (the GLSL compile cost lands in the loading screen, and a compile error fails the load — `loader.load()` rejects with an error carrying the asset's name). The source is a GLSL fragment body following the `ShaderEffect` convention (`uniform` declarations + `vec4 apply(vec4 color, vec2 uv)`), loaded from a `src` URL / data: URI or inline via the `data` field (the inline-TMX convention): `{ name: "waterRipple", type: "shader", src: "shaders/waterRipple.frag" }`. `loader.getShader(name)` returns the **shared, loader-owned instance** — the *same* object on every call, with `shared = true` so renderable cleanup never auto-destroys it; everything it is assigned to shares one set of uniform values, and it is freed only by `loader.unload()` / `loader.unloadAll()` (which destroy the GL program). For per-renderable uniform values, the new `ShaderEffect.clone()` compiles an independent, caller-owned copy: it copies the *recipe* (fragment source, precision, uniform values, `setTexture` bindings — with its own GL uploads) but never the *ownership* — the clone's `shared` flag is **always reset to `false`**, so it is auto-destroyed with the renderable it is assigned to unless explicitly re-shared. `GLShader.clone()` exists likewise for raw full-program (vertex + fragment) shaders, with the same recipe-not-ownership semantics. The **Water Refraction** example now ships its fragment as a preloaded shader asset. +- **Shader preloading: `"shader"` asset type + `loader.getShader()` + `ShaderEffect.clone()`** — custom shaders can now be preloaded like any other asset and are **compiled at load time** (the GLSL compile cost lands in the loading screen, and a compile error fails the load — `loader.load()` rejects with an error carrying the asset's name). The source is a GLSL fragment body following the `ShaderEffect` convention (`uniform` declarations + `vec4 apply(vec4 color, vec2 uv)`), loaded from a `src` URL / data: URI or inline via the `data` field (the inline-TMX convention): `{ name: "waterRipple", type: "shader", src: "shaders/waterRipple.frag" }`. `loader.getShader(name)` returns the **shared, loader-owned instance** — the *same* object on every call, with `shared = true` so renderable cleanup never auto-destroys it; everything it is assigned to shares one set of uniform values, and it is freed only by `loader.unload()` / `loader.unloadAll()` (which destroy the GL program). For per-renderable uniform values, the new `ShaderEffect.clone()` compiles an independent, caller-owned copy: it copies the *recipe* (fragment source, precision, uniform values, `setTexture` bindings — with its own GL uploads) but never the *ownership* — the clone's `shared` flag is **always reset to `false`**, so it is auto-destroyed with the renderable it is assigned to unless explicitly re-shared. `GLShader.clone()` exists likewise for raw full-program (vertex + fragment) shaders, with the same recipe-not-ownership semantics. The **Aquarium** and **Heat Haze** examples ship their fragments as preloaded shader assets. ### Fixed - **`loader.unloadAll()` threw if any fontface was loaded, and never actually freed videos** — two copy-paste type strings: the video-cache sweep dispatched entries as `type: "json"` (the lookup missed, so video assets survived every `unloadAll`), and the font-cache sweep used `type: "font"` — an unknown type that hit the loader's `unload` error path and **aborted the whole `unloadAll` mid-way**. Both now route through their correct types (`"video"` / `"fontface"`). Also fixed an always-true `typeof typeof globalThis.document` guard in the fontface unload case. diff --git a/packages/melonjs/src/video/webgl/shadereffect.js b/packages/melonjs/src/video/webgl/shadereffect.js index 2a4b6f400..dcf6747db 100644 --- a/packages/melonjs/src/video/webgl/shadereffect.js +++ b/packages/melonjs/src/video/webgl/shadereffect.js @@ -346,6 +346,7 @@ export default class ShaderEffect { cache.reserveUnit(nextUnit); } nextUnit = entry.unit - 1; + let bound = false; if (entry.live === true) { // live GPU-resident source (frame capture): bind the current // handle fresh each draw — the renderer refreshes it in place, @@ -355,6 +356,7 @@ export default class ShaderEffect { if (glTex !== null && typeof glTex !== "undefined") { batcher.bindTexture2D(glTex, entry.unit, false); this._shader.setUniform(name, entry.unit); + bound = true; } } else { if (entry.tex === null) { @@ -375,12 +377,17 @@ export default class ShaderEffect { batcher.bindTexture2D(entry.tex, entry.unit, false); } this._shader.setUniform(name, entry.unit); + bound = true; + } + // only when we actually bound: this sampler took a reserved high unit + // DIRECTLY (bypassing the shared texture cache), which overlaps the + // lit quad batcher's normal-map units — invalidate it on the OTHER + // batchers so a later lit draw re-binds its normal there instead of + // sampling this texture. A skipped live bind (no capture yet) clobbers + // nothing, so there's nothing to invalidate. + if (bound) { + batcher.renderer.invalidateTextureUnit(entry.unit, batcher); } - // this sampler was bound to a reserved high unit DIRECTLY (bypassing - // the shared texture cache), which overlaps the lit quad batcher's - // normal-map units — invalidate it on the OTHER batchers so a later - // lit draw re-binds its normal there instead of sampling this texture - batcher.renderer.invalidateTextureUnit(entry.unit, batcher); } } diff --git a/packages/melonjs/tests/toframetexture.spec.js b/packages/melonjs/tests/toframetexture.spec.js index 8e48582b2..206844f1e 100644 --- a/packages/melonjs/tests/toframetexture.spec.js +++ b/packages/melonjs/tests/toframetexture.spec.js @@ -64,7 +64,8 @@ describe("WebGLRenderer.toFrameTexture", () => { }; // read a capture's content back directly: attach its GL texture to a scratch - // FBO (the RGBA capture texture is colour-renderable) and readPixels + // FBO (the RGB capture texture is colour-renderable) and readPixels — RGBA + // readback of an RGB texture returns alpha = 255 (opaque) const readCapture = (frame, px = SIZE / 2, py = SIZE / 2) => { const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb);

okWT>6`wS+Ex;aGpmld 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%0DJ-6Oise$A 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