From 227cb944be246ca17e04c60821c67e82f73302bd Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 11:04:26 -0700 Subject: [PATCH 001/210] docs: begin Strata terrain hardening epic --- ...-bakes-headlessly-into-an-instanced-glb.md | 95 +++++++++++++++ ...a-forest-over-mcp-and-gets-a-60-fps-glb.md | 112 ++++++++++++++++++ .../PRD-585-strata-proof-runs-in-ci.md | 60 ++++++++++ ...-the-terrain-editor-never-drops-an-edit.md | 67 +++++++++++ ...588-the-terrain-editor-is-typed-modules.md | 65 ++++++++++ ...terrain-package-ships-code-not-kit-data.md | 60 ++++++++++ ...D-590-one-starter-world-source-not-five.md | 56 +++++++++ ...-stops-reimplementing-engine-mechanisms.md | 64 ++++++++++ ...ls-closed-and-erodes-without-allocating.md | 57 +++++++++ ...generated-and-the-upload-lane-leaves-it.md | 64 ++++++++++ ...es-from-381-get-native-and-visual-proof.md | 66 +++++++++++ ...splits-by-seam-and-survives-a-race-fuzz.md | 49 ++++++++ docs/PRDs/strata-terrain-hardening/README.md | 28 +++++ 13 files changed, 843 insertions(+) create mode 100644 docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md create mode 100644 docs/PRDs/strata-terrain-hardening/PRD-584-an-agent-asks-for-a-forest-over-mcp-and-gets-a-60-fps-glb.md create mode 100644 docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md create mode 100644 docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md create mode 100644 docs/PRDs/strata-terrain-hardening/PRD-588-the-terrain-editor-is-typed-modules.md create mode 100644 docs/PRDs/strata-terrain-hardening/PRD-589-the-terrain-package-ships-code-not-kit-data.md create mode 100644 docs/PRDs/strata-terrain-hardening/PRD-590-one-starter-world-source-not-five.md create mode 100644 docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md create mode 100644 docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md create mode 100644 docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md create mode 100644 docs/PRDs/strata-terrain-hardening/PRD-594-engine-changes-from-381-get-native-and-visual-proof.md create mode 100644 docs/PRDs/strata-terrain-hardening/PRD-595-world-cells-splits-by-seam-and-survives-a-race-fuzz.md create mode 100644 docs/PRDs/strata-terrain-hardening/README.md diff --git a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md new file mode 100644 index 000000000..710f8be90 --- /dev/null +++ b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md @@ -0,0 +1,95 @@ +# PRD-583 — A Strata world bakes headlessly into an instanced GLB + +**Status:** NOT STARTED +**Priority:** P1 — No headless path turns a Strata recipe into a GLB with its trees and rocks; PRD-584's agent handoff cannot start without AC-1 to AC-4. +**Complexity:** 5 (MEDIUM); risk override: none +**Owner:** ThreeNative maintainers +**Depends on:** None +**Parent:** [PRD-466](../done/PRD-466-strata-terrain-threejs-integration.md), [PRD-468](../done/PRD-468-strata-world-controls-and-asset-imports.md) +**Epic:** [Strata terrain hardening](README.md) + +## Context + +The owner's goal for Strata (2026-10-05): another agent says "I am making an xyz game, generate a W +terrain" and gets "a glb or something ready to use". Audit of `2d2124792` (2026-10-10) found three +gaps on that path: + +- `exportWorldGLB` (`packages/terrain/src/export.ts`) needs browser `FileReader` and canvas + (`packages/terrain/README.md`), so a Node job or an MCP server cannot call it. +- The same encoder rejects instancing: `export.ts:167-169` throws "resolve skinning/instancing + into static meshes first". A forest of 10 000 trees becomes 10 000 mesh nodes, so 10 000 draws + in the consumer. +- `bakeWorldPackage` (`packages/terrain/src/core/world-package.ts:221`) is headless and writes + `heightmap.u16`, `placements.bin`, `splat.rgba` and `world.json`, but no GLB, no LOD and no + cell proxy. The legacy `makeExport(..., "glb")` holds terrain geometry only. + +The cook in `@threenative/assets` already holds every piece this needs, in Node: gltf-transform +(`packages/assets/package.json:49-51`), the `EXT_mesh_gpu_instancing` compaction +(`packages/assets/src/passes/compact.ts:104`), meshopt simplification (`modelPass({ simplify })`) +and the per-cell HLOD proxy (`packages/assets/src/world/proxy.ts`). + +## Solution + +Add one cook entry in `@threenative/assets` (it carries the glTF dependencies the terrain package +must not inherit): `cookTerrainWorld({ state, species, out })`. + +1. Terrain: `bakeTerrain(state)` chunks become GLB meshes with the kit's PBR layer maps, written as + image bytes that already exist (JPG/PNG/KTX2). No canvas, no `FileReader`. +2. Scatter: each species GLB is read once. Placements of one species and one LOD become one node + with `EXT_mesh_gpu_instancing`. LOD levels come from the species' own `*-mid`/`*-lod1` GLBs when + present, else from `modelPass({ simplify })`. +3. Output: `world.glb` (single file, loads in vanilla `GLTFLoader`) and the existing world package + directory with the species GLBs and LODs filled in, for `WorldCells` streaming. +4. Every placement samples the baked heightmap. A tree whose base misses the ground by more than + one heightmap texel fails the cook by name (catches a flipped Z or transposed grid; audit H4). + +`exportWorldGLB` stays the browser editor's encoder for now. When the cook is the one path the +editor uses too, it is deleted (PRD-588 decides). + +```mermaid +flowchart LR + R[recipe.json] --> E[Terrain.evaluate] + E --> B[bakeTerrain + bakeWorldPackage] + S[species GLBs + LODs] --> C[cookTerrainWorld - @threenative/assets] + B --> C + C --> G[world.glb - EXT_mesh_gpu_instancing] + C --> P[world/ package - WorldCells] +``` + +## Acceptance Criteria + +- [ ] AC-1 [local]: `cookTerrainWorld` runs under plain Node 22 with no DOM globals and writes `world.glb` for the forest kit. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: pending. +- [ ] AC-2 [local]: Each species-and-LOD pair is one `EXT_mesh_gpu_instancing` node; node count does not grow with tree count. proof: same spec, glTF node census at 1 000 and 10 000 trees — Evidence: pending. +- [ ] AC-3 [local]: A vanilla three `GLTFLoader` loads `world.glb` into `InstancedMesh` objects whose instance count equals the bake's placement count. proof: `pnpm --filter strata-terrain-preview test:consumer` — Evidence: pending. +- [ ] AC-4 [local]: A placement mirrored in Z fails the cook with a named contact error. proof: spec case in `terrain-world.spec.ts` — Evidence: pending. +- [ ] AC-5 [local]: The world package written next to it passes `validateWorldPackage` and lists a GLB and at least one LOD per species. proof: `terrain-world.spec.ts` — Evidence: pending. + +## Integration Ledger + +| Capability | Reachable consumer/trigger | Replaces / disposition | Evidence | +|---|---|---|---| +| Headless world GLB | `cookTerrainWorld` from `@threenative/assets`; kit `bake.mjs`; PRD-584 MCP tool | Browser `exportWorldGLB` stays for the editor until PRD-588 | AC-1, AC-3 | +| Instanced scatter in GLB | Any `GLTFLoader` consumer | Per-tree mesh nodes | AC-2 | + +## Execution Phases + +#### Phase 1: terrain and scatter into one instanced GLB +**Status:** NOT STARTED +**Files:** `packages/assets/src/world/terrain-world.ts` (new), `packages/assets/src/index.ts`, `packages/assets/__tests__/terrain-world.spec.ts` +**Implementation:** read `bakeTerrain` chunks and placements; group by species and LOD; write `EXT_mesh_gpu_instancing` nodes with gltf-transform; pass existing image bytes through; contact check per placement. +- [ ] Node-only cook writes `world.glb`. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` +- [ ] Node count is constant from 1 000 to 10 000 trees. proof: same spec +- [ ] Mirrored placements fail by name. proof: same spec + +#### Phase 2: LODs and the world package +**Status:** NOT STARTED +**Files:** `packages/assets/src/world/terrain-world.ts`, `packages/terrain/starter/forest/bake.mjs` +**Implementation:** prefer authored `*-mid` / `*-lod1` GLBs; fall back to `modelPass({ simplify })`; fill `assets[].glb` and `lods` in `world.json`. +- [ ] World package validates and carries LODs per species. proof: `terrain-world.spec.ts` +- [ ] The forest kit bake emits both outputs. proof: `node packages/terrain/starter/forest/bake.mjs` then the spec's file check + +#### Phase 3: consumer load +**Status:** NOT STARTED +**Files:** `examples/strata-terrain-preview/scripts/verify-consumer.mjs` +**Implementation:** load the cooked `world.glb` with stock `GLTFLoader`; assert instance counts and that no mesh is a per-tree copy. +- [ ] Vanilla loader gets `InstancedMesh` with the bake's counts. proof: `pnpm --filter strata-terrain-preview test:consumer` diff --git a/docs/PRDs/strata-terrain-hardening/PRD-584-an-agent-asks-for-a-forest-over-mcp-and-gets-a-60-fps-glb.md b/docs/PRDs/strata-terrain-hardening/PRD-584-an-agent-asks-for-a-forest-over-mcp-and-gets-a-60-fps-glb.md new file mode 100644 index 000000000..0104cda9d --- /dev/null +++ b/docs/PRDs/strata-terrain-hardening/PRD-584-an-agent-asks-for-a-forest-over-mcp-and-gets-a-60-fps-glb.md @@ -0,0 +1,112 @@ +# PRD-584 — An agent asks for a forest over MCP and gets a 60 fps GLB + +**Status:** NOT STARTED +**Priority:** P1 — The epic's headline outcome: no MCP tool generates terrain today, and no measured number shows a handed-over Strata world holding 60 fps (AC-1 to AC-5). +**Complexity:** 7 (HIGH); risk override: closed-list boundary — a request-to-world tool sits next to the closed "preset/genre system" item +**Owner:** ThreeNative maintainers +**Depends on:** [PRD-583](PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md) +**Epic:** [Strata terrain hardening](README.md) + +## Context + +Owner, 2026-10-10: "the AI agent could call some MCP endpoint like terrain_generation => +'Generate a forest for me' => and then instantly it's generated and a .glb is provided, and the +game can use it at min 60fps performance." + +Today, on `2d2124792`: + +- No MCP server exposes terrain. `packages/core/mcp/servers.mjs` wires `threenative-assets`, + `-sculpt`, `-engine` and `-blender`. None of them generates terrain. +- `engine_search_capabilities("generate a forest terrain with trees and rocks for my game and + load it as a GLB", scope "request")` returns `Heightfield`, `getWorldCapabilities` and + `TerrainTiles` first. `exportWorldGLB` is rank 7 (score 0.28) and needs a browser. +- The forest kit's trees come from licensed local-only assets (`examples/strata-terrain-preview/AGENTS.md`). + CI and a user machine see procedural stand-ins. +- No playtest loads a handed-over Strata world in an ordinary game and measures it. The + preview's own FPS reflects its own renderer (owner, 2026-10-05: judge the exported world as + an ordinary game loads it). + +## Solution + +A fifth core-wired MCP server, `threenative-terrain`, follows the blender pattern: a shim in +`packages/core/mcp/terrain.mjs`, registered in `MCP_SERVERS`, launching a server bundled from +`packages/terrain`. It costs nothing until a tool is called. + +Tools: + +- `terrain_generate({ request, sizeMeters?, seed?, out? })` — picks one starter recipe + (`forest`, `coastal`, `alpine`, `desert`, `tundra`) from the request, sets size and seed, runs + `Terrain.evaluate` and PRD-583's `cookTerrainWorld` in a child process with a timeout (the + evaluator only checks `AbortSignal` between operations; `AGENT_GUIDE.md` "Bounds"), and writes + `public/worlds//world.glb`, `recipe.json` and the world package. It returns the paths, + triangle, instance and draw counts, and a three-line load snippet. +- `terrain_patch({ recipe, commands })` reuses the editor's command vocabulary (`upsert`, + `update`, `remove`, `move`, `toggle`) so the agent edits the returned recipe instead of + re-prompting. + +Species GLBs are CC0. They are fetched on first use through the existing asset MCP download path +and cached, so the tool does not depend on PRD-589's tarball diet. + +The return names the starter recipe it chose. The recipe is editable game data, not a hidden +preset. That keeps the CHARTER's terrain allowance ("editable terrain starter documents") and +does not add a genre system. Phase 1 adds one line to that allowance naming this tool. + +The 60 fps claim is measured on a stock template game, not the preview: the starter template +loads `world.glb` with `ctx.load`, walks a fixed path, and the playtest asserts frame time. + +```mermaid +sequenceDiagram + participant A as Game agent + participant M as threenative-terrain MCP + participant W as child process + participant G as starter template game + A->>M: terrain_generate("a pine forest, 512 m") + M->>W: evaluate(recipe) + cookTerrainWorld + W-->>M: world.glb, recipe.json, world/ + M-->>A: paths, counts, load snippet + A->>G: ctx.load("worlds/forest/world.glb") + G-->>A: playtest: p95 frame <= 16.7 ms +``` + +## Acceptance Criteria + +- [ ] AC-1 [local]: `tools/list` on the shipped shim lists `terrain_generate` and `terrain_patch`. proof: stdio spec beside the sculpt shim's `tools/list` test in `packages/core/__tests__` — Evidence: pending. +- [ ] AC-2 [local]: `terrain_generate({ request: "a pine forest" })` writes a `world.glb` that `GLTFLoader` loads, cold, within 60 s on the reference CPU. proof: `packages/terrain/__tests__/terrain-mcp.spec.ts` with timing — Evidence: pending. +- [ ] AC-3 [local]: `engine_search_capabilities` ranks the terrain tool or `cookTerrainWorld` first for "generate a forest for my game". proof: a case in the PRD-297 capability-recall gate — Evidence: pending. +- [ ] AC-4 [local]: The starter template loads the generated forest and holds p95 frame time ≤ 16.7 ms over a 300-frame walk on a named real GPU (WebGPU, `adapter.info` recorded). proof: `node packages/playtest/dist/runner/cli.js .playtest.json --browser-recipe webgpu` — Evidence: pending. +- [ ] AC-5 [local]: The same scenario holds p95 ≤ 16.7 ms on the native desktop host. proof: same scenario `--target desktop` — Evidence: pending. + +## Blocked on + +- An Android 60 fps claim needs the shared Pixel lane — unblocked by João lending the device (memory: ask for the Pixel). The emulator cannot hold a frame-time claim. + +## Integration Ledger + +| Capability | Reachable consumer/trigger | Replaces / disposition | Evidence | +|---|---|---|---| +| Terrain MCP server | `.mcp.json` written by installing `@threenative/core` → `mcp/terrain.mjs` | New | AC-1 | +| Request to world | `terrain_generate` → child process → `cookTerrainWorld` | Hand-run kit `bake.mjs` | AC-2 | +| Discovery | `engine_search_capabilities` | Rank 7 browser-only result | AC-3 | +| 60 fps handoff | starter template `ctx.load(world.glb)` | Preview-only FPS numbers | AC-4, AC-5 | + +## Execution Phases + +#### Phase 1: the tool generates a GLB +**Status:** NOT STARTED +**Files:** `packages/terrain/src/mcp/server.ts` (new), `packages/core/mcp/terrain.mjs` (new), `packages/core/mcp/servers.mjs`, `packages/core/scripts/bundle-terrain-mcp.mjs` (new), `docs/architecture/CHARTER.md` +**Implementation:** stdio MCP server; request → starter recipe by keyword with the choice returned; child process with timeout; CC0 species fetched through the asset MCP download path and cached. +- [ ] Shim lists both tools. proof: stdio `tools/list` spec +- [ ] A forest request writes a loadable GLB inside 60 s. proof: `terrain-mcp.spec.ts` + +#### Phase 2: agents find it +**Status:** NOT STARTED +**Files:** `packages/terrain` capability entries, `packages/create-threenative/capabilities.json` (generated), template `AGENTS.md` terrain line +**Implementation:** capability entry with situation "generate a forest/terrain/world for my game"; template `AGENTS.md` names the tool. +- [ ] Search ranks the tool first. proof: capability-recall gate case + +#### Phase 3: a stock game holds 60 fps on it +**Status:** NOT STARTED +**Files:** `packages/create-threenative/template-playtests/` (new forest scenario) +**Implementation:** scaffold the starter template, generate a forest with the tool, load it, walk 300 frames, assert p95 frame time. +- [ ] Web WebGPU p95 ≤ 16.7 ms. proof: playtest `--browser-recipe webgpu` +- [ ] Native desktop p95 ≤ 16.7 ms. proof: playtest `--target desktop` diff --git a/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md new file mode 100644 index 000000000..d1bb3d8db --- /dev/null +++ b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md @@ -0,0 +1,60 @@ +# PRD-585 — Strata's proof runs in CI, not only by hand + +**Status:** NOT STARTED +**Priority:** P1 — No Strata playtest, consumer check or editor script runs in CI, so every later PRD in this epic can regress unseen (AC-1 to AC-5). +**Complexity:** 4 (MEDIUM); risk override: none +**Owner:** ThreeNative maintainers +**Depends on:** None +**Epic:** [Strata terrain hardening](README.md) + +## Context + +Audit of `2d2124792` (2026-10-10): + +- `.github/workflows/ci.yml:125-126` and `:275-277` run only `pnpm --filter strata-terrain-preview bake`. + No `test:terrain:*`, `test:kit`, `test:consumer` or `verify-*.mjs` runs in any workflow. +- `examples/strata-terrain-preview/package.json:12-20` wires 7 `verify-*.mjs` scripts; 10 more + are chained from them, undocumented. `playtests/walk.playtest.json`, `scripts/measure-*.mjs`, + `check-water.mjs`, `check-temperate.mts` and `consumer-world.mjs` are referenced by nothing. +- `packages/terrain/__tests__/forest-runtime.spec.ts:11` mocks `WorldCells.load`. No test loads + a baked terrain world package through a real `WorldCells` (core audit D2). +- `editor.playtest.json` (60 lines) checks a vertex count and a screenshot. It does not + exercise a tool, stroke, undo or save. + +## Solution + +Add one job to the existing `ci.yml` (no new workflow file; root `AGENTS.md`): `strata` runs +`test:terrain:web`, `test:consumer` and `test:terrain:editor` against the selected-scope rules in +`scripts/ci-change-scope.mjs`, so it runs when `packages/terrain/`, the example or the world +modules in core change. Add a real `WorldCells` integration spec. Delete the orphan scripts or +index them in the example's `AGENTS.md`. + +## Acceptance Criteria + +- [ ] AC-1 [shared]: The `strata` job runs `test:terrain:web` green on a PR touching `packages/terrain/`. proof: CI run link — Evidence: pending. +- [ ] AC-2 [shared]: The same job runs `test:consumer` and `test:terrain:editor` green. proof: CI run link — Evidence: pending. +- [ ] AC-3 [local]: The change-scope selects the job for terrain, example and `world-cells` paths and skips it for an unrelated template. proof: `pnpm exec vitest run scripts/__tests__/ci-structure.spec.ts` — Evidence: pending. +- [ ] AC-4 [local]: A baked 3×3-cell package loads through a real `WorldCells`; walking across cells gives resident cells and instance counts equal to the bake. proof: `pnpm exec vitest run packages/core/__tests__/world-cells-terrain.spec.ts` — Evidence: pending. +- [ ] AC-5 [local]: Every file under the example's `scripts/` and `playtests/` is wired from `package.json` or named in its `AGENTS.md`. proof: a spec listing the folder against both — Evidence: pending. + +## Integration Ledger + +| Capability | Reachable consumer/trigger | Replaces / disposition | Evidence | +|---|---|---|---| +| Strata CI job | PR touching terrain paths → `ci.yml` `strata` | Bake-only steps | AC-1, AC-2 | +| Real package load | `bakeWorldPackage` → `WorldCells.load` | Mocked `WorldCells.load` | AC-4 | + +## Execution Phases + +#### Phase 1: integration spec and script census +**Status:** NOT STARTED +**Files:** `packages/core/__tests__/world-cells-terrain.spec.ts` (new), `examples/strata-terrain-preview/AGENTS.md`, orphan scripts (delete or index) +- [ ] Real `WorldCells` loads a baked package. proof: `pnpm exec vitest run packages/core/__tests__/world-cells-terrain.spec.ts` +- [ ] No unwired script or playtest is left. proof: census spec + +#### Phase 2: the CI job +**Status:** NOT STARTED +**Files:** `.github/workflows/ci.yml`, `scripts/ci-change-scope.mjs`, `scripts/__tests__/ci-structure.spec.ts` +- [ ] Scope selects and skips correctly. proof: `pnpm exec vitest run scripts/__tests__/ci-structure.spec.ts scripts/__tests__/ci-needs.spec.ts` +- [ ] Web playtest green in CI. proof: CI run link +- [ ] Consumer and editor scripts green in CI. proof: CI run link diff --git a/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md b/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md new file mode 100644 index 000000000..91b2a6dd9 --- /dev/null +++ b/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md @@ -0,0 +1,67 @@ +# PRD-587 — The terrain editor never drops an edit + +**Status:** NOT STARTED +**Priority:** P1 — A second stroke during a save is silently rolled back and a failed save overwrites the local edit without saying what was lost (AC-1, AC-2): user work is lost. +**Complexity:** 4 (MEDIUM); risk override: none +**Owner:** ThreeNative maintainers +**Depends on:** None +**Parent:** [PRD-467](../done/PRD-467-strata-live-terrain-editor.md) +**Epic:** [Strata terrain hardening](README.md) + +## Context + +Editor audit of `2d2124792` (2026-10-10), `packages/terrain/src/editor/`: + +- `app.js:360-393` (`changed()`) POSTs the whole document on every edit and waits. While a save + is in flight, a new edit is reverted with `terrain.loadJSON(saved…)` and the toast "Wait for the + current save" (`app.js:362-367`). The keyboard handler drops undo/redo the same way (`app.js:1013`). +- On a failed save (`app.js:379-392`) the editor refetches and replaces the local edit with no + record of what was lost. +- A malformed `world.json` at startup throws in the `TerrainEditorDocument` constructor + (`server.ts:156-160`) and takes down the Vite dev server. After a later bad external save, + `commit` answers 409 "restore a valid save before editing" (`server.ts:187-188`), and the UI + offers no way to restore. +- A crashed worker stays referenced; the next `build()` posts to it (`app.js:106-160`), and its + error text ("Serve the source over HTTP…", `app.js:154`) names the wrong cause. +- The worker returns the evaluated state by structured clone with no transfer list + (`worker.js:13`), so every build copies every typed array. +- The server already accepts `commands` (`upsert`, `update`, `remove`, `move`, `toggle`), but the + browser sends whole documents. + +## Solution + +Queue edits in the browser and send them as `commands`, coalesced, with the last-known revision. +A conflict rebases the queued commands on the new revision and retries once. A rejected command +stays in a visible "not saved" list with retry and discard. The server starts on a malformed file +in a read-only diagnostic state, and the UI offers "reload from disk" and "overwrite disk with +the last valid state". The worker is recreated after a crash. Typed arrays transfer. + +## Acceptance Criteria + +- [ ] AC-1 [local]: Two strokes 50 ms apart both persist to `world.json`. proof: a stroke step in `examples/strata-terrain-preview/playtests/editor.playtest.json` — Evidence: pending. +- [ ] AC-2 [local]: A save answered 409 leaves the edit in a visible "not saved" list with retry and discard. proof: `pnpm exec vitest run packages/terrain/__tests__/editor-save-queue.spec.ts` — Evidence: pending. +- [ ] AC-3 [local]: The dev server starts on a malformed `world.json`, and "reload from disk" recovers after the file is fixed. proof: `editor-document.spec.ts` case — Evidence: pending. +- [ ] AC-4 [local]: After the worker is terminated mid-build, the next edit rebuilds terrain. proof: `editor.playtest.json` step — Evidence: pending. +- [ ] AC-5 [local]: The worker's height, normal and splat buffers arrive transferred (detached on the worker side). proof: `editor-save-queue.spec.ts` or a worker spec — Evidence: pending. + +## Integration Ledger + +| Capability | Reachable consumer/trigger | Replaces / disposition | Evidence | +|---|---|---|---| +| Command save queue | Brush stroke → `changed()` → POST `commands` | Whole-document POST, discard while saving | AC-1, AC-2 | +| Invalid-file recovery | Dev server start → editor UI buttons | Server crash / dead-end 409 | AC-3 | + +## Execution Phases + +#### Phase 1: commands and the queue +**Status:** NOT STARTED +**Files:** `packages/terrain/src/editor/app.js`, `packages/terrain/src/editor/server.ts`, `packages/terrain/__tests__/editor-save-queue.spec.ts` (new) +- [ ] Rapid strokes persist. proof: `editor.playtest.json` +- [ ] Rejected edits stay visible. proof: `editor-save-queue.spec.ts` + +#### Phase 2: recovery paths +**Status:** NOT STARTED +**Files:** `packages/terrain/src/editor/server.ts`, `app.js`, `worker.js` +- [ ] Malformed file starts and recovers. proof: `editor-document.spec.ts` +- [ ] Worker crash recovers. proof: `editor.playtest.json` +- [ ] Buffers transfer. proof: worker spec diff --git a/docs/PRDs/strata-terrain-hardening/PRD-588-the-terrain-editor-is-typed-modules.md b/docs/PRDs/strata-terrain-hardening/PRD-588-the-terrain-editor-is-typed-modules.md new file mode 100644 index 000000000..dada0f4ae --- /dev/null +++ b/docs/PRDs/strata-terrain-hardening/PRD-588-the-terrain-editor-is-typed-modules.md @@ -0,0 +1,65 @@ +# PRD-588 — The terrain editor is typed modules, not one 1481-line closure + +**Status:** NOT STARTED +**Priority:** P2 — `app.js` stays untyped, string-built and mouse-only until AC-1 to AC-5 land; it slows every editor fix after PRD-587. +**Complexity:** 5 (MEDIUM); risk override: none +**Owner:** ThreeNative maintainers +**Depends on:** [PRD-587](PRD-587-the-terrain-editor-never-drops-an-edit.md) (lands its save model first, so the port moves settled code) +**Parent:** [PRD-467](../done/PRD-467-strata-live-terrain-editor.md) +**Epic:** [Strata terrain hardening](README.md) + +## Context + +Editor audit of `2d2124792` (2026-10-10): + +- `packages/terrain/src/editor/app.js` is 1481 lines of plain JS in one `mountRecoveredEditor` + closure with about 30 mutable `let` values. The rest of `src/editor/` is strict TypeScript. +- UI is built with `innerHTML` template strings (`app.js:416-457`, `1040-1046`, `1192`, `1269`); + `escapeHTML` is applied in some places, not in others (`app.js:748`). +- Accessibility: about one `aria-` attribute (`app.js:238`), no `role`, toasts disappear after + 4.3 s with no `aria-live`, tools have no keyboard path beyond digit hotkeys. +- Tuned constants have no names: `strength * 22`, `amplitude: strength * 130` (`app.js:529,544`), + 2048 stroke points, 35 ms debounce. +- Generic editor mechanics (picking, gizmo, brush, spline, placement overrides) live in the + example: `examples/strata-terrain-preview/src/render/editorView.ts` (1019) and `selection.ts` (722). +- Server hygiene: one 64 MiB body cap for every route (`server.ts:241`); every error response + carries a full `document.snapshot()` (`server.ts` catch block); assets served as + `application/octet-stream` (`server.ts:446-452`). +- Two encoders: browser `exportWorldGLB` and PRD-583's headless `cookTerrainWorld`. + +## Solution + +Port `app.js` to TypeScript first with no behaviour change, then split it by panel (toolbar, +tools, layers, inspector, camera, environment, assets, save/build). Build DOM with +`document.createElement`/`textContent`, not strings. Move picking, gizmo and brush mechanics from +the example into `packages/terrain/src/editor/`, and leave materials and colours in the example. +Route the editor's export button through `cookTerrainWorld` and delete `exportWorldGLB` if the +cook covers its contract. React is not required by this PRD; decide it after the split. + +## Acceptance Criteria + +- [ ] AC-1 [local]: `src/editor/` has no `.js` source and passes `tsc` strict. proof: `pnpm --filter @threenative/terrain typecheck` — Evidence: pending. +- [ ] AC-2 [local]: No `innerHTML` assignment remains in `src/editor/`. proof: a spec that greps the source — Evidence: pending. +- [ ] AC-3 [local]: Every tool is reachable and usable by keyboard, and errors are announced through an `aria-live` region. proof: `editor.playtest.json` keyboard steps — Evidence: pending. +- [ ] AC-4 [local]: Picking, gizmo and brush mechanics live in the package; the example keeps only appearance. proof: `pnpm tsx scripts/count-loc.ts` before/after plus `pnpm --filter strata-terrain-preview test:terrain:editor` — Evidence: pending. +- [ ] AC-5 [local]: Small routes reject a 10 MiB body with 413, and error responses carry no document snapshot. proof: `editor-document.spec.ts` cases — Evidence: pending. +- [ ] AC-6 [local]: The editor's export uses `cookTerrainWorld`, and `exportWorldGLB` is deleted or names its remaining caller. proof: `pnpm --filter strata-terrain-preview test:terrain:export` — Evidence: pending. + +## Execution Phases + +#### Phase 1: typed and split +**Status:** NOT STARTED +**Files:** `packages/terrain/src/editor/app.js` → `app/*.ts` +- [ ] Strict TS, no JS. proof: typecheck +- [ ] No `innerHTML`. proof: grep spec + +#### Phase 2: mechanics move, server hygiene +**Status:** NOT STARTED +**Files:** `editorView.ts`, `selection.ts` (example) → `packages/terrain/src/editor/`; `server.ts` +- [ ] Mechanics in package. proof: count-loc + editor script +- [ ] Route limits and slim errors. proof: `editor-document.spec.ts` + +#### Phase 3: keyboard, one encoder +**Status:** NOT STARTED +- [ ] Keyboard and `aria-live`. proof: `editor.playtest.json` +- [ ] One encoder. proof: `test:terrain:export` diff --git a/docs/PRDs/strata-terrain-hardening/PRD-589-the-terrain-package-ships-code-not-kit-data.md b/docs/PRDs/strata-terrain-hardening/PRD-589-the-terrain-package-ships-code-not-kit-data.md new file mode 100644 index 000000000..8bb9ea7d3 --- /dev/null +++ b/docs/PRDs/strata-terrain-hardening/PRD-589-the-terrain-package-ships-code-not-kit-data.md @@ -0,0 +1,60 @@ +# PRD-589 — `@threenative/terrain` ships code, not 33 MB of kit data + +**Status:** NOT STARTED +**Priority:** P2 — Every `npm i @threenative/terrain` downloads 32.6 MB of kit data and four 1.3 MB recipes, whether or not the game uses a kit (AC-1, AC-2). +**Complexity:** 4 (MEDIUM); risk override: none +**Owner:** ThreeNative maintainers +**Depends on:** None +**Epic:** [Strata terrain hardening](README.md) + +## Context + +Package and core audits of `2d2124792` (2026-10-10): + +- `npm pack --dry-run` in `packages/terrain`: 32.6 MB packed, 40.7 MB unpacked. `package.json` + `files` ships `starter/` (5.7 MB) and `starter-assets/` (34 MB, 15 MB of it `fir_tree_01`). +- Four `starter/*/recipe.json` files are 66k lines (about 1.3 MB) each, because a `heightmap` + layer inlines 257×257 numbers. `forest` has no inline array and is 4.5k lines. +- `starter-assets/forest_leaves_02` and `grass_medium_02` are referenced by nothing found; + `bark_brown_02`, `cliff_side`, `fern_02`, `lichen_rock` and `river_small_rocks` are used only + by the example. +- `src/core/io.ts:~403` writes the glTF generator string `"Strata Terrain 0.1.0"`. +- Registration: `scripts/prepare-release.ts` `syncPeerRanges` has no terrain entry. +- CHARTER: the new terrain allowance (owner, 2026-09-30) admits terrain-only editor companions, + but the closed-list row "An editor | Not in v1…" (`docs/architecture/CHARTER.md:102`) was not + reworded, and root `AGENTS.md` "Where a change goes" has no terrain row. + +## Solution + +Move kit binaries out of the runtime tarball: the scaffolder copies a kit into the game on +request (the same path PRD-584's MCP tool uses), and species GLBs come through the asset MCP +download path. Store inline heightmaps as RAW16 sidecars that `decodeRAW16` already reads, with +the recipe naming the file. Delete unreferenced assets and move example-only ones into the +example. Fix the generator string, the peer-range sync and the CHARTER row. + +## Acceptance Criteria + +- [ ] AC-1 [local]: The packed tarball is under 2 MB, enforced by a cap in `pnpm budgets`. proof: `pnpm budgets` — Evidence: pending. +- [ ] AC-2 [local]: Every `recipe.json` is under 200 KB, and each kit bake's output bytes are unchanged by the sidecar move. proof: `pnpm exec vitest run packages/terrain/__tests__/starter-kit.spec.ts` with a golden hash — Evidence: pending. +- [ ] AC-3 [local]: A scaffolded game that asks for the forest kit gets its recipe and assets, and the kit playtest passes. proof: `KIT=forest pnpm --filter strata-terrain-preview test:kit` — Evidence: pending. +- [ ] AC-4 [local]: No asset directory without a reference remains. proof: an asset-reference spec — Evidence: pending. +- [ ] AC-5 [local]: `prepare-release` syncs terrain's peer ranges, and the CHARTER closed-list row names the terrain allowance. proof: `pnpm check:docs` and the package-lists drift spec — Evidence: pending. + +## Execution Phases + +#### Phase 1: recipes shrink, bytes hold +**Status:** NOT STARTED +**Files:** `packages/terrain/starter/*/recipe.json`, sidecar `.r16` files, `starter-kit.spec.ts` +- [ ] Recipes under 200 KB, same bake bytes. proof: `starter-kit.spec.ts` + +#### Phase 2: kits leave the tarball +**Status:** NOT STARTED +**Files:** `packages/terrain/package.json` `files`, `packages/create-threenative/src/` kit copy, `starter-assets/` moves +- [ ] Tarball under 2 MB. proof: `pnpm budgets` +- [ ] Kit still scaffolds and plays. proof: `test:kit` +- [ ] No unreferenced assets. proof: asset-reference spec + +#### Phase 3: registration and charter +**Status:** NOT STARTED +**Files:** `scripts/prepare-release.ts`, `docs/architecture/CHARTER.md`, `AGENTS.md`, `src/core/io.ts` +- [ ] Peer sync and charter row. proof: `pnpm check:docs` + drift spec diff --git a/docs/PRDs/strata-terrain-hardening/PRD-590-one-starter-world-source-not-five.md b/docs/PRDs/strata-terrain-hardening/PRD-590-one-starter-world-source-not-five.md new file mode 100644 index 000000000..2234e0a0c --- /dev/null +++ b/docs/PRDs/strata-terrain-hardening/PRD-590-one-starter-world-source-not-five.md @@ -0,0 +1,56 @@ +# PRD-590 — One starter-world source, not five copies + +**Status:** NOT STARTED +**Priority:** P2 — About 6 000 lines of near-copies across kits, fixtures and specs remain until AC-1 to AC-4 land; a fix in one kit misses the other four. +**Complexity:** 4 (MEDIUM); risk override: none +**Owner:** ThreeNative maintainers +**Depends on:** None +**Epic:** [Strata terrain hardening](README.md) + +## Context + +Package and example audits of `2d2124792` (2026-10-10), diff lines measured against alpine: + +- `packages/terrain/starter/*/bake.mjs`: five files of 118 lines that differ by 2 lines each. +- `starter/*/world.ts`: 335–413 lines each; desert differs from alpine by 34 lines, tundra by 67, + coastal by 95. About 2 800 lines, mostly shared loader and streaming code. +- `examples/strata-terrain-preview/scripts/fixtures/kit-game/{Alpine,Desert,Tundra,Coastal,Forest}.ts`: + 2 651 lines; Alpine and Desert differ by 0 lines after a biome-name swap. +- `packages/terrain/__tests__/{alpine,coastal,desert,tundra}-runtime.spec.ts` (335–346 lines, + about 75 % identical) and the four `*-kit.spec.ts` files (61–74 lines): about 1 400 lines. +- Inverted dependency: seven `packages/terrain/__tests__` specs import example source + (`examples/strata-terrain-preview/src/render/{loading,props,propStreaming,pack,propMaterials,texturePreparation}.js`, + `viewStatistics.js`, `scripts/fixtures/kit-game/*`). The package cannot test without the example. + +`sky.ts` per kit is real per-world look data and stays per kit. + +## Solution + +One `bakeKit(dir)` in `@threenative/terrain` (or its `bin`); each kit's `bake.mjs` becomes a +three-line call. One `addStarterWorld(spec)` holds the shared world loader; each kit's `world.ts` +keeps only its look data. One `KitScene` fixture parameterised by kit. One `describe.each(kits)` +runtime spec with a table of per-kit expectations. Package specs that need example code move into +the example, or the code they test moves into the package (PRD-591). + +## Acceptance Criteria + +- [ ] AC-1 [local]: Each kit `bake.mjs` is at most 10 lines, and all five kits bake the same bytes as before. proof: `pnpm exec vitest run packages/terrain/__tests__/starter-kit.spec.ts` with golden hashes — Evidence: pending. +- [ ] AC-2 [local]: Five kit playtests pass on one `KitScene` fixture. proof: `pnpm --filter strata-terrain-preview test:kit` for each `KIT` — Evidence: pending. +- [ ] AC-3 [local]: One table-driven runtime spec replaces the four per-kit runtime specs with the same assertions. proof: `pnpm --filter @threenative/terrain test` — Evidence: pending. +- [ ] AC-4 [local]: No file under `packages/terrain/__tests__` imports from `examples/`. proof: a grep spec — Evidence: pending. +- [ ] AC-5 [local]: Net line count of kits, fixtures and specs drops by at least 4 000. proof: `pnpm tsx scripts/count-loc.ts` before/after — Evidence: pending. + +## Execution Phases + +#### Phase 1: bake and world source +**Status:** NOT STARTED +**Files:** `packages/terrain/src/kit.ts` (new), `starter/*/bake.mjs`, `starter/*/world.ts` +- [ ] Same bytes from one bake helper. proof: `starter-kit.spec.ts` + +#### Phase 2: fixtures and specs +**Status:** NOT STARTED +**Files:** `scripts/fixtures/kit-game/*`, `packages/terrain/__tests__/*-runtime.spec.ts`, `*-kit.spec.ts` +- [ ] Kit playtests on one fixture. proof: `test:kit` +- [ ] Table-driven spec. proof: `pnpm --filter @threenative/terrain test` +- [ ] No package spec imports the example. proof: grep spec +- [ ] Line count drops by 4 000+. proof: `count-loc.ts` diff --git a/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md b/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md new file mode 100644 index 000000000..f61617456 --- /dev/null +++ b/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md @@ -0,0 +1,64 @@ +# PRD-591 — The preview stops re-implementing engine mechanisms + +**Status:** NOT STARTED +**Priority:** P2 — A second prop LOD pipeline, a spawn-readiness gate and the scatter loop stay in example code where every terrain game must copy them (AC-1 to AC-4). +**Complexity:** 6 (MEDIUM); risk override: visual output changes, so the owner's visual-judge rule applies +**Owner:** ThreeNative maintainers +**Depends on:** [PRD-585](PRD-585-strata-proof-runs-in-ci.md) (the CI job guards the moves) +**Epic:** [Strata terrain hardening](README.md) + +## Context + +Example audit of `2d2124792` (2026-10-10), `examples/strata-terrain-preview/src/render/`: + +- `props.ts:119, 627, 798-897` hand-writes an LOD ladder (`LOD_BANDS`, `levelFor` with + hysteresis, `setLevels`, one `InstancedBatch` per variant, level and role). The game path + (`game.ts:736`) already uses `createStreamedProps` on `WorldCells`; only `editorView.ts:766` + calls `createProps`. Two prop pipelines exist, and `packages/terrain/__tests__/prop-lod.spec.ts` + (647 lines) tests the older one. +- `loading.ts:69-193`: `createSpawnReadiness` / `spawnReadinessSnapshot`, a spawn gate every + streamed terrain game repeats. +- `scatter.ts:299-340`: the rejection-sampling loop and spacing grid repeat for every terrain + game. Per-biome magic numbers (`treeLimit` 3200/600/55/0, `spruceAttempts: 80000`, + `scatter.ts:563-568`). It runs synchronously in `enter` (`game.ts:591`), not sliced. +- `terrain.ts` (1191 lines; `createGroundMaterial` about 430) does not use `loadTerrainSplat`, + which `packages/terrain/starter/forest/world.ts:4, 121` already uses. Height bands are constants + (`SNOW {145,195}`, `SHORE {7.5,1}`) instead of values measured from the world's height range. +- `propMaterials.ts` (1131) and `pack.ts` (838) each define wind, foliage tint and cutout; `pack.ts:144` + and `propMaterials.ts:282-310` repeat the wind hash. + +Mechanism (pooling, LOD, culling, streaming, readiness) belongs in `packages/core`; appearance +stays in game `src/render/` (root `AGENTS.md`, "Where a change goes"). + +## Solution + +The editor view uses the streamed prop path; delete the ladder and its spec. Move spawn readiness +into `WorldCells` as `whenSpawnReady(position)`. Ship a generic `scatter(rules)` in +`@threenative/terrain` and keep rules in game source; run it in the bake, not in `enter`. Compare +the hand-written ground against `loadTerrainSplat` under the visual judge and keep the plumbing +from the engine. Derive height bands from the measured height range. One `foliageSurface()` for +both material paths. + +## Acceptance Criteria + +- [ ] AC-1 [local]: The editor draws props through the streamed path, and `levelFor`/`setLevels` are deleted. proof: `pnpm --filter strata-terrain-preview test:terrain:editor` plus `count-loc.ts` — Evidence: pending. +- [ ] AC-2 [local]: `WorldCells.whenSpawnReady` gates the preview's spawn, and the example's readiness module is deleted. proof: `pnpm exec vitest run packages/core/__tests__/world-cells-data.spec.ts` plus `test:terrain:web` — Evidence: pending. +- [ ] AC-3 [local]: Scatter runs in the bake, and `enter` does no scatter work. proof: `test:terrain:web` asserting no long task over 50 ms during `enter` — Evidence: pending. +- [ ] AC-4 [local]: Ground plumbing uses `loadTerrainSplat`, and height bands come from the world's measured range. proof: `test:terrain:web` — Evidence: pending. +- [ ] AC-5 [local]: A fresh visual judge rates the before/after captures of all five kits NEUTRAL or IMPROVEMENT (same pose, headed WebGPU, two runs per arm). proof: judge verdict and images on the PR — Evidence: pending. + +## Execution Phases + +#### Phase 1: one prop pipeline +**Status:** NOT STARTED +- [ ] Ladder deleted; editor on streamed props. proof: editor script + count-loc + +#### Phase 2: readiness and scatter +**Status:** NOT STARTED +- [ ] Spawn readiness in `WorldCells`. proof: spec + web playtest +- [ ] Scatter off the `enter` path. proof: web playtest long-task assertion + +#### Phase 3: ground and look check +**Status:** NOT STARTED +- [ ] Engine splat plumbing, measured bands. proof: web playtest +- [ ] Visual judge passes. proof: judge verdict on the PR diff --git a/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md b/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md new file mode 100644 index 000000000..a19ce7bbf --- /dev/null +++ b/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md @@ -0,0 +1,57 @@ +# PRD-592 — Terrain core fails closed and erodes without allocating + +**Status:** NOT STARTED +**Priority:** P2 — Bad bake options produce NaN cells silently, and hydraulic erosion allocates about 8 million objects per layer, which slows every bake, including PRD-584's (AC-1 to AC-5). +**Complexity:** 3 (LOW); risk override: none +**Owner:** ThreeNative maintainers +**Depends on:** None +**Epic:** [Strata terrain hardening](README.md) + +## Context + +Package audit of `2d2124792` (2026-10-10), `packages/terrain/src/core/`: + +- `world-package.ts` `placementCells` / `bakeWorldPackage`: `cellSize` 0 gives `across = Infinity`; + a negative or NaN `cellSize` or a NaN position gives a `"NaN,NaN"` cell key. Out-of-extent + placements are clamped into edge cells, not reported. `placementRecord` checks rotation and scale + but not position. +- `io.ts:~273` `decodeHeightPNG`: `JSON.parse` of the metadata chunk is not checked to be an + object; `null` throws a plain `TypeError`, not the documented error. +- `erosion.ts`: `thermal` divides by `n - 1` (Infinity at `n < 2`); `droplets` and iterations + have no proven upper bound. +- `operations.ts`: `scatter` defaults `asset ?? "pine"` and `count ?? 300`, a hidden content + default in a package (root `AGENTS.md` "never own the look"); `s.instances.push(...accepted)` + can overflow the stack at large counts. +- Hot loops: hydraulic `get()` returns a new object twice per droplet step (default `n*n` droplets + × up to 64 steps ≈ 8 M objects per layer at `n = 257`); `thermal` builds a 4-element array per + cell per iteration; `splatBytes` runs `COMPONENTS.indexOf` in its innermost loop. +- `math.ts` `hashString` hashes only the first UTF-16 unit of astral characters. Changing it moves + existing placements, so it needs a golden hash first. + +## Solution + +Pin golden hashes of one bake per kit first. Then validate options and fail by name; require +`scatter.asset`; replace the spread push; rewrite the erosion and splat inner loops with scratch +state and precomputed indices so that output bytes stay identical. Record bake time before and +after on the same host. + +## Acceptance Criteria + +- [ ] AC-1 [local]: Golden hashes of each kit's bake are pinned and unchanged by this PRD. proof: `pnpm exec vitest run packages/terrain/__tests__/starter-kit.spec.ts` — Evidence: pending. +- [ ] AC-2 [local]: `bakeWorldPackage` throws by name for `cellSize` 0, -1 and NaN, a NaN position and an out-of-extent placement. proof: `world-package.spec.ts` cases — Evidence: pending. +- [ ] AC-3 [local]: `decodeHeightPNG` rejects `null`/number metadata, a truncated chunk and a bad CRC with the documented error. proof: new `io.spec.ts` — Evidence: pending. +- [ ] AC-4 [local]: A scatter layer without `asset` fails validation. proof: `validation` spec case — Evidence: pending. +- [ ] AC-5 [local]: Forest kit bake time drops by at least 30 % on the same host, with identical bytes. proof: `/usr/bin/time -v node packages/terrain/starter/forest/bake.mjs`, three runs per arm — Evidence: pending. + +## Execution Phases + +#### Phase 1: golden bytes and validation +**Status:** NOT STARTED +**Files:** `packages/terrain/src/core/{world-package,io,validation,operations,erosion}.ts`, specs +- [ ] Golden hashes pinned. proof: `starter-kit.spec.ts` +- [ ] Bad options throw by name. proof: `world-package.spec.ts`, `io.spec.ts`, validation spec + +#### Phase 2: allocation-free loops +**Status:** NOT STARTED +**Files:** `packages/terrain/src/core/erosion.ts`, `world-package.ts`, `operations.ts` +- [ ] Same bytes, faster bake. proof: golden spec + timed bake diff --git a/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md b/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md new file mode 100644 index 000000000..c4f3d127f --- /dev/null +++ b/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md @@ -0,0 +1,64 @@ +# PRD-593 — The three.js patch is generated, and the upload lane leaves it + +**Status:** NOT STARTED +**Priority:** P1 — #381 grew the hand-maintained three patches by about 11 000 lines on private three internals, so any three upgrade can silently break texture upload for every game (AC-1 to AC-4). +**Complexity:** 6 (MEDIUM); risk override: compatibility — a wrong patch breaks every game's renderer +**Owner:** ThreeNative maintainers +**Depends on:** None +**Epic:** [Strata terrain hardening](README.md) + +## Context + +Core audit of `2d2124792` (2026-10-10): + +- `packages/core/patches/three@0.185.1.patch` grew +630/−120 lines and + `three@0.185.1-prd269-upgrade.patch` grew from 5 620 to 15 838 lines. Each change appears up to + six times (`src/*`, `build/three.webgpu.js`, `build/three.webgpu.nodes.js`, in both files). + `packages/core/scripts/apply-three-patch.mjs` stacks them; no script generates them. +- The patch adds a bounded async compressed-texture upload lane (`Textures.js` +119, + `WebGPUTextureUtils.js` +147: `updateTextureAsync`, a promise queue, an `AbortController` per + texture, 2 ms and 64 KiB budgets), exposed as `prepareTextures` in `packages/core/src/renderer.ts`. + It reads private `_textures`, `_uploadTail` and `backend.get(texture).texture`. +- It also adds, for every game: a `_depthBuffers` cache per sample count, a 3D-texture + attachment guard for wgpu-native, bind-group invalidation on texture destroy, and an + `updateTexture` wrapper that checks `_pendingUploads` and `uploadError` on each call. Its + per-frame cost is unmeasured. +- Owner rule (memory "three patch last resort"): patching three is the last resort. + +## Solution + +1. Add a generator: a pinned three checkout plus a patched `src` tree regenerates both patch files; + the compiled-build hunks come from three's own build. CI fails when the generated patch differs + from the committed one. +2. Move the upload lane out of the patch: core creates the `GPUTexture` and writes mips with + `device.queue.writeTexture` on its own budget, then hands the texture to three through a + documented seam. If no seam exists, file the upstream issue and record the retire condition in + the patch header. +3. Split the remaining additions into named hunks with one red-green spec each, and measure the + `updateTexture` wrapper's per-frame cost. + +## Acceptance Criteria + +- [ ] AC-1 [local]: One command regenerates both patch files, and the result equals the committed files. proof: `pnpm --filter @threenative/core patch:generate && git diff --exit-code packages/core/patches` — Evidence: pending. +- [ ] AC-2 [shared]: CI fails when a patch file is edited by hand. proof: CI job step in `ci.yml` — Evidence: pending. +- [ ] AC-3 [local]: `prepareTextures` uploads compressed textures with the patch's upload-lane hunks removed. proof: `pnpm exec vitest run packages/core/__tests__/three-compressed-upload.spec.ts` plus a WebGPU playtest of the strata preview — Evidence: pending. +- [ ] AC-4 [local]: Each remaining hunk (depth cache, 3D guard, bind-group invalidation) has a spec that fails without it. proof: `pnpm exec vitest run packages/core/__tests__/three-patch-*.spec.ts` — Evidence: pending. +- [ ] AC-5 [local]: The `updateTexture` wrapper costs under 0.05 ms per frame on machinefall. proof: `TN_FRAME_SPANS` A/B, two runs per arm — Evidence: pending. + +## Execution Phases + +#### Phase 1: generated patches +**Status:** NOT STARTED +**Files:** `packages/core/scripts/generate-three-patch.mjs` (new), `packages/core/package.json`, `.github/workflows/ci.yml` +- [ ] Regeneration matches. proof: generate + `git diff --exit-code` +- [ ] CI drift check. proof: CI run link + +#### Phase 2: upload lane out of the patch +**Status:** NOT STARTED +**Files:** `packages/core/src/renderer.ts`, `packages/core/src/render/texture-upload.ts` (new), patch source tree +- [ ] Upload works without patch hunks. proof: spec + WebGPU playtest + +#### Phase 3: remaining hunks proven +**Status:** NOT STARTED +- [ ] One failing-without spec per hunk. proof: `three-patch-*.spec.ts` +- [ ] Wrapper cost measured. proof: `TN_FRAME_SPANS` A/B diff --git a/docs/PRDs/strata-terrain-hardening/PRD-594-engine-changes-from-381-get-native-and-visual-proof.md b/docs/PRDs/strata-terrain-hardening/PRD-594-engine-changes-from-381-get-native-and-visual-proof.md new file mode 100644 index 000000000..17ce7ab70 --- /dev/null +++ b/docs/PRDs/strata-terrain-hardening/PRD-594-engine-changes-from-381-get-native-and-visual-proof.md @@ -0,0 +1,66 @@ +# PRD-594 — The engine changes that rode in with #381 get native and visual proof + +**Status:** NOT STARTED +**Priority:** P1 — Six core behaviours from #381 that every game runs have no native conformance case and no visual-judge verdict (AC-1 to AC-6); "web-only is unfinished" applies. +**Complexity:** 5 (MEDIUM); risk override: supported-platform parity +**Owner:** ThreeNative maintainers +**Depends on:** None +**Epic:** [Strata terrain hardening](README.md) + +## Context + +Core audit of `2d2124792` (2026-10-10). The squash of 485 files carried engine changes beyond the +terrain package, without their own PRDs: + +- `packages/runtime-native/conformance/registry.json` has no case for WorldCells streaming, the + HLOD/impostor/shadow proxies, `prepareTextures`, virtual-shadow `renderGroup` uniforms and + `registeredOnCreate`, velocity buffer rotation or the streaming slice budget. The only native + proofs are the example's desktop scripts (not in CI), the `loading-screen-desktop` assertion and + `bindings.cpp`'s `device.lost` test. +- `render/virtual-shadow.ts` (+67/−54), `render/velocity.ts` (buffer rotation; a perf win), + `atmosphere/luts.ts` (`textureLoad` → `texture().level(0)`) change pixels. No visual-judge + verdict is recorded for them (owner rule 2026-10-09: any change to what reaches the screen). +- `game.ts` removes the `temporalUpscale` floor lift for render-chain velocity consumers, for + every game with temporal reconstruction. No spec proves the floor still holds for a TRAA chain. +- `streaming.ts` changes the public default of `addInSlices` from 256 objects to an 8 ms budget, + with no capability-detail note. +- `FrameBudget` now registers a `longtask` `PerformanceObserver` in its constructor; it is + released only from `game.ts` teardown. + +## Solution + +Add three native conformance cases and run them on the desktop host. Capture before/after at +fixed poses for the shadow, velocity and LUT changes and run the owner's visual-judge protocol. +Add the missing specs and the capability-detail note. + +## Acceptance Criteria + +- [ ] AC-1 [local]: A native conformance case streams two WorldCells cells and swaps a proxy with zero validation errors. proof: `pnpm parity` case in `registry.json` — Evidence: pending. +- [ ] AC-2 [local]: A native conformance case uploads a compressed texture through `prepareTextures`. proof: `pnpm parity` — Evidence: pending. +- [ ] AC-3 [local]: A native conformance case submits a virtual-shadow first frame with no "Destroyed texture" error. proof: `pnpm parity` — Evidence: pending. +- [ ] AC-4 [local]: A fresh visual judge rates the shadow, velocity and LUT changes NEUTRAL or IMPROVEMENT against the pre-#381 parent at fixed poses (headed WebGPU, two runs per arm). proof: judge verdict and images on the PR — Evidence: pending. +- [ ] AC-5 [local]: A TRAA chain holds its `temporalUpscale` floor without the removed lift. proof: `pnpm exec vitest run packages/core/__tests__/renderer.spec.ts` case — Evidence: pending. +- [ ] AC-6 [local]: `addInSlices`' capability detail states the 8 ms default, and `FrameBudget` disconnects its observer on `dispose()`. proof: `pnpm build` manifest diff + `frame-budget.spec.ts` case — Evidence: pending. + +## Blocked on + +- Android parity for AC-1 to AC-3 needs an emulator run with a hardware GPU mode; start the emulator before declaring this blocked (memory: Android emulator lanes are runnable here). + +## Execution Phases + +#### Phase 1: native cases +**Status:** NOT STARTED +**Files:** `packages/runtime-native/conformance/registry.json`, conformance fixtures +- [ ] WorldCells stream. proof: `pnpm parity` +- [ ] Texture upload. proof: `pnpm parity` +- [ ] Shadow first frame. proof: `pnpm parity` + +#### Phase 2: pixels judged +**Status:** NOT STARTED +- [ ] Judge verdict NEUTRAL or better. proof: PR images + verdict + +#### Phase 3: specs and notes +**Status:** NOT STARTED +**Files:** `packages/core/__tests__/renderer.spec.ts`, `frame-budget.spec.ts`, `packages/core/src/streaming.ts` doc comment +- [ ] TRAA floor spec. proof: `renderer.spec.ts` +- [ ] Slice default documented, observer released. proof: manifest + `frame-budget.spec.ts` diff --git a/docs/PRDs/strata-terrain-hardening/PRD-595-world-cells-splits-by-seam-and-survives-a-race-fuzz.md b/docs/PRDs/strata-terrain-hardening/PRD-595-world-cells-splits-by-seam-and-survives-a-race-fuzz.md new file mode 100644 index 000000000..8f6167584 --- /dev/null +++ b/docs/PRDs/strata-terrain-hardening/PRD-595-world-cells-splits-by-seam-and-survives-a-race-fuzz.md @@ -0,0 +1,49 @@ +# PRD-595 — `world-cells.ts` splits by seam and survives a load/evict race fuzz + +**Status:** NOT STARTED +**Priority:** P2 — `world-cells.ts` is 10 228 lines and its async load/evict orderings are proven for one interleaving only (AC-1 to AC-3). +**Complexity:** 5 (MEDIUM); risk override: complex async state +**Owner:** ThreeNative maintainers +**Depends on:** None (coordinate with [PRD-473](../open-world/PRD-473-open-worlds-hold-120-fps-gpu-driven.md), which edits the same file) +**Epic:** [Strata terrain hardening](README.md) + +## Context + +Core audit of `2d2124792` (2026-10-10): + +- `packages/core/src/world-cells.ts` is 10 228 lines (about 9 100 before #381). `WorldCells` + runs from line 4101 to the end with 183 private methods. The file also holds `SharedBatch` + (1098), `BundleSurfaceSafety` (3163), `ModelLoadLimiter` (3970), `AdmissionBudget` (4038) and the + shadow-proxy merge helpers (2687–3100). +- HLOD load (`world-cells.ts:9454-9490`) reserves bytes before the load and rechecks + `#cellLive`/`cancelled` after. The asset path (`8043-8068`) hands a model that finished after + `wanted()` turned false to `#adoptAsset` without a check at the call site. + `world-cells-hlod.spec.ts:394-410` covers one ordering. +- Misplaced JSDoc: two blocks back to back before `#disposeLoaded` (about `10059-10069`); in + `frame-budget.ts`, `dispose()` sits between `beginFrame`'s JSDoc and `beginFrame`. + +## Solution + +Write the race fuzz first, against today's file, so that the split is proven behaviour-preserving. +Then extract by seam with no logic change: `world-cell-batches.ts`, `world-load-limiter.ts`, +`world-bundle-safety.ts`, `world-chunk-shadow.ts`. Fix any race the fuzz finds in its own commit, +red then green. + +## Acceptance Criteria + +- [ ] AC-1 [local]: A seeded fuzz of 1 000 interleavings of load completion, eviction, `dispose()` and re-entry ends with `#bytes`, `#hlodBytes` and refcounts at 0, and each geometry disposed exactly once. proof: `pnpm exec vitest run packages/core/__tests__/world-cells-race.spec.ts` — Evidence: pending. +- [ ] AC-2 [local]: After the split, no file under `packages/core/src/world-*` exceeds 3 000 lines, and all existing world specs pass unchanged. proof: `pnpm exec vitest run packages/core/__tests__/world-*.spec.ts` plus `pnpm quality` — Evidence: pending. +- [ ] AC-3 [local]: The strata preview and machinefall walk playtests still pass after the split. proof: `pnpm --filter strata-terrain-preview test:terrain:web` and the machinefall map-walk scenario — Evidence: pending. + +## Execution Phases + +#### Phase 1: fuzz first +**Status:** NOT STARTED +**Files:** `packages/core/__tests__/world-cells-race.spec.ts` (new) +- [ ] Fuzz green on today's code, or a found race fixed red-green. proof: `world-cells-race.spec.ts` + +#### Phase 2: extract by seam +**Status:** NOT STARTED +**Files:** `packages/core/src/world-cells.ts` → four new modules +- [ ] Files under 3 000 lines, specs unchanged. proof: world specs + `pnpm quality` +- [ ] Walk playtests pass. proof: preview + machinefall scenarios diff --git a/docs/PRDs/strata-terrain-hardening/README.md b/docs/PRDs/strata-terrain-hardening/README.md new file mode 100644 index 000000000..b1d620ff8 --- /dev/null +++ b/docs/PRDs/strata-terrain-hardening/README.md @@ -0,0 +1,28 @@ +# Strata terrain hardening + +**Status:** IN PROGRESS — dedicated EPIC execution; all constituent proof boxes remain authoritative. + +**Delivery decision:** João requested one dedicated EPIC PR for this batch on 2026-10-10, overriding the usual one-PR-per-PRD delivery rule. Each constituent PRD retains its complete scope, phase checkboxes and verification requirements. + +## Execution checkout + +- Owner: Codex active terrain-hardening goal. +- Worktree: `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-hardening`. +- Branch: `epic/strata-terrain-hardening`; target: `develop`. +- Base: `origin/develop` at `2d212479255cf725f64343dc72aa2eafe9b7b92d`. +- Cleanup: retained while implementation and review are active; inspect data and merge evidence before ordinary worktree removal. + +## Scope + +- [PRD-583 — A Strata world bakes headlessly into an instanced GLB](PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md) +- [PRD-584 — An agent asks for a forest over MCP and gets a 60 fps GLB](PRD-584-an-agent-asks-for-a-forest-over-mcp-and-gets-a-60-fps-glb.md) +- [PRD-585 — Strata's proof runs in CI, not only by hand](PRD-585-strata-proof-runs-in-ci.md) +- [PRD-587 — The terrain editor never drops an edit](PRD-587-the-terrain-editor-never-drops-an-edit.md) +- [PRD-588 — The terrain editor is typed modules, not one 1481-line closure](PRD-588-the-terrain-editor-is-typed-modules.md) +- [PRD-589 — `@threenative/terrain` ships code, not 33 MB of kit data](PRD-589-the-terrain-package-ships-code-not-kit-data.md) +- [PRD-590 — One starter-world source, not five copies](PRD-590-one-starter-world-source-not-five.md) +- [PRD-591 — The preview stops re-implementing engine mechanisms](PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md) +- [PRD-592 — Terrain core fails closed and erodes without allocating](PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md) +- [PRD-593 — The three.js patch is generated, and the upload lane leaves it](PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md) +- [PRD-594 — The engine changes that rode in with #381 get native and visual proof](PRD-594-engine-changes-from-381-get-native-and-visual-proof.md) +- [PRD-595 — `world-cells.ts` splits by seam and survives a load/evict race fuzz](PRD-595-world-cells-splits-by-seam-and-survives-a-race-fuzz.md) From 9e7a8cab0686f788bc2e0fafd772ecaa0ab5a0fe Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 11:39:39 -0700 Subject: [PATCH 002/210] fix(terrain): validate bake inputs and pin starter output --- ...ls-closed-and-erodes-without-allocating.md | 18 +++-- docs/PRDs/strata-terrain-hardening/README.md | 11 +++ .../terrain/__tests__/erosion-bounds.spec.ts | 67 +++++++++++++++ packages/terrain/__tests__/io.spec.ts | 81 +++++++++++++++++++ .../__tests__/placement-overrides.spec.ts | 1 + .../__tests__/scatter-identity.spec.ts | 2 + .../terrain/__tests__/starter-kit.spec.ts | 72 +++++++++++++++-- .../__tests__/terrain-consumer.spec.ts | 9 +++ .../terrain/__tests__/world-package.spec.ts | 27 +++++++ packages/terrain/src/core/erosion.ts | 10 +++ packages/terrain/src/core/io.ts | 4 +- packages/terrain/src/core/operations.ts | 4 +- packages/terrain/src/core/terrain.ts | 2 +- packages/terrain/src/core/types.ts | 2 +- packages/terrain/src/core/validation.ts | 2 + packages/terrain/src/core/world-package.ts | 28 ++++++- 16 files changed, 318 insertions(+), 22 deletions(-) create mode 100644 packages/terrain/__tests__/erosion-bounds.spec.ts create mode 100644 packages/terrain/__tests__/io.spec.ts diff --git a/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md b/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md index a19ce7bbf..ca1d14ac5 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md @@ -1,6 +1,6 @@ # PRD-592 — Terrain core fails closed and erodes without allocating -**Status:** NOT STARTED +**Status:** PARTIAL **Priority:** P2 — Bad bake options produce NaN cells silently, and hydraulic erosion allocates about 8 million objects per layer, which slows every bake, including PRD-584's (AC-1 to AC-5). **Complexity:** 3 (LOW); risk override: none **Owner:** ThreeNative maintainers @@ -37,21 +37,23 @@ after on the same host. ## Acceptance Criteria -- [ ] AC-1 [local]: Golden hashes of each kit's bake are pinned and unchanged by this PRD. proof: `pnpm exec vitest run packages/terrain/__tests__/starter-kit.spec.ts` — Evidence: pending. -- [ ] AC-2 [local]: `bakeWorldPackage` throws by name for `cellSize` 0, -1 and NaN, a NaN position and an out-of-extent placement. proof: `world-package.spec.ts` cases — Evidence: pending. -- [ ] AC-3 [local]: `decodeHeightPNG` rejects `null`/number metadata, a truncated chunk and a bad CRC with the documented error. proof: new `io.spec.ts` — Evidence: pending. -- [ ] AC-4 [local]: A scatter layer without `asset` fails validation. proof: `validation` spec case — Evidence: pending. +- [x] AC-1 [local]: Golden hashes of each kit's bake are pinned and unchanged by this PRD. proof: `pnpm exec vitest run packages/terrain/__tests__/starter-kit.spec.ts` — Evidence: rebuilt candidate passes 9 starter-kit tests, including 20 pinned hashes across forest, alpine, coastal, desert and tundra; parent ran terrain build before the golden tests. +- [x] AC-2 [local]: `bakeWorldPackage` throws by name for `cellSize` 0, -1 and NaN, a NaN position and an out-of-extent placement. proof: `world-package.spec.ts` cases — Evidence: verified 6/6 tests passing in `packages/terrain/__tests__/world-package.spec.ts`. +- [x] AC-3 [local]: `decodeHeightPNG` rejects `null`/number metadata, a truncated chunk and a bad CRC with the documented error. proof: new `io.spec.ts` — Evidence: verified 4/4 tests passing in `packages/terrain/__tests__/io.spec.ts`. +- [x] AC-4 [local]: A scatter layer without `asset` fails validation. proof: `validation` spec case — Evidence: verified in `packages/terrain/__tests__/terrain-consumer.spec.ts` rejecting scatter without asset. - [ ] AC-5 [local]: Forest kit bake time drops by at least 30 % on the same host, with identical bytes. proof: `/usr/bin/time -v node packages/terrain/starter/forest/bake.mjs`, three runs per arm — Evidence: pending. ## Execution Phases #### Phase 1: golden bytes and validation -**Status:** NOT STARTED +**Status:** PARTIAL **Files:** `packages/terrain/src/core/{world-package,io,validation,operations,erosion}.ts`, specs -- [ ] Golden hashes pinned. proof: `starter-kit.spec.ts` -- [ ] Bad options throw by name. proof: `world-package.spec.ts`, `io.spec.ts`, validation spec +- [x] Golden hashes pinned. proof: `starter-kit.spec.ts` (pass, verified 5/5 starter kits with 4 pinned sha256 hashes each against HEAD baseline byte-for-byte) +- [ ] Bad options throw by name. proof: `world-package.spec.ts`, `io.spec.ts`, validation spec, `erosion-bounds.spec.ts` — 252 terrain tests and package/workspace typecheck pass; finite/noninteger checks and zero/large-grid defaults pass. Remaining: establish a compatible practical upper bound on hydraulic droplets; a finite-integer guard alone does not bound bake work. #### Phase 2: allocation-free loops **Status:** NOT STARTED **Files:** `packages/terrain/src/core/erosion.ts`, `world-package.ts`, `operations.ts` - [ ] Same bytes, faster bake. proof: golden spec + timed bake + +Parent verification (2026-10-10): `pnpm exec vitest run packages/terrain/__tests__` passed 34 files / 252 tests; `pnpm --filter @threenative/terrain typecheck`, `pnpm typecheck`, terrain build and Biome checks passed (14 warnings, no errors). The original 513-grid droplet-cap regression failed with `RangeError` before repair and the new bounds spec covers 513/1025 defaults after repair. Phase 1 remains partial until the hydraulic work bound is proven; phase 2 and the three-runs-per-arm 30% speed proof remain unrun. diff --git a/docs/PRDs/strata-terrain-hardening/README.md b/docs/PRDs/strata-terrain-hardening/README.md index b1d620ff8..cf46977b1 100644 --- a/docs/PRDs/strata-terrain-hardening/README.md +++ b/docs/PRDs/strata-terrain-hardening/README.md @@ -9,6 +9,7 @@ - Owner: Codex active terrain-hardening goal. - Worktree: `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-hardening`. - Branch: `epic/strata-terrain-hardening`; target: `develop`. +- Draft EPIC: [PR #483](https://github.com/ThreeNativeHQ/threenative-engine/pull/483). - Base: `origin/develop` at `2d212479255cf725f64343dc72aa2eafe9b7b92d`. - Cleanup: retained while implementation and review are active; inspect data and merge evidence before ordinary worktree removal. @@ -26,3 +27,13 @@ - [PRD-593 — The three.js patch is generated, and the upload lane leaves it](PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md) - [PRD-594 — The engine changes that rode in with #381 get native and visual proof](PRD-594-engine-changes-from-381-get-native-and-visual-proof.md) - [PRD-595 — `world-cells.ts` splits by seam and survives a load/evict race fuzz](PRD-595-world-cells-splits-by-seam-and-survives-a-race-fuzz.md) + +## Execution order + +Start with PRD-592 golden bytes and validation, then execute 585, 583, 587, 593, 591, 594, 589, 590, 595, 584 and 588. This order serializes shared-file edits; all phases and acceptance criteria in each PRD remain required. PRD-584 requires 583; 588 export consolidation also requires 583. Coordinate world-cell edits with PRD-473 before changing its shared implementation. + +Current baseline: `pnpm build` and `pnpm check:docs` passed; six documentation contract spec files passed (244 tests). No runtime or performance acceptance is established by those checks. + +## Active worker checkout + +PRD-585 phase 1 has an isolated execution checkout at `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-proof`, branch `epic/strata-terrain-proof`, based on EPIC commit `227cb944b`. Codex owns integration into PR #483. Its first arm was interrupted before edits to serialize Gemini provider use with the PRD-592 repair. Dependencies and focused package builds passed; its phase-1 arm resumed after that repair terminated. Cleanup must inspect ignored data and confirm its owner has stopped after integration. diff --git a/packages/terrain/__tests__/erosion-bounds.spec.ts b/packages/terrain/__tests__/erosion-bounds.spec.ts new file mode 100644 index 000000000..d88f7639f --- /dev/null +++ b/packages/terrain/__tests__/erosion-bounds.spec.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; +import { hydraulic, thermal } from "../src/core/erosion.js"; + +describe("erosion grid and parameter bounds validation", () => { + it("thermal erosion rejects n < 2, fractional or non-finite grid resolution", () => { + const h = new Float32Array(4); + expect(() => thermal(new Float32Array(1), 1, 10)).toThrow(RangeError); + expect(() => thermal(h, -1, 10)).toThrow(RangeError); + expect(() => thermal(h, 2.5, 10)).toThrow(RangeError); + expect(() => thermal(h, Number.NaN, 10)).toThrow(RangeError); + expect(() => thermal(h, Number.POSITIVE_INFINITY, 10)).toThrow(RangeError); + }); + + it("thermal erosion rejects out-of-bound, fractional or non-finite iterations", () => { + const h4 = new Float32Array(4); + expect(() => thermal(h4, 2, 10, { iterations: -1 })).toThrow(RangeError); + expect(() => thermal(h4, 2, 10, { iterations: 201 })).toThrow(RangeError); + expect(() => thermal(h4, 2, 10, { iterations: 1.5 })).toThrow(RangeError); + expect(() => thermal(h4, 2, 10, { iterations: Number.NaN })).toThrow(RangeError); + expect(() => thermal(h4, 2, 10, { iterations: Number.POSITIVE_INFINITY })).toThrow(RangeError); + }); + + it("thermal erosion preserves no-op zero and boundary 200 iterations", () => { + const h4 = new Float32Array([1, 2, 3, 4]); + const zero = thermal(h4, 2, 10, { iterations: 0 }); + expect(zero).toEqual(h4); + expect(() => thermal(h4, 2, 10, { iterations: 200 })).not.toThrow(); + }); + + it("hydraulic erosion rejects n < 2, fractional or non-finite grid resolution", () => { + const h = new Float32Array(4); + expect(() => hydraulic(new Float32Array(1), 1, 10)).toThrow(RangeError); + expect(() => hydraulic(h, -1, 10)).toThrow(RangeError); + expect(() => hydraulic(h, 2.5, 10)).toThrow(RangeError); + expect(() => hydraulic(h, Number.NaN, 10)).toThrow(RangeError); + expect(() => hydraulic(h, Number.POSITIVE_INFINITY, 10)).toThrow(RangeError); + }); + + it("hydraulic erosion rejects out-of-bound, fractional or non-finite droplets and steps", () => { + const h4 = new Float32Array(4); + expect(() => hydraulic(h4, 2, 10, { droplets: -1 })).toThrow(RangeError); + expect(() => hydraulic(h4, 2, 10, { droplets: 1.5 })).toThrow(RangeError); + expect(() => hydraulic(h4, 2, 10, { droplets: Number.NaN })).toThrow(RangeError); + expect(() => hydraulic(h4, 2, 10, { droplets: Number.POSITIVE_INFINITY })).toThrow(RangeError); + + expect(() => hydraulic(h4, 2, 10, { maxSteps: 0 })).toThrow(RangeError); + expect(() => hydraulic(h4, 2, 10, { maxSteps: 129 })).toThrow(RangeError); + expect(() => hydraulic(h4, 2, 10, { maxSteps: 1.5 })).toThrow(RangeError); + expect(() => hydraulic(h4, 2, 10, { maxSteps: Number.NaN })).toThrow(RangeError); + expect(() => hydraulic(h4, 2, 10, { maxSteps: Number.POSITIVE_INFINITY })).toThrow(RangeError); + }); + + it("hydraulic erosion supports no-op zero droplets and boundary 128 maxSteps", () => { + const h4 = new Float32Array([1, 2, 3, 4]); + const zero = hydraulic(h4, 2, 10, { droplets: 0 }); + expect(zero).toEqual(h4); + expect(() => hydraulic(h4, 2, 10, { droplets: 1, maxSteps: 128 })).not.toThrow(); + }); + + it("hydraulic erosion accepts large supported grid defaults (513 and 1025)", () => { + const flat513 = new Float32Array(513 * 513); + expect(() => hydraulic(flat513, 513, 512, { maxSteps: 1 })).not.toThrow(); + + const flat1025 = new Float32Array(1025 * 1025); + expect(() => hydraulic(flat1025, 1025, 512, { maxSteps: 1 })).not.toThrow(); + }); +}); diff --git a/packages/terrain/__tests__/io.spec.ts b/packages/terrain/__tests__/io.spec.ts new file mode 100644 index 000000000..c28ba724c --- /dev/null +++ b/packages/terrain/__tests__/io.spec.ts @@ -0,0 +1,81 @@ +import { Terrain } from "@threenative/terrain"; +import { describe, expect, it } from "vitest"; +import { decodeHeightPNG, encodeHeightPNG } from "../src/core/io.js"; + +const CRC_TABLE = Uint32Array.from({ length: 256 }, (_, initial) => { + let n = initial; + for (let k = 0; k < 8; k++) n = n & 1 ? 0xedb88320 ^ (n >>> 1) : n >>> 1; + return n >>> 0; +}); + +function crc32(bytes: Uint8Array): number { + let c = 0xffffffff; + for (const b of bytes) c = (CRC_TABLE[(c ^ b) & 255] as number) ^ (c >>> 8); + return (c ^ 0xffffffff) >>> 0; +} + +function replaceMetadata(png: Uint8Array, rawJson: string): Uint8Array { + const needle = new TextEncoder().encode("StrataHeightRange\0"); + let pos = -1; + for (let i = 0; i <= png.length - needle.length; i++) { + let match = true; + for (let j = 0; j < needle.length; j++) { + if (png[i + j] !== needle[j]) { + match = false; + break; + } + } + if (match) { + pos = i; + break; + } + } + if (pos < 8) throw new Error("Metadata chunk not found in PNG"); + const chunkOffset = pos - 8; + const oldLength = new DataView(png.buffer, png.byteOffset).getUint32(chunkOffset); + const payload = new TextEncoder().encode(`StrataHeightRange\0${rawJson}`); + const typeAndData = new Uint8Array(4 + payload.length); + typeAndData.set(new TextEncoder().encode("tEXt"), 0); + typeAndData.set(payload, 4); + + const chunk = new Uint8Array(12 + payload.length); + const view = new DataView(chunk.buffer); + view.setUint32(0, payload.length); + chunk.set(typeAndData, 4); + view.setUint32(8 + payload.length, crc32(typeAndData)); + + const afterOffset = chunkOffset + 12 + oldLength; + const corrupted = new Uint8Array(chunkOffset + chunk.length + (png.length - afterOffset)); + corrupted.set(png.subarray(0, chunkOffset), 0); + corrupted.set(chunk, chunkOffset); + corrupted.set(png.subarray(afterOffset), chunkOffset + chunk.length); + return corrupted; +} + +describe("decodeHeightPNG error handling", () => { + it.each([ + ["null", "null"], + ["numeric", " 12345"], + ])("rejects %s metadata with documented error", async (_, invalidJson) => { + const state = new Terrain({ resolution: 17 }).evaluate(); + const validPng = await encodeHeightPNG(state); + const corrupted = replaceMetadata(validPng, invalidJson); + await expect(decodeHeightPNG(corrupted)).rejects.toThrow("Invalid PNG height range metadata"); + }); + + it("rejects truncated chunk with documented error", async () => { + const state = new Terrain({ resolution: 17 }).evaluate(); + const validPng = await encodeHeightPNG(state); + const truncated = validPng.subarray(0, 45); + await expect(decodeHeightPNG(truncated)).rejects.toThrow("Truncated or oversized PNG chunk"); + }); + + it("rejects bad CRC with documented error", async () => { + const state = new Terrain({ resolution: 17 }).evaluate(); + const validPng = await encodeHeightPNG(state); + const corrupted = new Uint8Array(validPng); + const byte = corrupted[16]; + if (byte !== undefined) corrupted[16] = byte ^ 0xff; + await expect(decodeHeightPNG(corrupted)).rejects.toThrow(/PNG CRC mismatch/); + }); +}); diff --git a/packages/terrain/__tests__/placement-overrides.spec.ts b/packages/terrain/__tests__/placement-overrides.spec.ts index 8e1e1c9bc..7299943cc 100644 --- a/packages/terrain/__tests__/placement-overrides.spec.ts +++ b/packages/terrain/__tests__/placement-overrides.spec.ts @@ -20,6 +20,7 @@ const transform = { function fixture() { const terrain = new Terrain({ size: 64, resolution: 17, seed: 73 }).scatter({ id: "trees", + asset: "pine", count: 40, minDistance: 0, avoidWater: false, diff --git a/packages/terrain/__tests__/scatter-identity.spec.ts b/packages/terrain/__tests__/scatter-identity.spec.ts index 2d7cd2a65..73049914b 100644 --- a/packages/terrain/__tests__/scatter-identity.spec.ts +++ b/packages/terrain/__tests__/scatter-identity.spec.ts @@ -5,6 +5,7 @@ describe("public scatter candidate identity", () => { it("retains candidate coordinates, scale and yaw when earlier candidates are rejected", () => { const terrain = new Terrain({ size: 64, resolution: 17, seed: 73 }).scatter({ id: "trees", + asset: "pine", count: 80, minDistance: 0, avoidWater: false, @@ -25,6 +26,7 @@ describe("public scatter candidate identity", () => { it("gives a new seed different candidate identities and preserves keys on round trip", () => { const terrain = new Terrain({ size: 64, resolution: 17 }).scatter({ id: "trees", + asset: "pine", count: 20, seed: 10, minDistance: 0, diff --git a/packages/terrain/__tests__/starter-kit.spec.ts b/packages/terrain/__tests__/starter-kit.spec.ts index bef239687..96085327a 100644 --- a/packages/terrain/__tests__/starter-kit.spec.ts +++ b/packages/terrain/__tests__/starter-kit.spec.ts @@ -8,9 +8,41 @@ import { describe, expect, it } from "vitest"; import { makeTempDirSync } from "../../../test-support/temp-dir.js"; const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))); -const kit = join(packageRoot, "starter", "forest"); const starterAssets = join(packageRoot, "starter-assets"); +const GOLDEN_HASHES: Record> = { + forest: { + "heightmap.u16": "33b3fc66f63ca4df76531a44a2bd84e1aff6f642eb9ab0c6622ebe87d1057c2f", + "placements.bin": "36ab2fd57e76b4b0bd998c5398f983e5f030968b83d5326c0cb98df1b67e4019", + "splat.rgba": "f5e45e3740eea9e751e94570abe785854670d060fda6fe4b5518ccd4a562c7ec", + "world.json": "1bcd3d2c9f056d78b30fbc68b68d5e1a7c31176f0f79471ae473f3bc526b11e9", + }, + alpine: { + "heightmap.u16": "bd84cb6c52a1142303dfb0f304190a981f29d77d118d3880e3eb246549838ae1", + "placements.bin": "5c47dd59619843bc056fbc8391ca7bccdef86c9950cd192c53a6928676865c26", + "splat.rgba": "fde8bf22c6841847f58ab523682f29e77a774a364cdd0e34c0025b6ad06c6f42", + "world.json": "5af1fc81a424792ac440b9907b2b7e3d36a239936e7921f93a94f7fccdda021c", + }, + coastal: { + "heightmap.u16": "313539bae3010b949b65e55307736f6140a55255d81fbb8ab5dbae782bdc8482", + "placements.bin": "e10721cc99dfe96345a6b090eeb834ec87890927d8272ec8a8307b31a989b098", + "splat.rgba": "b3dcb6967095049a05fa113968b3cd8d74914c7b906f2c522ff109f6dbfe2b40", + "world.json": "dfd2dc68bab5fd95f5a0284f9df8656580f4544c92989e5f31363faefbe730fd", + }, + desert: { + "heightmap.u16": "a17965c4dd706f2732cc64af64dba086e006b405086d41a36f2ca37c45ee3add", + "placements.bin": "8dace58d5c91d50f8492149a826575edbff53d3ee78e5d892bbb12d8f809f3fe", + "splat.rgba": "a77791af98c157f371dfa1fdc053c712ea3bdb8a9f3f71de33a69b8f06c88d04", + "world.json": "755ae7887abfce8837256190cab8930639d8802343c10603202cfa7fe413e481", + }, + tundra: { + "heightmap.u16": "2fcf74db51407bcd4e65b7ae92e31da6ff1b0c7152301952fecedeea36c80710", + "placements.bin": "05aa8931ff15b96d21999947ca2c5fdb89fe2b0e17228d8f072021767b55a9ff", + "splat.rgba": "981bd12869273d4f959fcaed53b27b325b5748b7ec558ac25c4258b4fd5c9f07", + "world.json": "6986a3d5cc54f2f0690fc40ec4482c6f2a88cb7c5b773976d19dc513643adc47", + }, +}; + interface ITable { readonly base: { id: string }; readonly layers: readonly { id: string }[]; @@ -24,9 +56,10 @@ interface IKitWater { readonly rivers: readonly { id: string; width: number; points: number[][] }[]; } -/** Copies the kit into a temp directory whose node_modules resolves the workspace package. */ -function kitInTempDir(): string { - const dir = makeTempDirSync("terrain-starter-kit-"); +/** Copies the named kit into a temp directory whose node_modules resolves the workspace package. */ +function kitInTempDir(kitName = "forest"): string { + const dir = makeTempDirSync(`terrain-starter-kit-${kitName}-`); + const kit = join(packageRoot, "starter", kitName); for (const name of ["bake.mjs", "recipe.json", "assets.json", "surface.json"]) copyFileSync(join(kit, name), join(dir, name)); mkdirSync(join(dir, "node_modules", "@threenative"), { recursive: true }); @@ -34,8 +67,8 @@ function kitInTempDir(): string { return dir; } -function bake(out: string): { files: Map; total: number } { - const dir = kitInTempDir(); +function bake(out: string, kitName = "forest"): { files: Map; total: number } { + const dir = kitInTempDir(kitName); execFileSync(process.execPath, ["bake.mjs", "--assets", starterAssets, "--out", out], { cwd: dir, stdio: "pipe", @@ -74,11 +107,38 @@ describe("forest starter kit", () => { expect(Object.keys(perAsset).sort()).toEqual(["boulder", "fir", "fir-c"]); expect(total).toBeLessThanOrEqual(25 * 1024 * 1024); + // Pinned golden hashes for starter-kit forest bake (PRD-592 AC-1) + expect(files.get("heightmap.u16")).toBe( + "33b3fc66f63ca4df76531a44a2bd84e1aff6f642eb9ab0c6622ebe87d1057c2f", + ); + expect(files.get("placements.bin")).toBe( + "36ab2fd57e76b4b0bd998c5398f983e5f030968b83d5326c0cb98df1b67e4019", + ); + expect(files.get("splat.rgba")).toBe( + "f5e45e3740eea9e751e94570abe785854670d060fda6fe4b5518ccd4a562c7ec", + ); + expect(files.get("world.json")).toBe( + "1bcd3d2c9f056d78b30fbc68b68d5e1a7c31176f0f79471ae473f3bc526b11e9", + ); + // One recipe and seed, one package: a second bake is byte-identical. const second = bake(join(makeTempDirSync("terrain-starter-world-"), "world")); expect([...second.files].sort()).toEqual([...files].sort()); }); + it.each(["forest", "alpine", "coastal", "desert", "tundra"] as const)( + "produces pinned golden baseline hashes for %s kit bake (PRD-592 AC-1)", + (kitName) => { + expect.assertions(4); + const out = join(makeTempDirSync(`terrain-starter-world-${kitName}-`), "world"); + const { files } = bake(out, kitName); + const expected = GOLDEN_HASHES[kitName] ?? {}; + for (const [file, hash] of Object.entries(expected)) { + expect(files.get(file), `${kitName} ${file} hash mismatch`).toBe(hash); + } + }, + ); + it("writes every model and texture its manifest and table name", () => { const out = join(makeTempDirSync("terrain-starter-world-"), "world"); bake(out); diff --git a/packages/terrain/__tests__/terrain-consumer.spec.ts b/packages/terrain/__tests__/terrain-consumer.spec.ts index e1604267c..13ffdb194 100644 --- a/packages/terrain/__tests__/terrain-consumer.spec.ts +++ b/packages/terrain/__tests__/terrain-consumer.spec.ts @@ -258,3 +258,12 @@ describe("public terrain consumer", () => { expect(() => toGeometry(mesh)).toThrow(); }); }); +it("rejects a scatter layer without asset", () => { + expect(() => + new Terrain({ resolution: 17 }).scatter({ + id: "trees", + // @ts-expect-error testing missing asset validation + asset: undefined, + }), + ).toThrow(/asset/); +}); diff --git a/packages/terrain/__tests__/world-package.spec.ts b/packages/terrain/__tests__/world-package.spec.ts index 2d6ad6e8e..60897aae2 100644 --- a/packages/terrain/__tests__/world-package.spec.ts +++ b/packages/terrain/__tests__/world-package.spec.ts @@ -123,6 +123,33 @@ describe("world package writer", () => { ); }); + it("fails closed on invalid cellSize (0, -1, NaN), non-finite position, and out-of-extent position", () => { + const world = state(); + // cellSize validation + expect(() => bakeWorldPackage(world, { assets: ASSETS, cellSize: 0 })).toThrow(/cellSize/); + expect(() => bakeWorldPackage(world, { assets: ASSETS, cellSize: -1 })).toThrow(/cellSize/); + expect(() => bakeWorldPackage(world, { assets: ASSETS, cellSize: Number.NaN })).toThrow( + /cellSize/, + ); + + // non-finite position + const base = world.instances[0]; + if (base === undefined) throw new Error("Scatter fixture placed nothing"); + const nanPos: ITerrainState = { + ...world, + instances: [{ ...base, position: [Number.NaN, 0, 0] }], + }; + expect(() => bakeWorldPackage(nanPos, { assets: ASSETS })).toThrow(/position/); + + // out-of-extent position + // world size is 256, extent is [-128, 128] + const outPos: ITerrainState = { + ...world, + instances: [{ ...base, position: [200, 0, 0] }], + }; + expect(() => bakeWorldPackage(outPos, { assets: ASSETS })).toThrow(/extent/); + }); + it("fails closed on an asset the options do not name and on a non-uniform scale", () => { const world = state(); const base = world.instances[0]; diff --git a/packages/terrain/src/core/erosion.ts b/packages/terrain/src/core/erosion.ts index b5ea0e8df..864596077 100644 --- a/packages/terrain/src/core/erosion.ts +++ b/packages/terrain/src/core/erosion.ts @@ -29,6 +29,10 @@ export function thermal( { iterations = 20, talus = 32, rate = 0.22 }: IThermalOptions = {}, observations?: IErosionMaps, ): Float32Array { + if (!Number.isInteger(n) || n < 2) + throw RangeError("Thermal erosion requires grid resolution n >= 2"); + if (!Number.isInteger(iterations) || iterations < 0 || iterations > 200) + throw RangeError("Thermal erosion iterations must be between 0 and 200"); const h = height.slice(); const delta = new Float64Array(h.length); const limit = Math.tan((talus * Math.PI) / 180) * (size / (n - 1)); @@ -126,6 +130,12 @@ export function hydraulic( }: IHydraulicOptions = {}, observations?: IErosionMaps, ): Float32Array { + if (!Number.isInteger(n) || n < 2) + throw RangeError("Hydraulic erosion requires grid resolution n >= 2"); + if (!Number.isInteger(droplets) || droplets < 0) + throw RangeError("Hydraulic erosion droplets must be a non-negative integer"); + if (!Number.isInteger(maxSteps) || maxSteps < 1 || maxSteps > 128) + throw RangeError("Hydraulic erosion maxSteps must be between 1 and 128"); const h = Float64Array.from(height); const rnd = random(seed); const cell = size / (n - 1); diff --git a/packages/terrain/src/core/io.ts b/packages/terrain/src/core/io.ts index e6fbaaba2..da7ff88b6 100644 --- a/packages/terrain/src/core/io.ts +++ b/packages/terrain/src/core/io.ts @@ -270,7 +270,9 @@ export async function decodeHeightPNG( const text = decoder.decode(data); if (text.startsWith("StrataHeightRange\0")) { try { - metadata = JSON.parse(text.slice(18)); + const parsed = JSON.parse(text.slice(18)); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw Error(); + metadata = parsed; } catch { throw Error("Invalid PNG height range metadata"); } diff --git a/packages/terrain/src/core/operations.ts b/packages/terrain/src/core/operations.ts index 33e4fcb16..b51875211 100644 --- a/packages/terrain/src/core/operations.ts +++ b/packages/terrain/src/core/operations.ts @@ -833,7 +833,7 @@ export function finalizeState(s: ITerrainState): ITerrainState { accepted.push({ id: `${rule.id}:candidate:${seed}:${attempt}`, layer: rule.id, - asset: rule.asset ?? "pine", + asset: rule.asset, position: [x, h + (rule.offsetY ?? 0), z], rotation: yaw, scale: itemScale, @@ -845,7 +845,7 @@ export function finalizeState(s: ITerrainState): ITerrainState { bucket.push(index); buckets.set(key, bucket); } - s.instances.push(...accepted); + for (const instance of accepted) s.instances.push(instance); if (accepted.length < count) s.diagnostics.push( `Scatter '${rule.id}': placed ${accepted.length}/${count}; masks or spacing exhausted the attempt budget.`, diff --git a/packages/terrain/src/core/terrain.ts b/packages/terrain/src/core/terrain.ts index d9527d533..3814272a4 100644 --- a/packages/terrain/src/core/terrain.ts +++ b/packages/terrain/src/core/terrain.ts @@ -433,7 +433,7 @@ export class Terrain { biome(options: OperationOptions<"biome"> = {}): this { return this._operation("biome", options); } - scatter(options: OperationOptions<"scatter"> = {}): this { + scatter(options: OperationOptions<"scatter">): this { return this._operation("scatter", options); } clear(options: OperationOptions<"clear">): this { diff --git a/packages/terrain/src/core/types.ts b/packages/terrain/src/core/types.ts index 6fb9eb708..58b44478f 100644 --- a/packages/terrain/src/core/types.ts +++ b/packages/terrain/src/core/types.ts @@ -218,7 +218,7 @@ export interface IBiomeParams extends IBrushParams { } export interface IScatterParams extends IOperationBase { - readonly asset?: string; + readonly asset: string; readonly count?: number; /** Guaranteed within one rule, never across rules. */ readonly minDistance?: number; diff --git a/packages/terrain/src/core/validation.ts b/packages/terrain/src/core/validation.ts index b06af473b..466598099 100644 --- a/packages/terrain/src/core/validation.ts +++ b/packages/terrain/src/core/validation.ts @@ -513,6 +513,8 @@ export function validateLayer(layer: Layer): void { } if (params.name !== undefined) identifier(params.name, "name"); if (params.asset !== undefined) identifier(params.asset, "asset"); + if (layer.type === "scatter" && typeof params.asset !== "string") + fail("scatter requires an asset identifier"); if (["heightmap", "paste"].includes(layer.type) && !params.data) fail(`${layer.type} requires heightmap data`); const data = params.data as diff --git a/packages/terrain/src/core/world-package.ts b/packages/terrain/src/core/world-package.ts index 6d1144981..ebc4911f9 100644 --- a/packages/terrain/src/core/world-package.ts +++ b/packages/terrain/src/core/world-package.ts @@ -132,10 +132,27 @@ function uniformScale(asset: string, id: string, scale: readonly [number, number return mean; } -function placementRecord(item: IPlacement): number[] { +function placementRecord(item: IPlacement, extent: IWorldPackageManifest["extent"]): number[] { if (item.transform === undefined && !Number.isFinite(item.rotation)) throw Error(`World package: placement '${item.id}' has a non-finite rotation.`); const { position, quaternion, scale } = poseOf(item); + if ( + !Number.isFinite(position[0]) || + !Number.isFinite(position[1]) || + !Number.isFinite(position[2]) + ) + throw Error( + `World package: placement '${item.id}' has a non-finite position [${position.join(", ")}].`, + ); + if ( + position[0] < extent.minX || + position[0] > extent.minX + extent.sizeX || + position[2] < extent.minZ || + position[2] > extent.minZ + extent.sizeZ + ) + throw Error( + `World package: placement '${item.id}' position [${position[0]}, ${position[2]}] is outside extent bounds [${extent.minX}..${extent.minX + extent.sizeX}, ${extent.minZ}..${extent.minZ + extent.sizeZ}].`, + ); const length = Math.hypot(...quaternion); if (!(length > 0)) throw Error(`World package: placement '${item.id}' has a zero quaternion.`); return [ @@ -197,7 +214,7 @@ function placementCells( for (const [asset, items] of [...cell.items.entries()].sort(byId)) { runs.push({ asset, count: items.length, offset: written }); for (const item of items) { - records.set(placementRecord(item), written * RECORD_VALUES); + records.set(placementRecord(item, extent), written * RECORD_VALUES); written += 1; } } @@ -223,7 +240,12 @@ export function bakeWorldPackage( options: IBakeWorldPackageOptions, ): IBakedWorldPackage { const { size, resolution } = state; - const { assets, cellSize = 64, layers, names } = options; + const cellSize = options.cellSize ?? 64; + if (!Number.isFinite(cellSize) || cellSize <= 0) + throw Error( + `World package: cellSize must be a positive finite number, got ${String(cellSize)}.`, + ); + const { assets, layers, names } = options; const heightmapName = names?.heightmap ?? "heightmap.u16"; const placementsName = names?.placements ?? "placements.bin"; const splatName = names?.splat ?? "splat.rgba"; From 6c40d87575784c784f22f26f79850af46e7af0db Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 12:37:54 -0700 Subject: [PATCH 003/210] perf(terrain): bound erosion work and remove hot-loop allocations --- ...ls-closed-and-erodes-without-allocating.md | 18 +- .../terrain/__tests__/erosion-bounds.spec.ts | 42 +++- packages/terrain/src/core/erosion.ts | 207 ++++++++++++------ packages/terrain/src/core/world-package.ts | 9 +- 4 files changed, 195 insertions(+), 81 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md b/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md index ca1d14ac5..f79907910 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md @@ -41,19 +41,23 @@ after on the same host. - [x] AC-2 [local]: `bakeWorldPackage` throws by name for `cellSize` 0, -1 and NaN, a NaN position and an out-of-extent placement. proof: `world-package.spec.ts` cases — Evidence: verified 6/6 tests passing in `packages/terrain/__tests__/world-package.spec.ts`. - [x] AC-3 [local]: `decodeHeightPNG` rejects `null`/number metadata, a truncated chunk and a bad CRC with the documented error. proof: new `io.spec.ts` — Evidence: verified 4/4 tests passing in `packages/terrain/__tests__/io.spec.ts`. - [x] AC-4 [local]: A scatter layer without `asset` fails validation. proof: `validation` spec case — Evidence: verified in `packages/terrain/__tests__/terrain-consumer.spec.ts` rejecting scatter without asset. -- [ ] AC-5 [local]: Forest kit bake time drops by at least 30 % on the same host, with identical bytes. proof: `/usr/bin/time -v node packages/terrain/starter/forest/bake.mjs`, three runs per arm — Evidence: pending. +- [ ] AC-5 [local]: Forest kit bake time drops by at least 30 % on the same host, with identical bytes. proof: `/usr/bin/time -v node packages/terrain/starter/forest/bake.mjs`, three runs per arm — Evidence: Debian laptop (idle, same host), three warm runs per arm. Before: 0.59 / 0.62 / 0.57 s (mean 0.593). After: 0.48 / 0.51 / 0.46 s (mean 0.483). Load 1.41, CPU 76–80 °C. 18.5 % faster with identical golden bytes — below the 30 % target, so AC-5 stays open. ## Execution Phases #### Phase 1: golden bytes and validation -**Status:** PARTIAL +**Status:** DONE **Files:** `packages/terrain/src/core/{world-package,io,validation,operations,erosion}.ts`, specs - [x] Golden hashes pinned. proof: `starter-kit.spec.ts` (pass, verified 5/5 starter kits with 4 pinned sha256 hashes each against HEAD baseline byte-for-byte) -- [ ] Bad options throw by name. proof: `world-package.spec.ts`, `io.spec.ts`, validation spec, `erosion-bounds.spec.ts` — 252 terrain tests and package/workspace typecheck pass; finite/noninteger checks and zero/large-grid defaults pass. Remaining: establish a compatible practical upper bound on hydraulic droplets; a finite-integer guard alone does not bound bake work. +- [x] Bad options throw by name. proof: `world-package.spec.ts`, `io.spec.ts`, validation spec, `erosion-bounds.spec.ts` — Initial candidate: 254 terrain tests pass. Final candidate: rebuilt erosion bounds/defaults/spikes and starter golden suite passes 27 tests; independent parent bounds/golden gate passes 18 tests. Exact work-count regression observes 3 × 1025² seeded draws at both 1025 and 2048 grids; restoring original uncapped `n*n` reproduces failure (12582912 versus 3151875 draws), and the capped candidate passes. Explicit 200000 remains accepted, 200001/unsafe counts rejected, and 513/1025 defaults retained. #### Phase 2: allocation-free loops -**Status:** NOT STARTED -**Files:** `packages/terrain/src/core/erosion.ts`, `world-package.ts`, `operations.ts` -- [ ] Same bytes, faster bake. proof: golden spec + timed bake +**Status:** PARTIAL +**Files:** `packages/terrain/src/core/erosion.ts`, `world-package.ts` +- [ ] Same bytes, faster bake. proof: golden spec + timed bake — Golden hashes unchanged (`starter-kit.spec.ts` 9/9). Allocation removal landed: hydraulic reuses two probes instead of allocating an object twice per droplet step, thermal scans its four neighbours without a per-cell array, `splatBytes` precomputes the component index, and the hydraulic `spread` brush is flat typed arrays with an interior fast path. Timed bake fell 18.5 %, not the 30 % AC-5 requires, so this box stays open. Remaining: reach 30 % without changing output bytes. + +Verification (2026-10-10): the initial allocation-removal candidate passed 254 terrain tests and package/workspace typecheck. The final interior-offset candidate passes terrain build, the 27-test erosion/golden suite, terrain typecheck and focused Biome (three complexity warnings, no errors); the parent independently passes 18 bounds/golden tests. Phase 1 is done with actual uncapped red/capped green work-count proof. Phase 2 and AC-5 remain open: the earlier exploratory 18.5% result is below 30%, and final same-host 3+3 timing via `offload.sh run --quiet --no-sync` is queued behind the laptop CI runner’s shared quiet lock. No final speed claim is made. + +Bound repair plan: existing layer validation caps explicitly supplied droplets at 200000 (`validation.ts:449–455`). The first guard incorrectly capped automatic `n*n` defaults; removing the cap entirely left the work bound unproven. Preserve the supplied/default distinction and derive the automatic bound from the existing supported grid limit, with regression tests for large default grids and excessive explicit counts. -Parent verification (2026-10-10): `pnpm exec vitest run packages/terrain/__tests__` passed 34 files / 252 tests; `pnpm --filter @threenative/terrain typecheck`, `pnpm typecheck`, terrain build and Biome checks passed (14 warnings, no errors). The original 513-grid droplet-cap regression failed with `RangeError` before repair and the new bounds spec covers 513/1025 defaults after repair. Phase 1 remains partial until the hydraulic work bound is proven; phase 2 and the three-runs-per-arm 30% speed proof remain unrun. +Bound repair implemented (2026-10-10): `hydraulic` now distinguishes a supplied `droplets` (held to the 200000 recipe limit) from the automatic default (one droplet per cell, capped at the largest supported grid 1025²). `erosion-bounds.spec.ts` proves the 513/1025 defaults, the accepted 200000 limit, rejected 200001 and unsafe-integer counts, and equal exact capped work at 1025/2048 grids. diff --git a/packages/terrain/__tests__/erosion-bounds.spec.ts b/packages/terrain/__tests__/erosion-bounds.spec.ts index d88f7639f..ff511906e 100644 --- a/packages/terrain/__tests__/erosion-bounds.spec.ts +++ b/packages/terrain/__tests__/erosion-bounds.spec.ts @@ -1,6 +1,24 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { hydraulic, thermal } from "../src/core/erosion.js"; +// Count every seeded draw, so a test can measure the actual droplet work instead of trusting a +// completion-only assertion that an uncapped default would also satisfy. +const drawn = vi.hoisted(() => ({ count: 0 })); + +vi.mock("../src/core/math.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + random: (seed: number) => { + const rnd = actual.random(seed); + return () => { + drawn.count += 1; + return rnd(); + }; + }, + }; +}); + describe("erosion grid and parameter bounds validation", () => { it("thermal erosion rejects n < 2, fractional or non-finite grid resolution", () => { const h = new Float32Array(4); @@ -64,4 +82,26 @@ describe("erosion grid and parameter bounds validation", () => { const flat1025 = new Float32Array(1025 * 1025); expect(() => hydraulic(flat1025, 1025, 512, { maxSteps: 1 })).not.toThrow(); }); + + it("rejects an explicit droplet count above the recipe limit, accepts the limit itself", () => { + const h4 = new Float32Array(4); + expect(() => hydraulic(h4, 2, 10, { droplets: 200_001 })).toThrow(RangeError); + expect(() => hydraulic(h4, 2, 10, { droplets: 200_000, maxSteps: 1 })).not.toThrow(); + expect(() => hydraulic(h4, 2, 10, { droplets: Number.MAX_SAFE_INTEGER + 1 })).toThrow( + RangeError, + ); + }); + + it("bounds the automatic droplet default with an exact work count on an oversized grid", () => { + // The default is one droplet per cell, capped at the largest supported grid (1025 x 1025). + // An uncapped n*n default would run one droplet per cell of the 2048² field, about four times + // the capped work; counting seeded draws compares the two, so removing the cap turns this red. + drawn.count = 0; + hydraulic(new Float32Array(1025 * 1025), 1025, 512, { maxSteps: 1 }); + const capped = drawn.count; + drawn.count = 0; + hydraulic(new Float32Array(2048 * 2048), 2048, 512, { maxSteps: 1 }); + expect(drawn.count).toBe(capped); + expect(capped).toBe(3 * 1025 * 1025); + }); }); diff --git a/packages/terrain/src/core/erosion.ts b/packages/terrain/src/core/erosion.ts index 864596077..d27a3d0d1 100644 --- a/packages/terrain/src/core/erosion.ts +++ b/packages/terrain/src/core/erosion.ts @@ -41,18 +41,25 @@ export function thermal( for (let z = 0; z < n; z += 1) { for (let x = 0; x < n; x += 1) { const i = z * n + x; + const here = h[i] as number; let best = -1; let diff = limit; - for (const j of [ - x > 0 ? i - 1 : -1, - x < n - 1 ? i + 1 : -1, - z > 0 ? i - n : -1, - z < n - 1 ? i + n : -1, - ]) { - if (j >= 0 && (h[i] as number) - (h[j] as number) > diff) { - diff = (h[i] as number) - (h[j] as number); - best = j; - } + // The same four neighbours in the same order, without the per-cell array. + if (x > 0 && here - (h[i - 1] as number) > diff) { + diff = here - (h[i - 1] as number); + best = i - 1; + } + if (x < n - 1 && here - (h[i + 1] as number) > diff) { + diff = here - (h[i + 1] as number); + best = i + 1; + } + if (z > 0 && here - (h[i - n] as number) > diff) { + diff = here - (h[i - n] as number); + best = i - n; + } + if (z < n - 1 && here - (h[i + n] as number) > diff) { + diff = here - (h[i + n] as number); + best = i + n; } if (best >= 0) { const transfer = (diff - limit) * rate; @@ -68,30 +75,41 @@ export function thermal( return h; } -interface IBrushCell { - dx: number; - dz: number; - weight: number; +interface IBrush { + readonly dx: Int32Array; + readonly dz: Int32Array; + readonly weight: Float64Array; + /** Sum of every normalised weight: the total for a brush fully inside the field. */ + readonly full: number; + /** Largest offset on either axis, so a brush this far from an edge is fully inside. */ + readonly radius: number; } -const brushes = new Map(); +const brushes = new Map(); /** Erosion brush: normalised weights over a disc, so a droplet digs a dimple and not a single-cell pit. */ -function brush(radius: number): IBrushCell[] { +function brush(radius: number): IBrush { let cells = brushes.get(radius); if (!cells) { - cells = []; + const dx: number[] = []; + const dz: number[] = []; + const raw: number[] = []; let total = 0; - for (let dz = -radius; dz <= radius; dz += 1) { - for (let dx = -radius; dx <= radius; dx += 1) { - const d = Math.hypot(dx, dz); + for (let z = -radius; z <= radius; z += 1) { + for (let x = -radius; x <= radius; x += 1) { + const d = Math.hypot(x, z); if (d > radius) continue; const weight = 1 - d / (radius + 1); - cells.push({ dx, dz, weight }); + dx.push(x); + dz.push(z); + raw.push(weight); total += weight; } } - for (const c of cells) c.weight /= total; + const weight = Float64Array.from(raw, (w) => w / total); + let full = 0; + for (let k = 0; k < weight.length; k += 1) full += weight[k] as number; + cells = { dx: Int32Array.from(dx), dz: Int32Array.from(dz), full, radius, weight }; brushes.set(radius, cells); } return cells; @@ -105,15 +123,21 @@ interface IHydraulicProbe { iz: number; } +/** The largest supported grid resolution (`validation.ts` RESOLUTIONS): bounds the automatic default. */ +const MAX_AUTOMATIC_DROPLETS = 1025 * 1025; +/** The explicit recipe limit on `droplets` (`validation.ts`), so a direct call cannot exceed it. */ +const MAX_EXPLICIT_DROPLETS = 200_000; + /** Hydraulic erosion: seeded droplets carry sediment downhill, depositing where they slow. */ export function hydraulic( height: Float32Array, n: number, size: number, - { - // Roughly one droplet per cell. Incision saturates near here: more droplets keep cutting the - // same drainage lines over, which deepens nothing and spends the bake time. - droplets = n * n, + options: IHydraulicOptions = {}, + observations?: IErosionMaps, +): Float32Array { + const { + droplets: requestedDroplets, // A droplet has to be able to cross the world, so its step budget scales with the grid: a // fixed 40 barely leaves a 257 world while a 65 world would walk off it in a few steps. maxSteps = Math.round(Math.min(64, Math.max(24, n / 4))), @@ -127,19 +151,36 @@ export function hydraulic( evaporation = 0.025, seed = 1, brushRadius = 3, - }: IHydraulicOptions = {}, - observations?: IErosionMaps, -): Float32Array { + } = options; if (!Number.isInteger(n) || n < 2) throw RangeError("Hydraulic erosion requires grid resolution n >= 2"); - if (!Number.isInteger(droplets) || droplets < 0) + // Roughly one droplet per cell. Incision saturates near here: more droplets keep cutting the + // same drainage lines over, which deepens nothing and spends the bake time. A caller that + // supplies a count is held to the recipe limit; the automatic default is bounded by the largest + // supported grid, so leaving it out cannot request unbounded work. + const droplets = + requestedDroplets === undefined ? Math.min(n * n, MAX_AUTOMATIC_DROPLETS) : requestedDroplets; + if (!Number.isSafeInteger(droplets) || droplets < 0) throw RangeError("Hydraulic erosion droplets must be a non-negative integer"); + if (requestedDroplets !== undefined && droplets > MAX_EXPLICIT_DROPLETS) + throw RangeError(`Hydraulic erosion droplets must not exceed ${String(MAX_EXPLICIT_DROPLETS)}`); if (!Number.isInteger(maxSteps) || maxSteps < 1 || maxSteps > 128) throw RangeError("Hydraulic erosion maxSteps must be between 1 and 128"); const h = Float64Array.from(height); const rnd = random(seed); const cell = size / (n - 1); - const cells = brush(brushRadius); + const brushCells = brush(brushRadius); + const brushDx = brushCells.dx; + const brushDz = brushCells.dz; + const brushWeight = brushCells.weight; + const brushCount = brushDx.length; + const brushFull = brushCells.full; + const brushEdge = brushCells.radius; + // The flat index offset `dz * n + dx` per brush cell for this grid. The interior loop adds it to + // the centre index instead of multiplying two coordinates for every cell; integer-only, exact. + const brushOffset = new Int32Array(brushCount); + for (let k = 0; k < brushCount; k += 1) + brushOffset[k] = (brushDz[k] as number) * n + (brushDx[k] as number); /** Adds `amount` over the brush around one cell, renormalised where the field ends. */ const spread = ( x: number, @@ -147,58 +188,90 @@ export function hydraulic( amount: number, floor = Number.NEGATIVE_INFINITY, ): number => { + // A brush fully inside the field sums every weight; only a brush at the edge needs the + // partial total, which the first pass computes in the same order as `full`. + const interior = x >= brushEdge && x < n - brushEdge && z >= brushEdge && z < n - brushEdge; + // Interior brushes cover the field bulk: no edge total scan, no bounds branch and no per-cell + // `z * n + x` multiply. Same brush order, same weights and same arithmetic as the shared tail. + if (interior) { + const total = brushFull; + if (total <= 0) return 0; + let moved = 0; + const base = z * n + x; + const negative = amount < 0; + const depositing = observations !== undefined && amount > 0; + for (let k = 0; k < brushCount; k += 1) { + const i = base + (brushOffset[k] as number); + const share = (amount * (brushWeight[k] as number)) / total; + // A brush cell below the downstream bed has no soil the droplet can pick up. Taking it + // anyway digs pits deeper than the channel and leaves unsupported pillars between paths. + const change = negative ? -Math.min(-share, Math.max(0, (h[i] as number) - floor)) : share; + h[i] = (h[i] as number) + change; + moved += change; + if (depositing) + (observations as IErosionMaps).deposition[i] = + ((observations as IErosionMaps).deposition[i] as number) + + (amount * (brushWeight[k] as number)) / total; + } + return moved; + } let total = 0; - for (const c of cells) { - const ix = x + c.dx; - const iz = z + c.dz; - if (ix >= 0 && ix < n && iz >= 0 && iz < n) total += c.weight; + for (let k = 0; k < brushCount; k += 1) { + const ix = x + (brushDx[k] as number); + const iz = z + (brushDz[k] as number); + if (ix >= 0 && ix < n && iz >= 0 && iz < n) total += brushWeight[k] as number; } if (total <= 0) return 0; let moved = 0; - for (const c of cells) { - const ix = x + c.dx; - const iz = z + c.dz; + const negative = amount < 0; + const depositing = observations !== undefined && amount > 0; + for (let k = 0; k < brushCount; k += 1) { + const ix = x + (brushDx[k] as number); + const iz = z + (brushDz[k] as number); if (ix < 0 || ix >= n || iz < 0 || iz >= n) continue; const i = iz * n + ix; - const share = (amount * c.weight) / total; + const share = (amount * (brushWeight[k] as number)) / total; // A brush cell below the downstream bed has no soil the droplet can pick up. Taking it // anyway digs pits deeper than the channel and leaves unsupported pillars between paths. - const change = amount < 0 ? -Math.min(-share, Math.max(0, (h[i] as number) - floor)) : share; + const change = negative ? -Math.min(-share, Math.max(0, (h[i] as number) - floor)) : share; h[i] = (h[i] as number) + change; moved += change; - if (observations && amount > 0) - observations.deposition[i] = - (observations.deposition[i] as number) + (amount * c.weight) / total; + if (depositing) + (observations as IErosionMaps).deposition[i] = + ((observations as IErosionMaps).deposition[i] as number) + + (amount * (brushWeight[k] as number)) / total; } return moved; }; - const get = (x: number, z: number): IHydraulicProbe => { + // Two reusable probes: `get` fills one instead of allocating a fresh object twice per step. + const probeOld: IHydraulicProbe = { value: 0, dx: 0, dz: 0, ix: 0, iz: 0 }; + const probeNext: IHydraulicProbe = { value: 0, dx: 0, dz: 0, ix: 0, iz: 0 }; + const get = (x: number, z: number, out: IHydraulicProbe): IHydraulicProbe => { const ix = Math.min(n - 2, Math.floor(x)); const iz = Math.min(n - 2, Math.floor(z)); const tx = x - ix; const tz = z - iz; const i = iz * n + ix; - return { - value: lerp( - lerp(h[i] as number, h[i + 1] as number, tx), - lerp(h[i + n] as number, h[i + n + 1] as number, tx), + out.value = lerp( + lerp(h[i] as number, h[i + 1] as number, tx), + lerp(h[i + n] as number, h[i + n + 1] as number, tx), + tz, + ); + out.dx = + lerp( + (h[i + 1] as number) - (h[i] as number), + (h[i + n + 1] as number) - (h[i + n] as number), tz, - ), - dx: - lerp( - (h[i + 1] as number) - (h[i] as number), - (h[i + n + 1] as number) - (h[i + n] as number), - tz, - ) / cell, - dz: - lerp( - (h[i + n] as number) - (h[i] as number), - (h[i + n + 1] as number) - (h[i + 1] as number), - tx, - ) / cell, - ix, - iz, - }; + ) / cell; + out.dz = + lerp( + (h[i + n] as number) - (h[i] as number), + (h[i + n + 1] as number) - (h[i + 1] as number), + tx, + ) / cell; + out.ix = ix; + out.iz = iz; + return out; }; for (let k = 0; k < droplets; k += 1) { let x = rnd() * (n - 1 - 0.001); @@ -209,7 +282,7 @@ export function hydraulic( let speed = 1; let sediment = 0; for (let step = 0; step < maxSteps; step += 1) { - const old = get(x, z); + const old = get(x, z, probeOld); if (observations) { const tx = x - old.ix; const tz = z - old.iz; @@ -241,7 +314,7 @@ export function hydraulic( sediment = 0; break; } - const next = get(nx, nz); + const next = get(nx, nz, probeNext); const dh = next.value - old.value; const cap = Math.max(-dh, 0.005 * cell) * speed * water * capacity; if (dh > 0 || sediment > cap) { @@ -261,7 +334,7 @@ export function hydraulic( // A live droplet at the integration cutoff is still carrying its load. Dumping that entire // load here builds artificial sediment pillars; only evaporation settles the remainder. if (sediment > 0 && water < 0.02) { - const end = get(clamp(x, 0, n - 1.001), clamp(z, 0, n - 1.001)); + const end = get(clamp(x, 0, n - 1.001), clamp(z, 0, n - 1.001), probeOld); spread(end.ix, end.iz, sediment); } } diff --git a/packages/terrain/src/core/world-package.ts b/packages/terrain/src/core/world-package.ts index ebc4911f9..bd5bd2957 100644 --- a/packages/terrain/src/core/world-package.ts +++ b/packages/terrain/src/core/world-package.ts @@ -89,6 +89,7 @@ const SCALE_TOLERANCE = 1e-3; const SLOTS = MATERIAL_IDS.map((id, channel) => ({ channel, component: COMPONENTS[channel % COMPONENTS.length] as Component, + componentIndex: channel % COMPONENTS.length, id, plane: Math.floor(channel / COMPONENTS.length), })); @@ -325,12 +326,8 @@ function splatBytes(state: ITerrainState, resolution: number): Uint8Array { const sample = (source + column) * MATERIAL_IDS.length; for (const slot of SLOTS) { const weight = state.splat[sample + slot.channel] as number; - bytes[ - slot.plane * planeBytes + - row * resolution * 4 + - column * 4 + - COMPONENTS.indexOf(slot.component) - ] = Math.round(Math.min(1, Math.max(0, weight)) * 255); + bytes[slot.plane * planeBytes + row * resolution * 4 + column * 4 + slot.componentIndex] = + Math.round(Math.min(1, Math.max(0, weight)) * 255); } } } From c1e421198a13206f0e5a465c41de49cbe6bdc97d Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 12:42:30 -0700 Subject: [PATCH 004/210] docs: track active Strata EPIC worktree ownership --- docs/PRDs/strata-terrain-hardening/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/README.md b/docs/PRDs/strata-terrain-hardening/README.md index cf46977b1..acd343b4d 100644 --- a/docs/PRDs/strata-terrain-hardening/README.md +++ b/docs/PRDs/strata-terrain-hardening/README.md @@ -34,6 +34,6 @@ Start with PRD-592 golden bytes and validation, then execute 585, 583, 587, 593, Current baseline: `pnpm build` and `pnpm check:docs` passed; six documentation contract spec files passed (244 tests). No runtime or performance acceptance is established by those checks. -## Active worker checkout +## Active worker checkouts -PRD-585 phase 1 has an isolated execution checkout at `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-proof`, branch `epic/strata-terrain-proof`, based on EPIC commit `227cb944b`. Codex owns integration into PR #483. Its first arm was interrupted before edits to serialize Gemini provider use with the PRD-592 repair. Dependencies and focused package builds passed; its phase-1 arm resumed after that repair terminated. Cleanup must inspect ignored data and confirm its owner has stopped after integration. +PRD-592 phase 1 is committed at `6c40d8757`; phase 2 remains open pending the 30% speed proof. Its worker watches the queued quiet benchmark and has released the EPIC index for integration. PRD-585 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-proof`, branch `epic/strata-terrain-proof`, based on `227cb944b`; its integration test passes, with census repair and CI wiring in progress. PRD-583 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-bake`, branch `epic/strata-terrain-bake`, based on `9e7a8cab0`, for headless GLB baking. Codex coordinates integration into PR #483. Coding arms share a provider lock. Cleanup must inspect ignored data and confirm each owner has stopped after integration. From 84544261747f6df42fbf04a431a02be7c8ea8146 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 13:02:27 -0700 Subject: [PATCH 005/210] ci: select and require Strata terrain proof --- .github/workflows/ci.yml | 37 ++++- .../PRD-585-strata-proof-runs-in-ci.md | 22 +-- examples/strata-terrain-preview/AGENTS.md | 95 ++++++++++++ examples/strata-terrain-preview/CLAUDE.md | 95 ++++++++++++ .../__tests__/world-cells-terrain.spec.ts | 146 ++++++++++++++++++ scripts/__tests__/ci-needs.spec.ts | 113 ++++++++++++-- scripts/__tests__/ci-structure.spec.ts | 67 +++++++- .../__tests__/strata-script-census.spec.ts | 38 +++++ scripts/ci-change-scope.mjs | 32 ++++ 9 files changed, 620 insertions(+), 25 deletions(-) create mode 100644 packages/core/__tests__/world-cells-terrain.spec.ts create mode 100644 scripts/__tests__/strata-script-census.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd9605d1f..a4deb283d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -659,6 +659,40 @@ jobs: - uses: ./.github/actions/playwright-chromium - run: pnpm test:playtest:ci + strata: + needs: [scope, build-artifacts] + if: ${{ needs.scope.outputs.selection == 'full' && fromJSON(needs.scope.outputs.plan).jobs.strata.required }} + runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }} + timeout-minutes: 30 + env: + TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ needs.scope.outputs.candidate_sha || github.sha }} + - uses: ./.github/actions/pnpm + - uses: actions/setup-node@v7 + with: + node-version: 20 + cache: pnpm + cache-dependency-path: | + pnpm-lock.yaml + packages/create-threenative/templates/*/package.json + - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/workspace-dist + with: + shared-artifact: workspace-packages + save-bundles: "false" + - uses: ./.github/actions/playwright-chromium + - name: Generate the terrain example's ignored world source + run: pnpm --filter strata-terrain-preview bake + - name: Run strata web playtest + run: sh scripts/xvfb.sh pnpm --filter strata-terrain-preview test:terrain:web + - name: Run strata consumer verification + run: pnpm --filter strata-terrain-preview test:consumer + - name: Run strata editor verification + run: sh scripts/xvfb.sh pnpm --filter strata-terrain-preview test:terrain:editor + golden-path-template: needs: [scope, build-artifacts] if: ${{ (needs.scope.outputs.selection == 'full' || needs.scope.outputs.selection == 'template') }} @@ -1241,7 +1275,7 @@ jobs: # The only required context after protected-develop cutover. Explicit exemptions are not passes. # Legacy main contexts stay in place until the owner changes protection after full qualification. ci-required: - needs: [scope, integration, native-platforms, typecheck, lint, test, test-unit, test-native, test-browser, test-playtest, golden-path-template, template-nonvisual, golden-path, benchmark, build, build-artifacts, budgets, performance-contracts, supply-chain] + needs: [scope, integration, native-platforms, typecheck, lint, test, test-unit, test-native, test-browser, test-playtest, strata, golden-path-template, template-nonvisual, golden-path, benchmark, build, build-artifacts, budgets, performance-contracts, supply-chain] if: ${{ always() && !github.event.pull_request.draft }} runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER_LIGHT) && 'ubuntu-24.04' || vars.TN_RUNNER_LIGHT }} timeout-minutes: 5 @@ -1295,6 +1329,7 @@ jobs: - test-native - test-browser - test-playtest + - strata - golden-path-template - template-nonvisual - golden-path diff --git a/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md index d1bb3d8db..776bb65d1 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md @@ -1,6 +1,6 @@ # PRD-585 — Strata's proof runs in CI, not only by hand -**Status:** NOT STARTED +**Status:** PARTIAL **Priority:** P1 — No Strata playtest, consumer check or editor script runs in CI, so every later PRD in this epic can regress unseen (AC-1 to AC-5). **Complexity:** 4 (MEDIUM); risk override: none **Owner:** ThreeNative maintainers @@ -33,9 +33,9 @@ index them in the example's `AGENTS.md`. - [ ] AC-1 [shared]: The `strata` job runs `test:terrain:web` green on a PR touching `packages/terrain/`. proof: CI run link — Evidence: pending. - [ ] AC-2 [shared]: The same job runs `test:consumer` and `test:terrain:editor` green. proof: CI run link — Evidence: pending. -- [ ] AC-3 [local]: The change-scope selects the job for terrain, example and `world-cells` paths and skips it for an unrelated template. proof: `pnpm exec vitest run scripts/__tests__/ci-structure.spec.ts` — Evidence: pending. -- [ ] AC-4 [local]: A baked 3×3-cell package loads through a real `WorldCells`; walking across cells gives resident cells and instance counts equal to the bake. proof: `pnpm exec vitest run packages/core/__tests__/world-cells-terrain.spec.ts` — Evidence: pending. -- [ ] AC-5 [local]: Every file under the example's `scripts/` and `playtests/` is wired from `package.json` or named in its `AGENTS.md`. proof: a spec listing the folder against both — Evidence: pending. +- [x] AC-3 [local]: The change-scope selects the job for terrain, example and `world-cells` paths and skips it for an unrelated template. proof: `pnpm exec vitest run scripts/__tests__/ci-structure.spec.ts scripts/__tests__/ci-needs.spec.ts` — Evidence: PASS (213 tests), including `world.ts`, qualification, and required-verdict missing/skipped/failed/unmapped cases. +- [x] AC-4 [local]: A baked 3×3-cell package loads through a real `WorldCells`; walking across cells gives resident cells and instance counts equal to the bake. proof: `pnpm exec vitest run packages/core/__tests__/world-cells-terrain.spec.ts` — Evidence: PASS (1 test); bounded drain fails on timeout and cleanup runs in `finally`. +- [x] AC-5 [local]: Every file under the example's `scripts/` and `playtests/` is wired from `package.json` or named in its `AGENTS.md`. proof: `pnpm exec vitest run scripts/__tests__/strata-script-census.spec.ts` — Evidence: PASS (1 test); recursive, nonempty inventory matches exact documented relative paths, including fixtures. ## Integration Ledger @@ -47,14 +47,16 @@ index them in the example's `AGENTS.md`. ## Execution Phases #### Phase 1: integration spec and script census -**Status:** NOT STARTED +**Status:** COMPLETE **Files:** `packages/core/__tests__/world-cells-terrain.spec.ts` (new), `examples/strata-terrain-preview/AGENTS.md`, orphan scripts (delete or index) -- [ ] Real `WorldCells` loads a baked package. proof: `pnpm exec vitest run packages/core/__tests__/world-cells-terrain.spec.ts` -- [ ] No unwired script or playtest is left. proof: census spec +- [x] Real `WorldCells` loads a baked package. proof: `pnpm exec vitest run packages/core/__tests__/world-cells-terrain.spec.ts` — PASS (1 test); core typecheck also passed. +- [x] No unwired script or playtest is left. proof: census spec — PASS (1 test); original package scripts preserved, manual/preparation helpers indexed by purpose. `pnpm sync:agents`, mirror contracts (8 tests), and `pnpm check:docs` passed. #### Phase 2: the CI job -**Status:** NOT STARTED -**Files:** `.github/workflows/ci.yml`, `scripts/ci-change-scope.mjs`, `scripts/__tests__/ci-structure.spec.ts` -- [ ] Scope selects and skips correctly. proof: `pnpm exec vitest run scripts/__tests__/ci-structure.spec.ts scripts/__tests__/ci-needs.spec.ts` +**Status:** PARTIAL +**Files:** `.github/workflows/ci.yml`, `scripts/ci-change-scope.mjs`, `scripts/__tests__/ci-structure.spec.ts`, `scripts/__tests__/ci-needs.spec.ts` +- [x] Scope selects and skips correctly. proof: `pnpm exec vitest run scripts/__tests__/ci-structure.spec.ts scripts/__tests__/ci-needs.spec.ts` — PASS (213 tests); the existing protected main-job join rejects missing, skipped, failed and unmapped results. Focused Biome checks passed with complexity warnings. - [ ] Web playtest green in CI. proof: CI run link - [ ] Consumer and editor scripts green in CI. proof: CI run link + +The local job wiring is verified; AC-1/AC-2 need a real candidate CI run with successful `strata` execution. Skipped draft checks are not runtime proof. diff --git a/examples/strata-terrain-preview/AGENTS.md b/examples/strata-terrain-preview/AGENTS.md index be9aea9ed..02fe4c6c9 100644 --- a/examples/strata-terrain-preview/AGENTS.md +++ b/examples/strata-terrain-preview/AGENTS.md @@ -51,3 +51,98 @@ a full-world GLB in a plain browser page, and hands the result to a scaffolded T builds and plays with no authoring package (`CONSUMER_GAME=skip` leaves that last stage out). `pnpm --filter strata-terrain-preview test:terrain:authored` polishes a world through the editor's real controls, reloads it and reproduces `scripts/fixtures/editor-authored.json` (`RECORD=1` rewrites it). + +### Script, playtest and fixture index + +All scripts, playtest definitions, manual preparation helpers and fixtures under `scripts/` and `playtests/`: + +- Playtest scenarios: + - `playtests/editor.playtest.json`: playtest scenario checking vertex count and appearance in the recovered editor view. + - `playtests/export.playtest.json`: playtest scenario checking static GLB export loading and appearance. + - `playtests/terrain.playtest.json`: primary playtest scenario for shared web/desktop showcase views. + - `playtests/walk.playtest.json`: playtest scenario checking baked terrain triangles and player walk stability. + +- Verification and test suites: + - `scripts/verify-assets.mjs`: verifies asset manifests, replacement tables, and model loading. + - `scripts/verify-authored-world.mjs`: verifies that editor-authored modifications reproduce committed baseline output. + - `scripts/verify-cameras.mjs`: checks camera viewpoints, transitions, and framing across all world biomes. + - `scripts/verify-consumer.mjs`: verifies packaging and consumption of the authored world in an external project. + - `scripts/verify-custom-assets.mjs`: proves custom asset overrides swap cleanly without starter assets. + - `scripts/verify-editor.mjs`: verifies live editor tool application, brush operations, and undo/redo state. + - `scripts/verify-environment.mjs`: tests biome-specific environmental lighting and atmosphere settings. + - `scripts/verify-game-handoff.mjs`: verifies clean scene handoff into an ordinary playable ThreeNative game. + - `scripts/verify-import-export.mjs`: checks full import and export roundtrips for world documents. + - `scripts/verify-landforms.mjs`: validates terrain elevation bounds, slopes, and landform contours. + - `scripts/verify-ocean.mjs`: verifies ocean surface rendering, shorelines, and waterline properties. + - `scripts/verify-sky.mjs`: checks procedural and LUT sky dome conditions across time and weather. + - `scripts/verify-starter-kit.mjs`: verifies starter kit configuration, generation, and multi-world playtests. + - `scripts/verify-surfaces.mjs`: verifies PBR terrain surface materials, splat textures, and blend transitions. + - `scripts/verify-tool-groups.mjs`: tests editor UI tool groupings, interactions, and parameter schemas. + - `scripts/verify-transforms.mjs`: validates coordinate transforms, rotations, and placement matrices. + - `scripts/verify-world-export.mjs`: verifies headless static world GLB export and vanilla Three.js loading. + - `scripts/check-water.mjs`: regression tests for shared water rendering and shoreline depths. + - `scripts/check-temperate.mts`: checks foliage cover distribution and LOD distance compaction. + +- Authoring, staging and data preparation: + - `scripts/bake.mjs`: bakes source elevation data and generates canonical world JSON packages. + - `scripts/consumer-world.mjs`: exports `openConsumerPage` to serve an installed consumer directory to headless Chromium and run vanilla full-world export against packed tarballs. + - `scripts/stage-native-assets.mjs`: stages textures and models for native desktop/mobile bundles. + - `scripts/make-needle-atlas.mjs`: procedural generator for conifer needle foliage atlases. + - `scripts/prep-fab-pines.py`: manual prep script requiring Blender CLI and authored Fab Scots pine GLB to decimate and budget pines. + - `scripts/prep-fab-temperate.mjs`: cooks and fixes texture atlas and opacity bindings for temperate models. + - `scripts/prep-landscape-pro.mjs`: stages licensed Landscape Pro species for game runtime loading. + - `scripts/prep-polyhaven-fir.py`: manual prep script requiring Blender CLI and authored Poly Haven fir 1k .blend to export game GLBs. + - `scripts/prep-polyhaven-rock.py`: manual prep script requiring Blender CLI and authored Poly Haven boulder 1k .blend to export game GLBs. + - `scripts/prep-trees.py`: manual prep script requiring Blender CLI and authored CC0 tree models to decimate to triangle and atlas budgets. + +- Landform measurement and diagnostics: + - `scripts/measure-embankment.mjs`: measures cross-section geometry and bench cut slope of road embankments. + - `scripts/measure-light.mjs`: measures display-referred Rec.709 luminance against reference calibration. + - `scripts/measure-spikes.mjs`: detects elevation spikes and gradient discontinuities across terrain bakes. + - `scripts/measure-terrain.mjs`: tabulates terrain relief, slope, bake timing, and drainage metrics. + +- USGS DEM source data and cropping: + - `scripts/dem/crop.mjs`: crops and transforms raw USGS 3DEP GeoTIFFs into canonical DEM bins and metadata. + - `scripts/dem/alpine.bin`: canonical elevation heightfield binary for the alpine world. + - `scripts/dem/alpine.json`: grid metadata and coordinate bounding bounds for the alpine world. + - `scripts/dem/alpine-horizon.bin`: distant horizon ring elevation binary for the alpine world. + - `scripts/dem/alpine-horizon.json`: distant horizon ring grid metadata for the alpine world. + - `scripts/dem/coastal.bin`: canonical elevation heightfield binary for the coastal world. + - `scripts/dem/coastal.json`: grid metadata and coordinate bounding bounds for the coastal world. + - `scripts/dem/coastal-horizon.bin`: distant horizon ring elevation binary for the coastal world. + - `scripts/dem/coastal-horizon.json`: distant horizon ring grid metadata for the coastal world. + - `scripts/dem/desert.bin`: canonical elevation heightfield binary for the desert world. + - `scripts/dem/desert.json`: grid metadata and coordinate bounding bounds for the desert world. + - `scripts/dem/desert-horizon.bin`: distant horizon ring elevation binary for the desert world. + - `scripts/dem/desert-horizon.json`: distant horizon ring grid metadata for the desert world. + - `scripts/dem/forest.bin`: canonical elevation heightfield binary for the forest world. + - `scripts/dem/forest.json`: grid metadata and coordinate bounding bounds for the forest world. + - `scripts/dem/forest-horizon.bin`: distant horizon ring elevation binary for the forest world. + - `scripts/dem/forest-horizon.json`: distant horizon ring grid metadata for the forest world. + - `scripts/dem/tundra.bin`: canonical elevation heightfield binary for the tundra world. + - `scripts/dem/tundra.json`: grid metadata and coordinate bounding bounds for the tundra world. + - `scripts/dem/tundra-horizon.bin`: distant horizon ring elevation binary for the tundra world. + - `scripts/dem/tundra-horizon.json`: distant horizon ring grid metadata for the tundra world. + +- Fixtures and testing environments: + - `scripts/fixtures/consumer-world.mjs`: driver running an authored world in an isolated browser harness. + - `scripts/fixtures/editor-authored.json`: golden recorded output of real editor authoring operations. + - `scripts/fixtures/editor-readback.mjs`: browser harness reading back live editor canvas state. + - `scripts/fixtures/export-world.mjs`: browser-side test driver for headless full-world export. + - `scripts/fixtures/vanilla-world.mjs`: vanilla Three.js runner verifying static world GLB rendering. + - `scripts/fixtures/handoff-game/Handoff.ts`: handoff harness component receiving exported terrain. + - `scripts/fixtures/handoff-game/game.ts`: standalone ThreeNative entrypoint testing handoff consumption. + - `scripts/fixtures/handoff-game/handoffEnvironment.ts`: lighting and environment setup for handoff fixture. + - `scripts/fixtures/handoff-game/state.ts`: reactive state management for handoff fixture game. + - `scripts/fixtures/kit-game/Alpine.ts`: starter kit fixture scene for the alpine biome. + - `scripts/fixtures/kit-game/Coastal.ts`: starter kit fixture scene for the coastal biome. + - `scripts/fixtures/kit-game/Desert.ts`: starter kit fixture scene for the desert biome. + - `scripts/fixtures/kit-game/Forest.ts`: starter kit fixture scene for the forest biome. + - `scripts/fixtures/kit-game/Tundra.ts`: starter kit fixture scene for the tundra biome. + - `scripts/fixtures/kit-game/game.ts`: standalone multi-world game harness testing starter kit scenes. + - `scripts/fixtures/kit-game/state.ts`: shared state for starter kit game fixture. + - `scripts/fixtures/kit-game/playtests/alpine.playtest.json`: playtest scenario for starter kit alpine world. + - `scripts/fixtures/kit-game/playtests/coastal.playtest.json`: playtest scenario for starter kit coastal world. + - `scripts/fixtures/kit-game/playtests/desert.playtest.json`: playtest scenario for starter kit desert world. + - `scripts/fixtures/kit-game/playtests/kit.playtest.json`: master playtest scenario verifying starter kit worlds. + - `scripts/fixtures/kit-game/playtests/tundra.playtest.json`: playtest scenario for starter kit tundra world. diff --git a/examples/strata-terrain-preview/CLAUDE.md b/examples/strata-terrain-preview/CLAUDE.md index f24b589ae..4cf585247 100644 --- a/examples/strata-terrain-preview/CLAUDE.md +++ b/examples/strata-terrain-preview/CLAUDE.md @@ -53,3 +53,98 @@ a full-world GLB in a plain browser page, and hands the result to a scaffolded T builds and plays with no authoring package (`CONSUMER_GAME=skip` leaves that last stage out). `pnpm --filter strata-terrain-preview test:terrain:authored` polishes a world through the editor's real controls, reloads it and reproduces `scripts/fixtures/editor-authored.json` (`RECORD=1` rewrites it). + +### Script, playtest and fixture index + +All scripts, playtest definitions, manual preparation helpers and fixtures under `scripts/` and `playtests/`: + +- Playtest scenarios: + - `playtests/editor.playtest.json`: playtest scenario checking vertex count and appearance in the recovered editor view. + - `playtests/export.playtest.json`: playtest scenario checking static GLB export loading and appearance. + - `playtests/terrain.playtest.json`: primary playtest scenario for shared web/desktop showcase views. + - `playtests/walk.playtest.json`: playtest scenario checking baked terrain triangles and player walk stability. + +- Verification and test suites: + - `scripts/verify-assets.mjs`: verifies asset manifests, replacement tables, and model loading. + - `scripts/verify-authored-world.mjs`: verifies that editor-authored modifications reproduce committed baseline output. + - `scripts/verify-cameras.mjs`: checks camera viewpoints, transitions, and framing across all world biomes. + - `scripts/verify-consumer.mjs`: verifies packaging and consumption of the authored world in an external project. + - `scripts/verify-custom-assets.mjs`: proves custom asset overrides swap cleanly without starter assets. + - `scripts/verify-editor.mjs`: verifies live editor tool application, brush operations, and undo/redo state. + - `scripts/verify-environment.mjs`: tests biome-specific environmental lighting and atmosphere settings. + - `scripts/verify-game-handoff.mjs`: verifies clean scene handoff into an ordinary playable ThreeNative game. + - `scripts/verify-import-export.mjs`: checks full import and export roundtrips for world documents. + - `scripts/verify-landforms.mjs`: validates terrain elevation bounds, slopes, and landform contours. + - `scripts/verify-ocean.mjs`: verifies ocean surface rendering, shorelines, and waterline properties. + - `scripts/verify-sky.mjs`: checks procedural and LUT sky dome conditions across time and weather. + - `scripts/verify-starter-kit.mjs`: verifies starter kit configuration, generation, and multi-world playtests. + - `scripts/verify-surfaces.mjs`: verifies PBR terrain surface materials, splat textures, and blend transitions. + - `scripts/verify-tool-groups.mjs`: tests editor UI tool groupings, interactions, and parameter schemas. + - `scripts/verify-transforms.mjs`: validates coordinate transforms, rotations, and placement matrices. + - `scripts/verify-world-export.mjs`: verifies headless static world GLB export and vanilla Three.js loading. + - `scripts/check-water.mjs`: regression tests for shared water rendering and shoreline depths. + - `scripts/check-temperate.mts`: checks foliage cover distribution and LOD distance compaction. + +- Authoring, staging and data preparation: + - `scripts/bake.mjs`: bakes source elevation data and generates canonical world JSON packages. + - `scripts/consumer-world.mjs`: exports `openConsumerPage` to serve an installed consumer directory to headless Chromium and run vanilla full-world export against packed tarballs. + - `scripts/stage-native-assets.mjs`: stages textures and models for native desktop/mobile bundles. + - `scripts/make-needle-atlas.mjs`: procedural generator for conifer needle foliage atlases. + - `scripts/prep-fab-pines.py`: manual prep script requiring Blender CLI and authored Fab Scots pine GLB to decimate and budget pines. + - `scripts/prep-fab-temperate.mjs`: cooks and fixes texture atlas and opacity bindings for temperate models. + - `scripts/prep-landscape-pro.mjs`: stages licensed Landscape Pro species for game runtime loading. + - `scripts/prep-polyhaven-fir.py`: manual prep script requiring Blender CLI and authored Poly Haven fir 1k .blend to export game GLBs. + - `scripts/prep-polyhaven-rock.py`: manual prep script requiring Blender CLI and authored Poly Haven boulder 1k .blend to export game GLBs. + - `scripts/prep-trees.py`: manual prep script requiring Blender CLI and authored CC0 tree models to decimate to triangle and atlas budgets. + +- Landform measurement and diagnostics: + - `scripts/measure-embankment.mjs`: measures cross-section geometry and bench cut slope of road embankments. + - `scripts/measure-light.mjs`: measures display-referred Rec.709 luminance against reference calibration. + - `scripts/measure-spikes.mjs`: detects elevation spikes and gradient discontinuities across terrain bakes. + - `scripts/measure-terrain.mjs`: tabulates terrain relief, slope, bake timing, and drainage metrics. + +- USGS DEM source data and cropping: + - `scripts/dem/crop.mjs`: crops and transforms raw USGS 3DEP GeoTIFFs into canonical DEM bins and metadata. + - `scripts/dem/alpine.bin`: canonical elevation heightfield binary for the alpine world. + - `scripts/dem/alpine.json`: grid metadata and coordinate bounding bounds for the alpine world. + - `scripts/dem/alpine-horizon.bin`: distant horizon ring elevation binary for the alpine world. + - `scripts/dem/alpine-horizon.json`: distant horizon ring grid metadata for the alpine world. + - `scripts/dem/coastal.bin`: canonical elevation heightfield binary for the coastal world. + - `scripts/dem/coastal.json`: grid metadata and coordinate bounding bounds for the coastal world. + - `scripts/dem/coastal-horizon.bin`: distant horizon ring elevation binary for the coastal world. + - `scripts/dem/coastal-horizon.json`: distant horizon ring grid metadata for the coastal world. + - `scripts/dem/desert.bin`: canonical elevation heightfield binary for the desert world. + - `scripts/dem/desert.json`: grid metadata and coordinate bounding bounds for the desert world. + - `scripts/dem/desert-horizon.bin`: distant horizon ring elevation binary for the desert world. + - `scripts/dem/desert-horizon.json`: distant horizon ring grid metadata for the desert world. + - `scripts/dem/forest.bin`: canonical elevation heightfield binary for the forest world. + - `scripts/dem/forest.json`: grid metadata and coordinate bounding bounds for the forest world. + - `scripts/dem/forest-horizon.bin`: distant horizon ring elevation binary for the forest world. + - `scripts/dem/forest-horizon.json`: distant horizon ring grid metadata for the forest world. + - `scripts/dem/tundra.bin`: canonical elevation heightfield binary for the tundra world. + - `scripts/dem/tundra.json`: grid metadata and coordinate bounding bounds for the tundra world. + - `scripts/dem/tundra-horizon.bin`: distant horizon ring elevation binary for the tundra world. + - `scripts/dem/tundra-horizon.json`: distant horizon ring grid metadata for the tundra world. + +- Fixtures and testing environments: + - `scripts/fixtures/consumer-world.mjs`: driver running an authored world in an isolated browser harness. + - `scripts/fixtures/editor-authored.json`: golden recorded output of real editor authoring operations. + - `scripts/fixtures/editor-readback.mjs`: browser harness reading back live editor canvas state. + - `scripts/fixtures/export-world.mjs`: browser-side test driver for headless full-world export. + - `scripts/fixtures/vanilla-world.mjs`: vanilla Three.js runner verifying static world GLB rendering. + - `scripts/fixtures/handoff-game/Handoff.ts`: handoff harness component receiving exported terrain. + - `scripts/fixtures/handoff-game/game.ts`: standalone ThreeNative entrypoint testing handoff consumption. + - `scripts/fixtures/handoff-game/handoffEnvironment.ts`: lighting and environment setup for handoff fixture. + - `scripts/fixtures/handoff-game/state.ts`: reactive state management for handoff fixture game. + - `scripts/fixtures/kit-game/Alpine.ts`: starter kit fixture scene for the alpine biome. + - `scripts/fixtures/kit-game/Coastal.ts`: starter kit fixture scene for the coastal biome. + - `scripts/fixtures/kit-game/Desert.ts`: starter kit fixture scene for the desert biome. + - `scripts/fixtures/kit-game/Forest.ts`: starter kit fixture scene for the forest biome. + - `scripts/fixtures/kit-game/Tundra.ts`: starter kit fixture scene for the tundra biome. + - `scripts/fixtures/kit-game/game.ts`: standalone multi-world game harness testing starter kit scenes. + - `scripts/fixtures/kit-game/state.ts`: shared state for starter kit game fixture. + - `scripts/fixtures/kit-game/playtests/alpine.playtest.json`: playtest scenario for starter kit alpine world. + - `scripts/fixtures/kit-game/playtests/coastal.playtest.json`: playtest scenario for starter kit coastal world. + - `scripts/fixtures/kit-game/playtests/desert.playtest.json`: playtest scenario for starter kit desert world. + - `scripts/fixtures/kit-game/playtests/kit.playtest.json`: master playtest scenario verifying starter kit worlds. + - `scripts/fixtures/kit-game/playtests/tundra.playtest.json`: playtest scenario for starter kit tundra world. diff --git a/packages/core/__tests__/world-cells-terrain.spec.ts b/packages/core/__tests__/world-cells-terrain.spec.ts new file mode 100644 index 000000000..8ab663490 --- /dev/null +++ b/packages/core/__tests__/world-cells-terrain.spec.ts @@ -0,0 +1,146 @@ +import { BoxGeometry, Group, Mesh, MeshBasicMaterial, type Object3D } from "three"; +import { describe, expect, it, vi } from "vitest"; +import { Terrain, bakeWorldPackage } from "../../../packages/terrain/src/index.js"; +import type { IAssetLoader } from "../src/assets.js"; +import { type IWorldPackage, WorldCells } from "../src/world.js"; + +/** Single asset declaration for the test terrain */ +const ASSETS = { + fir: { + bounds: { max: [3, 14, 3] as const, min: [-3, 0, -3] as const }, + glb: "models/fir.glb", + lods: [{ distance: 50, glb: "models/fir-mid.glb" }], + maxDistance: 300, + }, +}; + +function makeModel(): Object3D { + const group = new Group(); + group.add(new Mesh(new BoxGeometry(1, 1, 1), new MeshBasicMaterial())); + return group; +} + +function cacheLoader(): IAssetLoader { + return { + audio: () => Promise.reject(new Error("unused")), + clear: () => {}, + model: (() => + Promise.resolve({ scene: makeModel() } as unknown as T)) as IAssetLoader["model"], + progress: { pending: [], requested: 0, requestedBytes: 0, settled: 0, settledBytes: 0 }, + release: () => true, + resolve: (p) => Promise.resolve([p]), + resolved: new Map(), + texture: () => Promise.reject(new Error("unused")), + }; +} + +describe("WorldCells baked terrain package integration", () => { + it("loads a real baked 3x3-cell package and verifies resident cells and instances during walk", async () => { + let world: WorldCells | undefined; + try { + // 192m extent with cellSize 64 -> exactly 3x3 cells (extent -96 to 96) + const terrain = new Terrain({ resolution: 65, seed: 123, size: 192 }) + .scatter({ id: "firs", asset: "fir", count: 90, minDistance: 2 }) + .evaluate(); + + const baked = bakeWorldPackage(terrain, { assets: ASSETS, cellSize: 64 }); + const manifest = baked.manifest as IWorldPackage; + + expect(manifest.cells).toHaveLength(9); + const expectedCells = new Map(); + for (const cell of manifest.cells) { + const count = cell.runs.reduce((acc, run) => acc + run.count, 0); + expectedCells.set(`${cell.x}:${cell.z}`, count); + } + + // Verify every cell has placements + expect(expectedCells.size).toBe(9); + const totalInstances = Array.from(expectedCells.values()).reduce((a, b) => a + b, 0); + expect(totalInstances).toBe(terrain.instances.length); + + const files = baked.files; + vi.stubGlobal( + "fetch", + vi.fn(async (url: unknown) => { + const raw = String(url); + const filename = raw.replace(/^\//u, "").replace(/^world\//u, ""); + if (filename.endsWith("world.json")) { + return { + ok: true, + json: async () => manifest, + arrayBuffer: async () => new TextEncoder().encode(JSON.stringify(manifest)).buffer, + }; + } + const data = files[filename]; + if (data) { + const buffer = (data as Uint8Array).buffer.slice( + (data as Uint8Array).byteOffset, + (data as Uint8Array).byteOffset + (data as Uint8Array).byteLength, + ); + return { + ok: true, + arrayBuffer: async () => buffer, + json: async () => JSON.parse(new TextDecoder().decode(buffer)), + }; + } + return { ok: false, status: 404 }; + }), + ); + + const follow = { position: { x: -64, z: -64 } }; + world = await WorldCells.load({ + url: "world/world.json", + assets: cacheLoader(), + follow, + surface: new MeshBasicMaterial(), + budgets: { bytes: 1_000_000_000, instances: 1_000_000, residentCells: 64 }, + ring: 0, + lodHysteresis: 0, + gpuScene: false, + impostors: false, + admissionBudgetMs: 10_000, + freshMeshesPerUpdate: 100, + }); + + const drainWorld = async (activeWorld: WorldCells) => { + for (let pass = 0; pass < 200; pass += 1) { + await new Promise((resolve) => setTimeout(resolve, 0)); + activeWorld.update(); + const stats = activeWorld.stats(); + if (stats.admission.backlog === 0 && stats.loadsInFlight === 0) return; + } + throw new Error("Timed out waiting for WorldCells to settle drain within bounds"); + }; + + // Walk across cells from (0, 0) to (2, 2) + // Chebyshev distance > ring + 1 (0 + 1 = 1) evicts leaving cells. + // From (0, 0): + // At (0, 0): resident [0:0] + // Move to (1, 1): distance from (0, 0) is max(1, 1) = 1 <= 1 (kept by hysteresis) -> [0:0, 1:1] + // Move to (2, 2): distance from (0, 0) is max(2, 2) = 2 > 1 -> (0, 0) evicted; distance from (1, 1) is 1 <= 1 -> [1:1, 2:2] + const waypoints: Array<{ x: number; z: number; expectedKeys: string[] }> = [ + { x: -64, z: -64, expectedKeys: ["0:0"] }, + { x: 0, z: 0, expectedKeys: ["0:0", "1:1"] }, + { x: 64, z: 64, expectedKeys: ["1:1", "2:2"] }, + ]; + + for (const { x, z, expectedKeys } of waypoints) { + follow.position.x = x; + follow.position.z = z; + await drainWorld(world); + + const stats = world.stats(); + expect(new Set(stats.residentKeys)).toEqual(new Set(expectedKeys)); + expect(stats.residentCells).toBe(expectedKeys.length); + const expectedInstanceCount = expectedKeys.reduce( + (sum, k) => sum + (expectedCells.get(k) ?? 0), + 0, + ); + expect(stats.instances).toBe(expectedInstanceCount); + } + } finally { + world?.dispose(); + vi.unstubAllGlobals(); + } + }); +}); diff --git a/scripts/__tests__/ci-needs.spec.ts b/scripts/__tests__/ci-needs.spec.ts index ba9914789..a96aca14d 100644 --- a/scripts/__tests__/ci-needs.spec.ts +++ b/scripts/__tests__/ci-needs.spec.ts @@ -1,8 +1,9 @@ import { spawnSync } from "node:child_process"; -import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { integrationEvidence } from "../../test-support/ci-integration-fixture.js"; import { makeTempDirSync } from "../../test-support/temp-dir.js"; import { formatJobTimings, formatRunSummary, summaryRows } from "../ci-run-summary.js"; import { ciJobGraph, ciNeedsFindings, declaredNeeds, jobSections } from "../ci-workflow.js"; @@ -413,6 +414,7 @@ function boardLegs(): string[] { "test-native", "test-browser", "test-playtest", + "strata", "golden-path-template (starter)", "template-nonvisual (rain, 1/1)", "golden-path", @@ -890,18 +892,7 @@ describe("PRD-481 the runner class is compared per job, because every board is m }); /** The verdict gate on a non-reuse plan, with every job's own result supplied by the caller. */ -function verifyWarmPlan( - fixture: IReuseFixture, - plan: Record, - results: Record, -) { - const jobs = plan.jobs as Record; - const needs = { - scope: { result: "success", outputs: { plan: JSON.stringify(plan) } }, - ...Object.fromEntries( - Object.entries(jobs).map(([name]) => [name, { result: results[name] ?? "skipped" }]), - ), - }; +function verifyNeedsVerdict(fixture: IReuseFixture, needs: Record) { return spawnSync(process.execPath, [path.join(repo, "scripts/ci-required.mjs")], { cwd: fixture.root, encoding: "utf8", @@ -917,6 +908,22 @@ function verifyWarmPlan( }); } +/** The verdict gate on a non-reuse plan, with every job's own result supplied by the caller. */ +function verifyWarmPlan( + fixture: IReuseFixture, + plan: Record, + results: Record, +) { + const jobs = plan.jobs as Record; + const needs = { + scope: { result: "success", outputs: { plan: JSON.stringify(plan) } }, + ...Object.fromEntries( + Object.entries(jobs).map(([name]) => [name, { result: results[name] ?? "skipped" }]), + ), + }; + return verifyNeedsVerdict(fixture, needs); +} + describe("PRD-481 a develop push only warms the caches every pull request reads", () => { // GitHub scopes a pull request's cache to `refs/pull/N/merge`, which no other pull request can // read. Only a run on the base branch publishes something every pull request into develop reads, @@ -1013,6 +1020,86 @@ describe("PRD-481 a develop push only warms the caches every pull request reads" expect(red.status).toBe(1); expect(red.stderr).toContain("CI_REQUIRED_JOB_NOT_SUCCESS: test-native"); }); + + it("enforces selected strata in ci-required verdict: rejects missing, skipped, failed, and unmapped, and accepts success", () => { + const planResult = spawnSync( + process.execPath, + ["scripts/ci-change-scope.mjs", "--event", "push", "--format", "json"], + { cwd: repo, encoding: "utf8" }, + ); + expect(planResult.status, planResult.stderr).toBe(0); + const plan = JSON.parse(planResult.stdout) as Record; + const jobs = plan.jobs as Record; + expect(jobs.strata?.required).toBe(true); + + function runWithNeeds(targetPlan: Record, needs: Record) { + const directory = makeTempDirSync("ci-strata-verdict-"); + try { + const evidence = integrationEvidence( + repo, + directory, + targetPlan as unknown as Parameters[2], + ).env; + return spawnSync(process.execPath, ["scripts/ci-required.mjs"], { + cwd: repo, + encoding: "utf8", + env: { + ...Object.fromEntries( + Object.entries(process.env).filter(([key]) => !key.startsWith("TN_CI_")), + ), + ...evidence, + TN_CI_NEEDS: JSON.stringify(needs), + }, + }); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + } + + const baseNeeds: Record = { + scope: { result: "success", outputs: { plan: JSON.stringify(plan) } }, + ...Object.fromEntries( + Object.entries(jobs).map(([name, job]) => [ + name, + { result: job.required ? "success" : "skipped" }, + ]), + ), + }; + + // 1. Success when strata succeeds + const successResult = runWithNeeds(plan, baseNeeds); + expect(successResult.status, successResult.stdout + successResult.stderr).toBe(0); + expect(successResult.stdout).toContain("strata: required (success)"); + + // 2. Rejected when strata is missing from needs + const missingNeeds = Object.fromEntries( + Object.entries(baseNeeds).filter(([name]) => name !== "strata"), + ); + const missingResult = runWithNeeds(plan, missingNeeds); + expect(missingResult.status).toBe(1); + expect(missingResult.stderr).toContain("CI_REQUIRED_JOB_NOT_SUCCESS: strata (missing)"); + + // 3. Rejected when strata skipped + const skippedNeeds = { ...baseNeeds, strata: { result: "skipped" } }; + const skippedResult = runWithNeeds(plan, skippedNeeds); + expect(skippedResult.status).toBe(1); + expect(skippedResult.stderr).toContain("CI_REQUIRED_JOB_NOT_SUCCESS: strata (skipped)"); + + // 4. Rejected when strata failed + const failedNeeds = { ...baseNeeds, strata: { result: "failure" } }; + const failedResult = runWithNeeds(plan, failedNeeds); + expect(failedResult.status).toBe(1); + expect(failedResult.stderr).toContain("CI_REQUIRED_JOB_NOT_SUCCESS: strata (failure)"); + + // 5. Rejected when strata is in needs but unmapped in plan.jobs + const unmappedNeeds = { + ...baseNeeds, + "strata-unmapped": { result: "success" }, + }; + const unmappedResult = runWithNeeds(plan, unmappedNeeds); + expect(unmappedResult.status).toBe(1); + expect(unmappedResult.stderr).toContain("CI_REQUIRED_UNMAPPED_JOB: strata-unmapped"); + }); }); describe("unproven matrix reuse runs the normal board", () => { diff --git a/scripts/__tests__/ci-structure.spec.ts b/scripts/__tests__/ci-structure.spec.ts index 870e2785c..2e32ecf27 100644 --- a/scripts/__tests__/ci-structure.spec.ts +++ b/scripts/__tests__/ci-structure.spec.ts @@ -3526,6 +3526,67 @@ describe("PRD-373 selective feature verification", () => { } }); + describe("strata terrain CI selection", () => { + it.each([ + "packages/terrain/src/index.ts", + "examples/strata-terrain-preview/src/game.ts", + "packages/core/src/world.ts", + "packages/core/src/world-cells.ts", + "packages/core/__tests__/world-cells-terrain.spec.ts", + ])("selects strata job when %s changes on develop PR", async (path) => { + const fixture = await scopeFixture(); + try { + const head = await commitScopeChange( + fixture, + path, + "export const terrainTouch = true;\n", + "terrain change", + ); + const plan = classifyScope(fixture.root, fixture.base, head, ["--target", "develop"]); + expect(plan.selection).toBe("full"); + const jobs = plan.jobs as Record; + expect(jobs.strata?.required).toBe(true); + } finally { + await removeFixture(fixture.root); + } + }); + + it("skips strata job for an unrelated template change on develop PR", async () => { + const fixture = await scopeFixture(); + try { + const head = await commitScopeChange( + fixture, + "packages/create-threenative/templates/rain/src/game.ts", + "export const rainTouch = true;\n", + "unrelated template", + ); + const plan = classifyScope(fixture.root, fixture.base, head, ["--target", "develop"]); + const jobs = plan.jobs as Record; + expect(jobs.strata?.required).toBe(false); + } finally { + await removeFixture(fixture.root); + } + }); + + it("requires strata job in full qualification mode", async () => { + const fixture = await scopeFixture(); + try { + const head = await commitScopeChange( + fixture, + "packages/create-threenative/templates/rain/src/game.ts", + "export const rainTouch = true;\n", + "unrelated template", + ); + const plan = classifyScope(fixture.root, fixture.base, head, ["--target", "main"]); + expect(plan.qualification).toBe(true); + const jobs = plan.jobs as Record; + expect(jobs.strata?.required).toBe(true); + } finally { + await removeFixture(fixture.root); + } + }); + }); + it("a fresh install cannot stop on the interactive node_modules purge prompt", () => { // A checkout whose node_modules was not created by this pnpm makes `pnpm install` ask before // purging it. Agents run without a TTY, so the prompt hangs them until `CI=true` is added by @@ -3809,6 +3870,7 @@ const CI_SKIPPED_JOBS = [ "golden-path-template", "native-platforms", "performance-contracts", + "strata", "template-nonvisual", "test", "test-browser", @@ -3860,10 +3922,13 @@ describe("a CI-configuration-only pull request", () => { expect(Object.values(jobs).filter((job) => job.required).length).toBeGreaterThan(6); // Every gate the `ci` selection skipped comes back. `native-platforms` is the one a clean // develop diff legitimately waives, and this diff does not touch a native path. - for (const name of CI_SKIPPED_JOBS.filter((job) => job !== "native-platforms")) { + for (const name of CI_SKIPPED_JOBS.filter( + (job) => job !== "native-platforms" && job !== "strata", + )) { expect(jobs[name]?.required, name).toBe(true); } expect(jobs["native-platforms"]?.required).toBe(true); + expect(jobs.strata?.required).toBe(false); } finally { await removeFixture(fixture.root); } diff --git a/scripts/__tests__/strata-script-census.spec.ts b/scripts/__tests__/strata-script-census.spec.ts new file mode 100644 index 000000000..5206e2d2a --- /dev/null +++ b/scripts/__tests__/strata-script-census.spec.ts @@ -0,0 +1,38 @@ +import { readFileSync, readdirSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +describe("strata-terrain-preview script and playtest census", () => { + it("ensures every file under scripts/ and playtests/ is documented in AGENTS.md", () => { + const exampleRoot = path.resolve(__dirname, "../../examples/strata-terrain-preview"); + const scriptsDir = path.join(exampleRoot, "scripts"); + const playtestsDir = path.join(exampleRoot, "playtests"); + + function collectFiles(dir: string, relPrefix: string): string[] { + const entries = readdirSync(dir, { withFileTypes: true }); + let out: string[] = []; + for (const entry of entries) { + const rel = path.join(relPrefix, entry.name); + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + out = out.concat(collectFiles(full, rel)); + } else if (entry.isFile()) { + out.push(rel); + } + } + return out; + } + + const scriptFiles = collectFiles(scriptsDir, "scripts"); + const playtestFiles = collectFiles(playtestsDir, "playtests"); + const allFiles = [...scriptFiles, ...playtestFiles].sort(); + + expect(allFiles.length).toBeGreaterThan(0); + + const agents = readFileSync(path.join(exampleRoot, "AGENTS.md"), "utf8"); + const documented = new Set(Array.from(agents.matchAll(/`([^`]+)`/g), (m) => m[1])); + + const undocumented = allFiles.filter((file) => !documented.has(file)); + expect(undocumented).toEqual([]); + }); +}); diff --git a/scripts/ci-change-scope.mjs b/scripts/ci-change-scope.mjs index a43805439..b89cdbefd 100644 --- a/scripts/ci-change-scope.mjs +++ b/scripts/ci-change-scope.mjs @@ -118,6 +118,7 @@ const FULL_JOBS = [ "test-native", "test-browser", "test-playtest", + "strata", "golden-path-template", "template-nonvisual", "golden-path", @@ -142,6 +143,23 @@ const CI_JOBS = new Set(["typecheck", "test-unit", "budgets", "build-artifacts"] // `test-native` saves `native-build-*`, `native-third-party-*` and its ccache through the cache // action's own post step. A warm run verifies nothing else, so nothing else runs. const WARM_JOBS = ["build-artifacts", "test-native"]; +// Strata terrain jobs: runs when terrain, example or core world modules change, or full qualification. +const STRATA_PATHS = [ + /^packages\/terrain\//u, + /^examples\/strata-terrain-preview\//u, + /^packages\/core\/(?:src\/world(?:-|\.ts)|src\/terrain-jobs|__tests__\/world-)/u, +]; + +export function isStrataPath(file) { + return STRATA_PATHS.some((pattern) => pattern.test(file)); +} + +function strataSelected(files, qualification) { + if (qualification) return true; + if (files.some(isStrataPath)) return true; + return false; +} + // Strength, not identity: a stronger source profile may serve a weaker requirement, never the reverse. const TARGET_RANKS = { main: 2, develop: 1, other: 0 }; const NATIVE_RANKS = { full: 2, reduced: 1, none: 0 }; @@ -184,6 +202,20 @@ export function selectionPlan( FULL_JOBS.map((name) => { // `ci` narrows the board to the gates that read the configuration and `warm` to the two cache // producers; neither widens one, so any other job is exempt exactly as it is for prose. + if (name === "strata") { + const required = full && strataSelected(files, qualification); + return [ + name, + { + required, + reason: required + ? qualification + ? `Full dependency closure: ${reason}` + : `Selected dependency closure: ${reason}` + : exemption(`Exempt: the ${selection} dependency closure does not reach ${name}`), + }, + ]; + } const required = (full && (name !== "test-native" || native)) || ((ciLane || template) && CI_JOBS.has(name)) || From c2bbd30fda32cf57c2fab8cc6bd028543e589cea Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 13:07:59 -0700 Subject: [PATCH 006/210] docs: record Strata proof integration and checkout reuse --- docs/PRDs/strata-terrain-hardening/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/PRDs/strata-terrain-hardening/README.md b/docs/PRDs/strata-terrain-hardening/README.md index acd343b4d..b85926480 100644 --- a/docs/PRDs/strata-terrain-hardening/README.md +++ b/docs/PRDs/strata-terrain-hardening/README.md @@ -36,4 +36,4 @@ Current baseline: `pnpm build` and `pnpm check:docs` passed; six documentation c ## Active worker checkouts -PRD-592 phase 1 is committed at `6c40d8757`; phase 2 remains open pending the 30% speed proof. Its worker watches the queued quiet benchmark and has released the EPIC index for integration. PRD-585 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-proof`, branch `epic/strata-terrain-proof`, based on `227cb944b`; its integration test passes, with census repair and CI wiring in progress. PRD-583 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-bake`, branch `epic/strata-terrain-bake`, based on `9e7a8cab0`, for headless GLB baking. Codex coordinates integration into PR #483. Coding arms share a provider lock. Cleanup must inspect ignored data and confirm each owner has stopped after integration. +PRD-592 phase 1 is committed at `6c40d8757`; phase 2 remains open pending the 30% speed proof. Its queued benchmark was cancelled after the quiet lock proved unavailable; baseline and scripts are preserved. PRD-585 local work is integrated at `845442617`: 215 focused tests pass on the combined candidate; real candidate CI and runtime acceptance remain open. Its released checkout `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-proof` is reused by PRD-593 on branch `epic/strata-three-patch`, based on `845442617`; ignored dependencies and artifacts are preserved. PRD-583 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-bake`, branch `epic/strata-terrain-bake`, based on `9e7a8cab0`, for headless GLB baking. PRD-587 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-editor-durability`, branch `epic/strata-editor-durability`, based on `c1e421198`, for editor durability. Codex coordinates integration into PR #483. Default coding arms share a Gemini provider lock; routes explicitly excluding Gemini after a verified task quality failure can run separately. Cleanup must inspect ignored data and confirm each owner has stopped after integration. From bd8072b6469b5039b55fdef77ac28c22fc025e5f Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 13:33:13 -0700 Subject: [PATCH 007/210] docs: record Strata runtime failures and editor proof --- .../strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md index 776bb65d1..3f1bd036b 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md @@ -60,3 +60,5 @@ index them in the example's `AGENTS.md`. - [ ] Consumer and editor scripts green in CI. proof: CI run link The local job wiring is verified; AC-1/AC-2 need a real candidate CI run with successful `strata` execution. Skipped draft checks are not runtime proof. + +Local runtime attempt on candidate `845442617` (2026-10-10, Debian laptop, headed WebGPU, verified Intel `gen-12lp`): workspace build passed and bake was current; terrain playtest failed with 58/62 assertions passing. Failures: `afterFirstFrameTaskMs` 490 > 250 ms, `timeToReadyMs` 21,395.6 > 15,000 ms, `maxViewGpuMs` 326.69 > 12 ms, and a `MeshBasicNodeMaterial` pipeline multisampled-binding mismatch (group 1, binding 2). Thresholds remain unchanged. The independent follow-up passed `test:terrain:editor` on the same Intel WebGPU candidate, including transform, conflict/reload/undo, tool and environment checks. `test:consumer` failed during scaffold install: the old Corepack launcher targeted nonexistent pnpm 12 `bin/pnpm.cjs`. The cached package exposes a native `pnpm` launcher, verified at version 12.10.1; a retry uses that launcher on a job-local PATH, without changing system defaults. That retry reached dependency installation, then failed closed with `ERR_PNPM_IGNORED_BUILDS` for core, runtime-native, esbuild and sharp; the generated project build-approval policy needs repair before consumer behavior can be verified. No approval policy was bypassed. This is failed local evidence, not CI acceptance. From 11e7fc71a321e42431b032abf82788623373ddc4 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 13:36:45 -0700 Subject: [PATCH 008/210] feat(assets): bake terrain worlds with instanced static species --- ...-bakes-headlessly-into-an-instanced-glb.md | 38 +- .../assets/__tests__/terrain-world.spec.ts | 425 +++++++++++++++++ packages/assets/package.json | 1 + packages/assets/src/index.ts | 14 + packages/assets/src/world/terrain-world.ts | 432 ++++++++++++++++++ packages/core/capabilities.json | 14 + .../references/capability-reference.md | 10 + packages/create-threenative/capabilities.json | 14 + pnpm-lock.yaml | 3 + scripts/api-surface.json | 1 + 10 files changed, 938 insertions(+), 14 deletions(-) create mode 100644 packages/assets/__tests__/terrain-world.spec.ts create mode 100644 packages/assets/src/world/terrain-world.ts diff --git a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md index 710f8be90..f72d4f5f6 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md @@ -1,6 +1,6 @@ # PRD-583 — A Strata world bakes headlessly into an instanced GLB -**Status:** NOT STARTED +**Status:** PARTIAL **Priority:** P1 — No headless path turns a Strata recipe into a GLB with its trees and rocks; PRD-584's agent handoff cannot start without AC-1 to AC-4. **Complexity:** 5 (MEDIUM); risk override: none **Owner:** ThreeNative maintainers @@ -58,38 +58,48 @@ flowchart LR ## Acceptance Criteria -- [ ] AC-1 [local]: `cookTerrainWorld` runs under plain Node 22 with no DOM globals and writes `world.glb` for the forest kit. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: pending. -- [ ] AC-2 [local]: Each species-and-LOD pair is one `EXT_mesh_gpu_instancing` node; node count does not grow with tree count. proof: same spec, glTF node census at 1 000 and 10 000 trees — Evidence: pending. -- [ ] AC-3 [local]: A vanilla three `GLTFLoader` loads `world.glb` into `InstancedMesh` objects whose instance count equals the bake's placement count. proof: `pnpm --filter strata-terrain-preview test:consumer` — Evidence: pending. -- [ ] AC-4 [local]: A placement mirrored in Z fails the cook with a named contact error. proof: spec case in `terrain-world.spec.ts` — Evidence: pending. -- [ ] AC-5 [local]: The world package written next to it passes `validateWorldPackage` and lists a GLB and at least one LOD per species. proof: `terrain-world.spec.ts` — Evidence: pending. +- [ ] AC-1 [local]: `cookTerrainWorld` runs under plain Node 22 with no DOM globals and writes `world.glb` for the forest kit. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 focused spec 7/10 passed; the cook writes `world.glb` for the evaluated-terrain fixture under Node. The forest-kit bake is not run, so this stays open. +- [ ] AC-2 [local]: Each species-and-LOD pair is one `EXT_mesh_gpu_instancing` node; node count does not grow with tree count. proof: same spec, glTF node census at 1 000 and 10 000 trees — Evidence: 2026-10-10 node count constant at 1k/10k and one node for a single-mesh species; a two-mesh species still yields one node per source mesh, which the source-GLB flatten step (next arm) must fix. +- [ ] AC-3 [local]: A vanilla three `GLTFLoader` loads `world.glb` into `InstancedMesh` objects whose instance count equals the bake's placement count. proof: `pnpm --filter strata-terrain-preview test:consumer` — Evidence: 2026-10-10 the spec's stock `GLTFLoader` reads instance count equal to placement count for the fixture; `test:consumer` is not run. +- [ ] AC-4 [local]: A placement mirrored in Z fails the cook with a named contact error. proof: spec case in `terrain-world.spec.ts` — Evidence: 2026-10-10 spec case green: `Placement '' of species 'fir' contact check failed ... mirrored-Z`. +- [ ] AC-5 [local]: The world package written next to it passes `validateWorldPackage` and lists a GLB and at least one LOD per species. proof: `terrain-world.spec.ts` — Evidence: 2026-10-10 `splat.rgba` and `placements.bin` byte-equal `bakeWorldPackage` output and explicit LOD files match source bytes; `validateWorldPackage` is not called by the spec. ## Integration Ledger | Capability | Reachable consumer/trigger | Replaces / disposition | Evidence | |---|---|---|---| -| Headless world GLB | `cookTerrainWorld` from `@threenative/assets`; kit `bake.mjs`; PRD-584 MCP tool | Browser `exportWorldGLB` stays for the editor until PRD-588 | AC-1, AC-3 | -| Instanced scatter in GLB | Any `GLTFLoader` consumer | Per-tree mesh nodes | AC-2 | +| Headless world GLB | `cookTerrainWorld` from `@threenative/assets`; kit `bake.mjs`; PRD-584 MCP tool | Browser `exportWorldGLB` stays for the editor until PRD-588 | AC-1, AC-3 (unverified) | +| Instanced scatter in GLB | Any `GLTFLoader` consumer | Per-tree mesh nodes | AC-2 (unverified) | ## Execution Phases #### Phase 1: terrain and scatter into one instanced GLB -**Status:** NOT STARTED +**Status:** done **Files:** `packages/assets/src/world/terrain-world.ts` (new), `packages/assets/src/index.ts`, `packages/assets/__tests__/terrain-world.spec.ts` **Implementation:** read `bakeTerrain` chunks and placements; group by species and LOD; write `EXT_mesh_gpu_instancing` nodes with gltf-transform; pass existing image bytes through; contact check per placement. -- [ ] Node-only cook writes `world.glb`. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` -- [ ] Node count is constant from 1 000 to 10 000 trees. proof: same spec -- [ ] Mirrored placements fail by name. proof: same spec +- [x] Node-only cook writes `world.glb`. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — 2026-10-10: 7/10 passed; cook writes `world.glb` for the evaluated-terrain fixture under Node. +- [x] Node count is constant from 1 000 to 10 000 trees. proof: same spec — 2026-10-10: node count equal at 1k and 10k, one instancing node for a single-mesh species. +- [x] Mirrored placements fail by name. proof: same spec — 2026-10-10: mirrored-Z case throws `Placement '' of species 'fir' contact check failed`. #### Phase 2: LODs and the world package -**Status:** NOT STARTED +**Status:** PARTIAL **Files:** `packages/assets/src/world/terrain-world.ts`, `packages/terrain/starter/forest/bake.mjs` **Implementation:** prefer authored `*-mid` / `*-lod1` GLBs; fall back to `modelPass({ simplify })`; fill `assets[].glb` and `lods` in `world.json`. - [ ] World package validates and carries LODs per species. proof: `terrain-world.spec.ts` - [ ] The forest kit bake emits both outputs. proof: `node packages/terrain/starter/forest/bake.mjs` then the spec's file check #### Phase 3: consumer load -**Status:** NOT STARTED +**Status:** PARTIAL **Files:** `examples/strata-terrain-preview/scripts/verify-consumer.mjs` **Implementation:** load the cooked `world.glb` with stock `GLTFLoader`; assert instance counts and that no mesh is a per-tree copy. - [ ] Vanilla loader gets `InstancedMesh` with the bake's counts. proof: `pnpm --filter strata-terrain-preview test:consumer` + +## Current plan + +1. Reuse `bakeTerrain` and `bakeWorldPackage`; preserve source glTF textures/transforms and verify real terrain, splat and species batching. +2. Repair the consumer script and run its actual forest bake/load proof after the focused assets gate passes. +3. Run required runtime and visual proof; keep every unsupported claim open. + +Initial review: six weak unit tests passed; `node --check examples/strata-terrain-preview/scripts/verify-consumer.mjs` failed at line 584, and assets typecheck failed with three errors. Cook also dropped model textures/transforms, omitted terrain without custom LOD data and zeroed splat bytes. That initial completion claim was rejected; corrected focused results follow. + +2026-10-10 focused correction: the cook uses `bakeTerrain` and `bakeWorldPackage`, preserves source image bytes and every source-node world transform through glTF-transform copying/flattening, rejects skinned or animated species, and produces one near-detail instance node per species. The 11-test spec passes (including reused source-mesh nodes, 1 000/10 000 placement census and exact splat/placement bytes); assets typecheck and scoped Biome pass, with one nonfatal cognitive-complexity warning. Node is v20.19.6, so Node 22 and the forest-kit claim remain unverified. Authored sibling LOD discovery, multi-layer terrain PBR appearance, actual forest bake/consumer and runtime/visual proof remain pending. No full-PRD completion is claimed. diff --git a/packages/assets/__tests__/terrain-world.spec.ts b/packages/assets/__tests__/terrain-world.spec.ts new file mode 100644 index 000000000..e415a08a6 --- /dev/null +++ b/packages/assets/__tests__/terrain-world.spec.ts @@ -0,0 +1,425 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { Document, type Buffer as GltfBuffer, type Material, NodeIO } from "@gltf-transform/core"; +import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; +import { type ITerrainState, Terrain, bakeTerrain, bakeWorldPackage } from "@threenative/terrain"; +import { PNG } from "pngjs"; +import { type Group, InstancedMesh, Mesh } from "three"; +import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { cookTerrainWorld } from "../src/world/terrain-world.js"; + +// GLTFLoader reads the global `self` while it parses. Node has none. +Object.assign(globalThis, { self: globalThis }); + +/** A real evaluated terrain. The fixed seed fixes every scatter position. */ +function evaluatedTerrain(): ITerrainState { + const state = new Terrain({ resolution: 33, seed: 73, size: 256 }) + .scatter({ id: "firs", asset: "fir", count: 40, minDistance: 3 }) + .evaluate(); + if (state.instances.length === 0) throw new Error("fixture terrain has no placements"); + return state; +} + +function io(): NodeIO { + return new NodeIO().registerExtensions([...ALL_EXTENSIONS]); +} + +/** One triangle with normals and UVs. Its vertices sit in the unit square on the XZ plane. */ +function triangle(doc: Document, buffer: GltfBuffer, material: Material) { + return doc + .createPrimitive() + .setAttribute( + "POSITION", + doc + .createAccessor() + .setType("VEC3") + .setArray(new Float32Array([0, 0, 0, 1, 0, 0, 0, 0, 1])) + .setBuffer(buffer), + ) + .setAttribute( + "NORMAL", + doc + .createAccessor() + .setType("VEC3") + .setArray(new Float32Array([0, 1, 0, 0, 1, 0, 0, 1, 0])) + .setBuffer(buffer), + ) + .setAttribute( + "TEXCOORD_0", + doc + .createAccessor() + .setType("VEC2") + .setArray(new Float32Array([0, 0, 1, 0, 0, 1])) + .setBuffer(buffer), + ) + .setIndices( + doc + .createAccessor() + .setType("SCALAR") + .setArray(new Uint16Array([0, 1, 2])) + .setBuffer(buffer), + ) + .setMaterial(material); +} + +/** A static species with one mesh of one primitive. */ +async function simpleSpecies(): Promise { + const doc = new Document(); + const buffer = doc.createBuffer(); + const scene = doc.createScene("Scene"); + const mesh = doc + .createMesh("rock") + .addPrimitive(triangle(doc, buffer, doc.createMaterial("rock"))); + scene.addChild(doc.createNode("rockNode").setMesh(mesh)); + return io().writeBinary(doc); +} + +/** A static species with two meshes. The trunk is textured and sits 5 m along +X in its node. */ +async function twoMeshSpecies(): Promise<{ glb: Uint8Array; png: Uint8Array }> { + const doc = new Document(); + const buffer = doc.createBuffer(); + const scene = doc.createScene("Scene"); + const png = PNG.sync.write(new PNG({ width: 2, height: 2 })); + const texture = doc.createTexture("bark").setImage(png).setMimeType("image/png"); + const bark = doc.createMaterial("bark").setBaseColorTexture(texture); + const trunk = doc.createMesh("trunk").addPrimitive(triangle(doc, buffer, bark)); + const crown = doc + .createMesh("crown") + .addPrimitive(triangle(doc, buffer, doc.createMaterial("leaf"))); + scene.addChild(doc.createNode("trunkNode").setMesh(trunk).setTranslation([5, 0, 0])); + scene.addChild(doc.createNode("crownNode").setMesh(crown)); + return { glb: await io().writeBinary(doc), png }; +} + +/** A static species whose one textured mesh is reused by two nodes, at X=0 and X=5. */ +async function sharedMeshSpecies(): Promise<{ glb: Uint8Array; png: Uint8Array }> { + const doc = new Document(); + const buffer = doc.createBuffer(); + const scene = doc.createScene("Scene"); + const png = PNG.sync.write(new PNG({ width: 2, height: 2 })); + const texture = doc.createTexture("shared").setImage(png).setMimeType("image/png"); + const material = doc.createMaterial("shared").setBaseColorTexture(texture); + const mesh = doc.createMesh("shared").addPrimitive(triangle(doc, buffer, material)); + scene.addChild(doc.createNode("node0").setMesh(mesh).setTranslation([0, 0, 0])); + scene.addChild(doc.createNode("node1").setMesh(mesh).setTranslation([5, 0, 0])); + return { glb: await io().writeBinary(doc), png }; +} + +/** A species with a skin. The cook must refuse it by name. */ +async function skinnedSpecies(): Promise { + const doc = new Document(); + const buffer = doc.createBuffer(); + const scene = doc.createScene("Scene"); + const joint = doc.createNode("root"); + const skin = doc.createSkin("rig").addJoint(joint); + const mesh = doc + .createMesh("rigged") + .addPrimitive(triangle(doc, buffer, doc.createMaterial("rigged"))); + scene.addChild(joint); + scene.addChild(doc.createNode("rigged").setMesh(mesh).setSkin(skin)); + return io().writeBinary(doc); +} + +/** Parses a GLB with the vanilla three loader, the same reader a game uses. */ +function loadWorld(glb: Uint8Array): Promise { + return new Promise((resolve, reject) => { + new GLTFLoader().parse(glb.slice().buffer, "", (gltf) => resolve(gltf.scene), reject); + }); +} + +describe("cookTerrainWorld", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(path.join(tmpdir(), "strata-terrain-world-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("writes the real terrain chunks and one instancing node for the species", async () => { + const state = evaluatedTerrain(); + const result = await cookTerrainWorld({ + state, + species: { fir: { glb: await simpleSpecies() } }, + out: dir, + }); + + const lod0 = bakeTerrain(state, { lodSteps: [1] }).lods[0]; + assert.ok(lod0); + const chunks = lod0.chunks; + expect(chunks.length).toBeGreaterThan(0); + const scene = await loadWorld(result.worldGlb); + const terrainNames: string[] = []; + const instanceCounts: number[] = []; + scene.traverse((object) => { + if (object instanceof InstancedMesh) instanceCounts.push(object.count); + else if (object instanceof Mesh && object.name.startsWith("terrain_")) + terrainNames.push(object.name); + }); + expect(terrainNames.sort()).toEqual(chunks.map((chunk) => chunk.name).sort()); + expect(instanceCounts).toEqual([state.instances.length]); + + for (const file of [ + "world.glb", + "world.json", + "heightmap.u16", + "placements.bin", + "splat.rgba", + "models/fir.glb", + ]) { + expect((await stat(path.join(dir, file))).size).toBeGreaterThan(0); + } + }); + + it("writes the splat and placement files exactly as bakeWorldPackage bakes them", async () => { + const state = evaluatedTerrain(); + const result = await cookTerrainWorld({ + state, + species: { fir: { glb: await simpleSpecies() } }, + out: dir, + }); + + const baked = bakeWorldPackage(state, { assets: result.manifest.assets }); + const splat = new Uint8Array(await readFile(path.join(dir, "splat.rgba"))); + expect(splat.some((byte) => byte !== 0)).toBe(true); + expect(splat).toEqual(baked.files["splat.rgba"]); + expect(new Uint8Array(await readFile(path.join(dir, "placements.bin")))).toEqual( + baked.files["placements.bin"], + ); + }); + + it("keeps one instancing node per species when its source has two meshes", async () => { + const state = evaluatedTerrain(); + const species = await twoMeshSpecies(); + const result = await cookTerrainWorld({ + state, + species: { fir: { glb: species.glb } }, + out: dir, + }); + + expect(result.instancedMeshCount).toBe(1); + expect(result.placementCounts.fir).toBe(state.instances.length); + const scene = await loadWorld(result.worldGlb); + const instanceCounts: number[] = []; + scene.traverse((object) => { + if (object instanceof InstancedMesh) instanceCounts.push(object.count); + }); + // One InstancedMesh per primitive; the two source meshes flatten into one glTF mesh. + expect(instanceCounts).toEqual([state.instances.length, state.instances.length]); + }); + + it("keeps source translation, texture bytes and vertex attributes when it flattens a species", async () => { + const species = await twoMeshSpecies(); + const result = await cookTerrainWorld({ + state: evaluatedTerrain(), + species: { fir: { glb: species.glb } }, + out: dir, + }); + + // The trunk's child translation becomes model-space bounds: x runs from 0 to 6. + const fir = result.manifest.assets.fir; + assert.ok(fir); + expect(fir.bounds).toEqual({ min: [0, 0, 0], max: [6, 0, 1] }); + + const doc = await io().readBinary(result.worldGlb); + const textured = doc + .getRoot() + .listMaterials() + .find((m) => m.getBaseColorTexture() !== null); + assert.ok(textured); + const baseColorTexture = textured.getBaseColorTexture(); + assert.ok(baseColorTexture); + const image = baseColorTexture.getImage(); + assert.ok(image); + expect(new Uint8Array(image)).toEqual(new Uint8Array(species.png)); + const instancedNode = doc + .getRoot() + .listNodes() + .find((node) => node.getExtension("EXT_mesh_gpu_instancing") !== null); + assert.ok(instancedNode); + const instancedMesh = instancedNode.getMesh(); + assert.ok(instancedMesh); + const primitive = instancedMesh.listPrimitives().find((p) => p.getMaterial() === textured); + assert.ok(primitive); + const positionAttr = primitive.getAttribute("POSITION"); + assert.ok(positionAttr); + const positions = positionAttr.getArray() as Float32Array; + expect(Math.min(...positions.filter((_, i) => i % 3 === 0))).toBe(5); + expect(primitive.getAttribute("NORMAL")).not.toBeNull(); + expect(primitive.getAttribute("TEXCOORD_0")).not.toBeNull(); + expect(primitive.getMode()).toBe(4); // TRIANGLES + }); + + it("flattens one shared mesh at each node's own transform", async () => { + const species = await sharedMeshSpecies(); + const state = evaluatedTerrain(); + const result = await cookTerrainWorld({ + state, + species: { fir: { glb: species.glb } }, + out: dir, + }); + + expect(result.instancedMeshCount).toBe(1); + expect(result.placementCounts.fir).toBe(state.instances.length); + + const doc = await io().readBinary(result.worldGlb); + const instancedNode = doc + .getRoot() + .listNodes() + .find((node) => node.getExtension("EXT_mesh_gpu_instancing") !== null); + assert.ok(instancedNode); + const instancedMesh = instancedNode.getMesh(); + assert.ok(instancedMesh); + const primitives = instancedMesh.listPrimitives(); + // Both source nodes survive as their own primitive: one translated to x=0, one to x=5. + expect(primitives.length).toBe(2); + const minX = primitives.map((p) => { + const positionAttr = p.getAttribute("POSITION"); + assert.ok(positionAttr); + return Math.min(...(positionAttr.getArray() as Float32Array).filter((_, i) => i % 3 === 0)); + }); + expect(minX.sort((a, b) => a - b)).toEqual([0, 5]); + + const textured = doc + .getRoot() + .listMaterials() + .find((m) => m.getBaseColorTexture() !== null); + assert.ok(textured); + const baseColorTexture = textured.getBaseColorTexture(); + assert.ok(baseColorTexture); + const image = baseColorTexture.getImage(); + assert.ok(image); + expect(new Uint8Array(image)).toEqual(new Uint8Array(species.png)); + }); + + it("writes explicit LOD files and authored bounds without changing them", async () => { + const near = await simpleSpecies(); + const lod = await simpleSpecies(); + const result = await cookTerrainWorld({ + state: evaluatedTerrain(), + species: { + fir: { + glb: near, + bounds: { min: [-1, 0, -1], max: [1, 2, 1] }, + lods: [{ distance: 45, glb: lod }], + }, + }, + out: dir, + }); + + expect(result.manifest.assets.fir).toMatchObject({ + glb: "models/fir.glb", + bounds: { min: [-1, 0, -1], max: [1, 2, 1] }, + lods: [{ distance: 45, glb: "models/fir_lod1.glb" }], + }); + expect(new Uint8Array(await readFile(path.join(dir, "models", "fir.glb")))).toEqual(near); + expect(new Uint8Array(await readFile(path.join(dir, "models", "fir_lod1.glb")))).toEqual(lod); + }); + + it("fails before writing when a placement names a species that is not in the map", async () => { + await expect( + cookTerrainWorld({ state: evaluatedTerrain(), species: {}, out: dir }), + ).rejects.toThrow(/species 'fir'/); + await expect(stat(path.join(dir, "world.glb"))).rejects.toThrow(); + }); + + it("fails before writing when a placement has a non-finite position", async () => { + const state = evaluatedTerrain(); + const [first, ...rest] = state.instances; + assert.ok(first); + const broken = { + ...state, + instances: [ + { + ...first, + position: [Number.NaN, first.position[1], first.position[2]] as [number, number, number], + }, + ...rest, + ], + }; + await expect( + cookTerrainWorld({ + state: broken, + species: { fir: { glb: await simpleSpecies() } }, + out: dir, + }), + ).rejects.toThrow(/non-finite/); + await expect(stat(path.join(dir, "world.glb"))).rejects.toThrow(); + }); + + it("refuses a skinned species by name before writing", async () => { + await expect( + cookTerrainWorld({ + state: evaluatedTerrain(), + species: { fir: { glb: await skinnedSpecies() } }, + out: dir, + }), + ).rejects.toThrow(/species 'fir' .*skin/); + await expect(stat(path.join(dir, "world.glb"))).rejects.toThrow(); + }); + + it("names a placement that is mirrored in Z as a contact failure", async () => { + const state = evaluatedTerrain(); + const spacing = state.size / (state.resolution - 1); + // A deterministic north-to-south ramp makes mirroring change the ground by half the map size. + state.height.set( + Array.from(state.height, (_, i) => Math.floor(i / state.resolution) * spacing), + ); + const first = state.instances[0]; + assert.ok(first); + const item = { + ...first, + position: [0, state.size / 4, -state.size / 4] as [number, number, number], + }; + const mirrored = { + ...item, + position: [item.position[0], item.position[1], -item.position[2]] as [number, number, number], + }; + const error = await cookTerrainWorld({ + state: { ...state, instances: [mirrored] }, + species: { fir: { glb: await simpleSpecies() } }, + out: dir, + }).then( + () => null, + (caught: Error) => caught, + ); + expect(error?.message).toContain( + `Placement '${item.id}' of species 'fir' contact check failed`, + ); + expect(error?.message).toContain("mirrored-Z"); + }); + + it("keeps the node count constant from 1 000 to 10 000 placements", async () => { + const base = evaluatedTerrain(); + const withPlacements = (count: number): ITerrainState => ({ + ...base, + instances: Array.from({ length: count }, (_, i) => { + const placement = base.instances[i % base.instances.length]; + assert.ok(placement); + return { ...placement, id: `fir_${i}` }; + }), + }); + const species = { fir: { glb: await simpleSpecies() } }; + + const small = await cookTerrainWorld({ + state: withPlacements(1_000), + species, + out: path.join(dir, "1k"), + }); + const large = await cookTerrainWorld({ + state: withPlacements(10_000), + species, + out: path.join(dir, "10k"), + }); + + expect(large.nodeCount).toBe(small.nodeCount); + expect(small.instancedMeshCount).toBe(1); + expect(large.instancedMeshCount).toBe(1); + expect(large.placementCounts.fir).toBe(10_000); + }); +}); diff --git a/packages/assets/package.json b/packages/assets/package.json index 77da8576c..c94d4b250 100644 --- a/packages/assets/package.json +++ b/packages/assets/package.json @@ -49,6 +49,7 @@ "@gltf-transform/core": "catalog:", "@gltf-transform/extensions": "catalog:", "@gltf-transform/functions": "catalog:", + "@threenative/terrain": "workspace:*", "@wasm-audio-decoders/ogg-vorbis": "0.1.20", "draco3dgltf": "^1.5.7", "jpeg-js": "0.4.4", diff --git a/packages/assets/src/index.ts b/packages/assets/src/index.ts index 1fc00ba3b..7ce9072d3 100644 --- a/packages/assets/src/index.ts +++ b/packages/assets/src/index.ts @@ -258,3 +258,17 @@ export type { IDedupeCensus, IMaterialBucket, IMaterialState } from "./content/d */ export { censusDocument, materialStateOf, totalCensus } from "./content/census.js"; export type { IContentCensus } from "./content/census.js"; +/** + * Bake evaluated terrain and static species into an instanced GLB and streaming world package. + * @situation bake a terrain world headlessly into a GLB with instanced trees and rocks + */ +export { cookTerrainWorld } from "./world/terrain-world.js"; +export type { + ICookTerrainWorldMaterialLayer, + ICookTerrainWorldOptions, + ICookTerrainWorldResult, + ISpeciesLODSpec, + ISpeciesSpec, + IWorldPackageAsset, + IWorldPackageManifest, +} from "./world/terrain-world.js"; diff --git a/packages/assets/src/world/terrain-world.ts b/packages/assets/src/world/terrain-world.ts new file mode 100644 index 000000000..c9e9fe259 --- /dev/null +++ b/packages/assets/src/world/terrain-world.ts @@ -0,0 +1,432 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { Document, type Mesh, type Node, NodeIO } from "@gltf-transform/core"; +import { ALL_EXTENSIONS, EXTMeshGPUInstancing } from "@gltf-transform/extensions"; +import { + copyToDocument, + createDefaultPropertyResolver, + getBounds, + prune, + transformMesh, + unpartition, +} from "@gltf-transform/functions"; +import { bakeTerrain, bakeWorldPackage, sampleHeight } from "@threenative/terrain"; +import type { IPlacement, ITerrainState } from "@threenative/terrain"; +import { createGltfReader, readGltfDocument } from "../gltf-io.js"; +import { modelPass } from "../passes/model.js"; + +/** The world.json contract, taken from the shared baker rather than restated here. */ +export type IWorldPackageManifest = ReturnType["manifest"]; +/** One streamed species entry, taken from the shared baker's own option type. */ +export type IWorldPackageAsset = NonNullable; + +export interface ISpeciesLODSpec { + readonly glb?: string | Uint8Array; + readonly distance: number; +} + +export interface ISpeciesSpec { + readonly glb: string | Uint8Array; + readonly bounds?: { + readonly min: readonly [number, number, number]; + readonly max: readonly [number, number, number]; + }; + readonly lods?: readonly ISpeciesLODSpec[]; + readonly maxDistance?: number; +} + +export interface ICookTerrainWorldMaterialLayer { + readonly id: string; + readonly diff?: string | Uint8Array; + readonly nrm?: string | Uint8Array; + readonly diffBytes?: Uint8Array; + readonly nrmBytes?: Uint8Array; + readonly diffMimeType?: string; + readonly nrmMimeType?: string; +} + +export interface ICookTerrainWorldOptions { + readonly state: ITerrainState; + readonly species: Readonly>; + readonly out: string; + readonly materials?: { + readonly layers?: readonly ICookTerrainWorldMaterialLayer[]; + readonly texturesDir?: string; + }; + readonly cellSize?: number; + readonly extraLayers?: Readonly>; +} + +export interface ICookTerrainWorldResult { + readonly worldGlb: Uint8Array; + readonly manifest: IWorldPackageManifest; + readonly placementCounts: Readonly>; + readonly nodeCount: number; + readonly instancedMeshCount: number; +} + +/** A placement as a world-package record: `item.transform` when authored, else the scatter pose. */ +function poseOf(item: IPlacement): { + position: [number, number, number]; + quaternion: [number, number, number, number]; + scale: [number, number, number]; + grounding: boolean; +} { + if (item.transform !== undefined) { + const { position, quaternion, scale, grounding = true } = item.transform; + return { + position: [position[0], position[1], position[2]], + quaternion: [quaternion[0], quaternion[1], quaternion[2], quaternion[3]], + scale: [scale[0], scale[1], scale[2]], + grounding, + }; + } + return { + position: [item.position[0], item.position[1], item.position[2]], + quaternion: [0, Math.sin(item.rotation / 2), 0, Math.cos(item.rotation / 2)], + scale: [item.scale, item.scale, item.scale], + grounding: true, + }; +} + +async function resolveBytes(input: string | Uint8Array): Promise { + if (typeof input === "string") { + return new Uint8Array(await readFile(input)); + } + return input; +} + +/** A species id becomes an output filename, so refuse anything that is not a bare name. */ +const SAFE_SPECIES_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; + +/** Source world-space bounds when the species authors none: geometry plus its node world matrices. */ +function sourceBounds(speciesDoc: Document): { + min: [number, number, number]; + max: [number, number, number]; +} { + const root = speciesDoc.getRoot(); + const scene = root.getDefaultScene() ?? root.listScenes()[0] ?? null; + if (scene) { + const { min, max } = getBounds(scene); + if (min.every(Number.isFinite) && max.every(Number.isFinite)) { + return { min: [min[0], min[1], min[2]], max: [max[0], max[1], max[2]] }; + } + } + return { min: [0, 0, 0], max: [0, 0, 0] }; +} + +export async function cookTerrainWorld( + options: ICookTerrainWorldOptions, +): Promise { + const { state, species, out } = options; + if (!state || typeof state.size !== "number" || typeof state.resolution !== "number") { + throw new Error("cookTerrainWorld: state with size and resolution is required"); + } + if (!state.height || state.height.length !== state.resolution * state.resolution) { + throw new Error("cookTerrainWorld: state.height length does not match resolution * resolution"); + } + + // A species id becomes an output filename, so every key must be a safe bare name. + for (const speciesId of Object.keys(species)) { + if (!SAFE_SPECIES_ID.test(speciesId)) { + throw new Error( + `cookTerrainWorld: species id '${speciesId}' is not a safe output filename; use letters, digits, '.', '_' or '-'.`, + ); + } + } + + // Every placement must name a species the caller supplied, before anything is read or written. + for (const item of state.instances) { + if (species[item.asset] === undefined) { + throw new Error( + `cookTerrainWorld: placement '${item.id}' names species '${item.asset}', which is not in the species map.`, + ); + } + } + + // Contact check: a grounded placement must meet the sampled terrain surface, within the + // quantisation the uint16 heightmap costs. A mirrored-Z placement reads the wrong row and misses. + const spacing = state.size / (state.resolution - 1); + let heightMin = Number.POSITIVE_INFINITY; + let heightMax = Number.NEGATIVE_INFINITY; + for (const height of state.height) { + if (height < heightMin) heightMin = height; + if (height > heightMax) heightMax = height; + } + const range = heightMax - heightMin; + const tolerance = spacing + (Number.isFinite(range) ? range / 65_535 : 0); + for (const item of state.instances) { + const pose = poseOf(item); + if (!pose.grounding) continue; + const groundY = sampleHeight(state, pose.position[0], pose.position[2]); + const difference = Math.abs(pose.position[1] - groundY); + if (Number.isFinite(difference) && difference > tolerance) { + throw new Error( + `Placement '${item.id}' of species '${item.asset}' contact check failed: base Y=${pose.position[1]} misses terrain ground Y=${groundY} by ${difference.toFixed(3)}m (> tolerance ${tolerance.toFixed(3)}m). Check for mirrored-Z or invalid coordinates.`, + ); + } + } + + // Build every species' manifest entry (and its LOD bytes) before the shared bake. Each species + // model is parsed exactly once here: the same document later flattens into its instancing node. + const assets: Record = {}; + const speciesFiles: { + speciesId: string; + lod0: Uint8Array; + speciesDoc: Document; + lods: { distance: number; glb: Uint8Array; filename: string }[]; + }[] = []; + + for (const [speciesId, spec] of Object.entries(species)) { + const lod0 = await resolveBytes(spec.glb); + + // Parse once, and refuse anything that deforms or replays before a single byte is written. + const reader = await createGltfReader(Buffer.from(lod0)); + const speciesDoc = await readGltfDocument(reader, Buffer.from(lod0)); + if (speciesDoc.getRoot().listSkins().length > 0) { + throw new Error( + `cookTerrainWorld: species '${speciesId}' declares a skin; only static species can be instanced.`, + ); + } + if (speciesDoc.getRoot().listAnimations().length > 0) { + throw new Error( + `cookTerrainWorld: species '${speciesId}' declares an animation; only static species can be instanced.`, + ); + } + + const lods: { distance: number; glb: Uint8Array; filename: string }[] = []; + if (spec.lods && spec.lods.length > 0) { + for (const [i, level] of spec.lods.entries()) { + if (level.glb === undefined) continue; + const bytes = await resolveBytes(level.glb); + lods.push({ + distance: level.distance, + glb: bytes, + filename: `${speciesId}_lod${i + 1}.glb`, + }); + } + } else { + const pass = modelPass({ + simplify: { ratio: 0.5 }, + textures: "none", + passes: { meshopt: false, prune: true, dedup: true }, + }); + const simplified = await pass.apply(Buffer.from(lod0), `${speciesId}_lod1.glb`); + const bytes = Uint8Array.from(Buffer.isBuffer(simplified) ? simplified : simplified.buffer); + lods.push({ distance: 40, glb: bytes, filename: `${speciesId}_lod1.glb` }); + } + + speciesFiles.push({ speciesId, lod0, speciesDoc, lods }); + + assets[speciesId] = { + glb: `models/${speciesId}.glb`, + bounds: spec.bounds ?? sourceBounds(speciesDoc), + lods: lods.map((level) => ({ distance: level.distance, glb: `models/${level.filename}` })), + ...(spec.maxDistance === undefined ? {} : { maxDistance: spec.maxDistance }), + }; + } + + // Fail closed before any glTF work or output write: the shared baker validates finite heights, + // known assets and uniform poses, and hands back the exact binary files this cook writes. + const { manifest, files } = bakeWorldPackage(state, { + assets, + ...(options.cellSize === undefined ? {} : { cellSize: options.cellSize }), + ...(options.extraLayers === undefined ? {} : { layers: options.extraLayers }), + }); + + // Setup glTF document + const doc = new Document(); + const scene = doc.createScene("TerrainWorld"); + const buffer = doc.createBuffer(); + const instancingExt = doc.createExtension(EXTMeshGPUInstancing); + + // Terrain mesh chunks, always from the shared baker's LOD0. + const chunks = bakeTerrain(state).lods[0]?.chunks ?? []; + + // Material for terrain with PBR layer maps if provided + const terrainMaterial = doc.createMaterial("TerrainMaterial"); + if (options.materials?.layers && options.materials.layers.length > 0) { + const baseLayer = options.materials.layers[0]; + if (baseLayer) { + let diffBytes = baseLayer.diffBytes; + if (!diffBytes && baseLayer.diff) { + diffBytes = await resolveBytes(baseLayer.diff); + } + if (diffBytes) { + const texture = doc + .createTexture("TerrainBaseDiff") + .setImage(diffBytes) + .setMimeType(baseLayer.diffMimeType ?? "image/jpeg"); + terrainMaterial.setBaseColorTexture(texture); + } + let nrmBytes = baseLayer.nrmBytes; + if (!nrmBytes && baseLayer.nrm) { + nrmBytes = await resolveBytes(baseLayer.nrm); + } + if (nrmBytes) { + const nrmTexture = doc + .createTexture("TerrainBaseNrm") + .setImage(nrmBytes) + .setMimeType(baseLayer.nrmMimeType ?? "image/jpeg"); + terrainMaterial.setNormalTexture(nrmTexture); + } + } + } + + // Add chunk meshes + for (let c = 0; c < chunks.length; c++) { + const chunk = chunks[c]; + if (!chunk) continue; + const mesh = doc.createMesh(chunk.name ?? `terrain_chunk_${c}`); + const prim = doc.createPrimitive(); + prim.setMaterial(terrainMaterial); + prim.setAttribute( + "POSITION", + doc.createAccessor().setType("VEC3").setArray(chunk.positions).setBuffer(buffer), + ); + prim.setAttribute( + "NORMAL", + doc.createAccessor().setType("VEC3").setArray(chunk.normals).setBuffer(buffer), + ); + prim.setAttribute( + "TEXCOORD_0", + doc.createAccessor().setType("VEC2").setArray(chunk.uvs).setBuffer(buffer), + ); + prim.setIndices( + doc.createAccessor().setType("SCALAR").setArray(chunk.indices).setBuffer(buffer), + ); + mesh.addPrimitive(prim); + const node = doc.createNode(chunk.name ?? `terrain_node_${c}`).setMesh(mesh); + scene.addChild(node); + } + + // Group placements by species + const placementsBySpecies = new Map(); + const placementCounts: Record = {}; + for (const item of state.instances) { + let list = placementsBySpecies.get(item.asset); + if (!list) { + list = []; + placementsBySpecies.set(item.asset, list); + } + list.push(item); + placementCounts[item.asset] = (placementCounts[item.asset] ?? 0) + 1; + } + + // One EXT_mesh_gpu_instancing node per species: every logical placement is one instance, and the + // species' source meshes flatten into a single mesh so the node count stays constant. + for (const { speciesId, speciesDoc } of speciesFiles) { + const items = placementsBySpecies.get(speciesId) ?? []; + if (items.length === 0) continue; + + const count = items.length; + const translations = new Float32Array(count * 3); + const rotations = new Float32Array(count * 4); + const scales = new Float32Array(count * 3); + + for (const [i, item] of items.entries()) { + const pose = poseOf(item); + translations[i * 3 + 0] = pose.position[0]; + translations[i * 3 + 1] = pose.position[1]; + translations[i * 3 + 2] = pose.position[2]; + + rotations[i * 4 + 0] = pose.quaternion[0]; + rotations[i * 4 + 1] = pose.quaternion[1]; + rotations[i * 4 + 2] = pose.quaternion[2]; + rotations[i * 4 + 3] = pose.quaternion[3]; + + scales[i * 3 + 0] = pose.scale[0]; + scales[i * 3 + 1] = pose.scale[1]; + scales[i * 3 + 2] = pose.scale[2]; + } + + const transAccessor = doc + .createAccessor() + .setType("VEC3") + .setArray(translations) + .setBuffer(buffer); + const rotAccessor = doc.createAccessor().setType("VEC4").setArray(rotations).setBuffer(buffer); + const scaleAccessor = doc.createAccessor().setType("VEC3").setArray(scales).setBuffer(buffer); + + const batch = instancingExt + .createInstancedMesh() + .setAttribute("TRANSLATION", transAccessor) + .setAttribute("ROTATION", rotAccessor) + .setAttribute("SCALE", scaleAccessor); + + // Any extension the source uses must exist on the target document before its properties copy. + type ExtensionCtor = Parameters[0]; + for (const sourceExtension of speciesDoc.getRoot().listExtensionsUsed()) { + doc.createExtension(sourceExtension.constructor as unknown as ExtensionCtor); + } + + // Bring meshes, materials, textures and accessors across, then bake each source node's world + // matrix into its geometry. A fresh resolver per node keeps geometry and accessors private, so + // one source mesh reused by several nodes keeps each node's own transform. `transformMesh` + // keeps every attribute and material. + const rootScene = + speciesDoc.getRoot().getDefaultScene() ?? speciesDoc.getRoot().listScenes()[0] ?? null; + const meshNodes: { node: Node; mesh: Mesh }[] = []; + rootScene?.traverse((node: Node) => { + const mesh = node.getMesh(); + if (mesh) meshNodes.push({ node, mesh }); + }); + + const combined = doc.createMesh(`${speciesId}_mesh`); + for (const { node, mesh } of meshNodes) { + const resolve = createDefaultPropertyResolver(doc, speciesDoc); + const targetMesh = copyToDocument(doc, speciesDoc, [mesh], resolve).get(mesh) as Mesh; + transformMesh(targetMesh, node.getWorldMatrix()); + for (const primitive of targetMesh.listPrimitives()) { + targetMesh.removePrimitive(primitive); + combined.addPrimitive(primitive); + } + } + + const instancedNode = doc + .createNode(`${speciesId}_instances`) + .setMesh(combined) + .setExtension("EXT_mesh_gpu_instancing", batch); + scene.addChild(instancedNode); + } + + await doc.transform(unpartition()); + // Keep solid-texture removal off: species materials are authored, and a flat image is not ours to drop. + await doc.transform(prune({ keepSolidTextures: true })); + + const writer = new NodeIO().registerExtensions([...ALL_EXTENSIONS]); + const worldGlb = await writer.writeBinary(doc); + + // Write files to out directory + await mkdir(out, { recursive: true }); + await mkdir(path.join(out, "models"), { recursive: true }); + await writeFile(path.join(out, "world.glb"), worldGlb); + + for (const [name, bytes] of Object.entries(files)) { + await writeFile(path.join(out, name), bytes); + } + + for (const { speciesId, lod0, lods } of speciesFiles) { + const nearGlbName = `${speciesId}.glb`; + await writeFile(path.join(out, "models", nearGlbName), lod0); + for (const level of lods) { + await writeFile(path.join(out, "models", level.filename), level.glb); + } + } + + await writeFile(path.join(out, "world.json"), `${JSON.stringify(manifest, null, 2)}\n`); + + const finalNodes = doc.getRoot().listNodes(); + let instancedMeshCount = 0; + for (const n of finalNodes) { + if (n.getExtension("EXT_mesh_gpu_instancing") !== null) instancedMeshCount++; + } + + return { + worldGlb, + manifest, + placementCounts, + nodeCount: finalNodes.length, + instancedMeshCount, + }; +} diff --git a/packages/core/capabilities.json b/packages/core/capabilities.json index 9d8840d26..f75f5f5dc 100644 --- a/packages/core/capabilities.json +++ b/packages/core/capabilities.json @@ -84,6 +84,20 @@ "supersedes": [], "symbol": "compileAssets" }, + { + "aliases": [], + "constraints": [], + "example": "", + "importPath": "@threenative/assets", + "kind": "function", + "overrides": [], + "package": "@threenative/assets", + "signature": "export async function cookTerrainWorld( options: ICookTerrainWorldOptions, ): Promise { … }", + "situations": ["bake a terrain world headlessly into a GLB with instanced trees and rocks"], + "summary": "Bake evaluated terrain and static species into an instanced GLB and streaming world package.", + "supersedes": [], + "symbol": "cookTerrainWorld" + }, { "aliases": [], "constraints": [ diff --git a/packages/create-threenative/agent-docs/references/capability-reference.md b/packages/create-threenative/agent-docs/references/capability-reference.md index 52be24855..ea8855eda 100644 --- a/packages/create-threenative/agent-docs/references/capability-reference.md +++ b/packages/create-threenative/agent-docs/references/capability-reference.md @@ -70,6 +70,16 @@ export async function compileAssets( options: IAssetCompileOptions = { … } const result = await compileAssets({ source: "assets", output: "public" }); ``` +### `cookTerrainWorld` + +`function` — Bake evaluated terrain and static species into an instanced GLB and streaming world package. + +```ts +export async function cookTerrainWorld( options: ICookTerrainWorldOptions, ): Promise { … } +``` + +- **Use when:** bake a terrain world headlessly into a GLB with instanced trees and rocks + ### `dedupeMaterials` `function` — Collapse materials that became identical once their textures shared an atlas page, and count the buckets a merge would find — before and after — so the promise can be checked rather than made. diff --git a/packages/create-threenative/capabilities.json b/packages/create-threenative/capabilities.json index 9d8840d26..f75f5f5dc 100644 --- a/packages/create-threenative/capabilities.json +++ b/packages/create-threenative/capabilities.json @@ -84,6 +84,20 @@ "supersedes": [], "symbol": "compileAssets" }, + { + "aliases": [], + "constraints": [], + "example": "", + "importPath": "@threenative/assets", + "kind": "function", + "overrides": [], + "package": "@threenative/assets", + "signature": "export async function cookTerrainWorld( options: ICookTerrainWorldOptions, ): Promise { … }", + "situations": ["bake a terrain world headlessly into a GLB with instanced trees and rocks"], + "summary": "Bake evaluated terrain and static species into an instanced GLB and streaming world package.", + "supersedes": [], + "symbol": "cookTerrainWorld" + }, { "aliases": [], "constraints": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49674c51c..4ea3a2507 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -961,6 +961,9 @@ importers: '@gltf-transform/functions': specifier: 'catalog:' version: 4.4.2(@types/node@20.19.43) + '@threenative/terrain': + specifier: workspace:* + version: link:../terrain '@wasm-audio-decoders/ogg-vorbis': specifier: 0.1.20 version: 0.1.20 diff --git a/scripts/api-surface.json b/scripts/api-surface.json index 3974495b4..09c5e9a74 100644 --- a/scripts/api-surface.json +++ b/scripts/api-surface.json @@ -7,6 +7,7 @@ "@threenative/assets#audioPass": "export function audioPass(options: IAudioPassOptions = { … }", "@threenative/assets#censusDocument": "export function censusDocument( model: string, document: Document, pageSize = 4_096, ): IContentCensus { … }", "@threenative/assets#compileAssets": "export async function compileAssets( options: IAssetCompileOptions = { … }", + "@threenative/assets#cookTerrainWorld": "export async function cookTerrainWorld( options: ICookTerrainWorldOptions, ): Promise { … }", "@threenative/assets#dedupeMaterials": "export function dedupeMaterials(materials: readonly IMaterialState[]): { … }", "@threenative/assets#formatAudioSizes": "export function formatAudioSizes(rows: readonly IAudioRow[]): readonly string[] { … }", "@threenative/assets#formatHealthReport": "export function formatHealthReport(report: IAssetHealthReport): readonly string[] { … }", From ede0cfffa91b888e6c52478417b7a49a4a0f6bf9 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 13:42:58 -0700 Subject: [PATCH 009/210] docs(strata): record export integration and desktop runtime results --- .../strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md | 2 ++ docs/PRDs/strata-terrain-hardening/README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md index 3f1bd036b..66242d2af 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md @@ -62,3 +62,5 @@ index them in the example's `AGENTS.md`. The local job wiring is verified; AC-1/AC-2 need a real candidate CI run with successful `strata` execution. Skipped draft checks are not runtime proof. Local runtime attempt on candidate `845442617` (2026-10-10, Debian laptop, headed WebGPU, verified Intel `gen-12lp`): workspace build passed and bake was current; terrain playtest failed with 58/62 assertions passing. Failures: `afterFirstFrameTaskMs` 490 > 250 ms, `timeToReadyMs` 21,395.6 > 15,000 ms, `maxViewGpuMs` 326.69 > 12 ms, and a `MeshBasicNodeMaterial` pipeline multisampled-binding mismatch (group 1, binding 2). Thresholds remain unchanged. The independent follow-up passed `test:terrain:editor` on the same Intel WebGPU candidate, including transform, conflict/reload/undo, tool and environment checks. `test:consumer` failed during scaffold install: the old Corepack launcher targeted nonexistent pnpm 12 `bin/pnpm.cjs`. The cached package exposes a native `pnpm` launcher, verified at version 12.10.1; a retry uses that launcher on a job-local PATH, without changing system defaults. That retry reached dependency installation, then failed closed with `ERR_PNPM_IGNORED_BUILDS` for core, runtime-native, esbuild and sharp; the generated project build-approval policy needs repair before consumer behavior can be verified. No approval policy was bypassed. This is failed local evidence, not CI acceptance. + +A desktop comparison on the same executable candidate used verified NVIDIA Turing WebGPU and passed 59/62 assertions. Its failures were `afterFirstFrameTaskMs` 12,635 > 250 ms, `timeToReadyMs` 38,741.8 > 15,000 ms, and `maxViewGpuMs` 35.22 > 12 ms. The Intel shader error did not recur. This is another failed local performance result, not runtime or CI acceptance. Generated consumers currently omit `packageManager`, allowing Corepack to select ambient pnpm 12; the shared scaffolder repair is active and preserves explicit dependency-build approvals. diff --git a/docs/PRDs/strata-terrain-hardening/README.md b/docs/PRDs/strata-terrain-hardening/README.md index b85926480..73bc5e2be 100644 --- a/docs/PRDs/strata-terrain-hardening/README.md +++ b/docs/PRDs/strata-terrain-hardening/README.md @@ -36,4 +36,4 @@ Current baseline: `pnpm build` and `pnpm check:docs` passed; six documentation c ## Active worker checkouts -PRD-592 phase 1 is committed at `6c40d8757`; phase 2 remains open pending the 30% speed proof. Its queued benchmark was cancelled after the quiet lock proved unavailable; baseline and scripts are preserved. PRD-585 local work is integrated at `845442617`: 215 focused tests pass on the combined candidate; real candidate CI and runtime acceptance remain open. Its released checkout `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-proof` is reused by PRD-593 on branch `epic/strata-three-patch`, based on `845442617`; ignored dependencies and artifacts are preserved. PRD-583 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-bake`, branch `epic/strata-terrain-bake`, based on `9e7a8cab0`, for headless GLB baking. PRD-587 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-editor-durability`, branch `epic/strata-editor-durability`, based on `c1e421198`, for editor durability. Codex coordinates integration into PR #483. Default coding arms share a Gemini provider lock; routes explicitly excluding Gemini after a verified task quality failure can run separately. Cleanup must inspect ignored data and confirm each owner has stopped after integration. +PRD-592 phase 1 is committed at `6c40d8757`; phase 2 remains open pending the 30% speed proof. Its queued benchmark was cancelled after the quiet lock proved unavailable; baseline and scripts are preserved. PRD-585 local work is integrated at `845442617`: 215 focused tests pass; real candidate CI and runtime acceptance remain open. Its released checkout `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-proof` is now owned by the PRD-585 scaffold bootstrap repair on branch `epic/strata-runtime-proof`; PRD-593 is queued, with its source and artifacts preserved. PRD-583 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-bake`, branch `epic/strata-terrain-bake`, based on `9e7a8cab0`. Its assets phase is integrated at `11e7fc71a`: frozen installation and 11 focused terrain-world tests pass on the EPIC candidate; GLB consumer, forest, LOD and runtime acceptance remain open. PRD-587 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-editor-durability`, branch `epic/strata-editor-durability`, based on `c1e421198`. Its queue correction remains isolated after review found discard/reload paths losing edits despite passing tests; the next bounded correction uses an ordered local-intent journal with explicit lifecycle regressions. Codex coordinates integration into PR #483. Default coding arms share a Gemini provider lock; routes explicitly excluding Gemini after a verified task quality failure can run separately. Cleanup must inspect ignored data and confirm each owner has stopped after integration. From 5b8fbfa60252aac9896487069dda2c0e5e05d434 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 13:52:45 -0700 Subject: [PATCH 010/210] fix(terrain): start editor on invalid saves and expose recovery --- ...-the-terrain-editor-never-drops-an-edit.md | 34 ++++++----- .../terrain/__tests__/editor-document.spec.ts | 60 +++++++++++++++++++ packages/terrain/src/editor/index.ts | 8 +++ packages/terrain/src/editor/server.ts | 56 ++++++++++++++++- 4 files changed, 142 insertions(+), 16 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md b/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md index 91b2a6dd9..e7f9c727c 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md @@ -1,6 +1,6 @@ # PRD-587 — The terrain editor never drops an edit -**Status:** NOT STARTED +**Status:** IN PROGRESS **Priority:** P1 — A second stroke during a save is silently rolled back and a failed save overwrites the local edit without saying what was lost (AC-1, AC-2): user work is lost. **Complexity:** 4 (MEDIUM); risk override: none **Owner:** ThreeNative maintainers @@ -38,11 +38,11 @@ the last valid state". The worker is recreated after a crash. Typed arrays trans ## Acceptance Criteria -- [ ] AC-1 [local]: Two strokes 50 ms apart both persist to `world.json`. proof: a stroke step in `examples/strata-terrain-preview/playtests/editor.playtest.json` — Evidence: pending. -- [ ] AC-2 [local]: A save answered 409 leaves the edit in a visible "not saved" list with retry and discard. proof: `pnpm exec vitest run packages/terrain/__tests__/editor-save-queue.spec.ts` — Evidence: pending. -- [ ] AC-3 [local]: The dev server starts on a malformed `world.json`, and "reload from disk" recovers after the file is fixed. proof: `editor-document.spec.ts` case — Evidence: pending. -- [ ] AC-4 [local]: After the worker is terminated mid-build, the next edit rebuilds terrain. proof: `editor.playtest.json` step — Evidence: pending. -- [ ] AC-5 [local]: The worker's height, normal and splat buffers arrive transferred (detached on the worker side). proof: `editor-save-queue.spec.ts` or a worker spec — Evidence: pending. +- [ ] AC-1 [local]: Two strokes 50 ms apart both persist to `world.json`. proof: a stroke step in `examples/strata-terrain-preview/playtests/editor.playtest.json` — Evidence: pending. Not run this arm (playtest deferred). +- [ ] AC-2 [local]: A save answered 409 leaves the edit in a visible "not saved" list with retry and discard. proof: a playtest that forces a 409 and reads the rendered not-saved list — Evidence: untested UI. `editor-save-queue.spec.ts` (20/20) proves each refused edit stays in `unsent`, discarding one keeps the others, and retry/discard behave, but no test drives the rendered list, so this stays open. +- [x] AC-3 [local]: The dev server starts on a malformed `world.json`, and "reload from disk" recovers after the file is fixed. proof: `editor-document.spec.ts` case — Evidence: 9/9 passed; malformed start reports a diagnostic, reload recovers, and restore is refused when the file was never valid. +- [ ] AC-4 [local]: After the worker is terminated mid-build, the next edit rebuilds terrain. proof: `editor.playtest.json` step — Evidence: pending. Not run this arm (worker/playtest deferred). +- [ ] AC-5 [local]: The worker's height, normal and splat buffers arrive transferred (detached on the worker side). proof: `editor-save-queue.spec.ts` or a worker spec — Evidence: pending. Fabricated `state.normal` zeros deleted; transfer list kept for the next phase. ## Integration Ledger @@ -54,14 +54,20 @@ the last valid state". The worker is recreated after a crash. Typed arrays trans ## Execution Phases #### Phase 1: commands and the queue -**Status:** NOT STARTED -**Files:** `packages/terrain/src/editor/app.js`, `packages/terrain/src/editor/server.ts`, `packages/terrain/__tests__/editor-save-queue.spec.ts` (new) -- [ ] Rapid strokes persist. proof: `editor.playtest.json` -- [ ] Rejected edits stay visible. proof: `editor-save-queue.spec.ts` +**Status:** PARTIAL — queue, config/whole-document fallback and refused-edit list landed; rapid-stroke and refused-list UI playtests deferred +**Files:** `packages/terrain/src/editor/app.js`, `packages/terrain/src/editor/saveQueue.ts` (new), `packages/terrain/src/editor/server.ts`, `packages/terrain/__tests__/editor-save-queue.spec.ts` (new) +- [ ] Rapid strokes persist. proof: `editor.playtest.json` — not run this arm. +- [ ] Refused edits are kept and never resent. proof: `editor-save-queue.spec.ts` — 17/17 passed; local-observation diffs, coalesce fix, config/asset as whole document, one 409 rebase-retry that merges unrelated external edits, per-field whole-document rebase, adopt-during-save, retry, and discard (no resend). #### Phase 2: recovery paths -**Status:** NOT STARTED +**Status:** PARTIAL — malformed-file recovery and never-valid restore guard landed; worker/playtest deferred **Files:** `packages/terrain/src/editor/server.ts`, `app.js`, `worker.js` -- [ ] Malformed file starts and recovers. proof: `editor-document.spec.ts` -- [ ] Worker crash recovers. proof: `editor.playtest.json` -- [ ] Buffers transfer. proof: worker spec +- [x] Malformed file starts and recovers. proof: `editor-document.spec.ts` — 9/9 passed. `restore` now refuses when the document was never valid. +- [ ] Worker crash recovers. proof: `editor.playtest.json` — not run this arm. +- [ ] Buffers transfer. proof: worker spec — deferred; fabricated `state.normal` zeros deleted. + +## Execution notes + +2026-10-10: offline frozen dependencies installed in the isolated durability checkout; `pnpm prd:progress` runs (1/5 acceptance criteria verified). Server recovery/controller changes are the accepted phase: malformed startup remains diagnostic/read-only, reload recovers a repaired file, atomic restore requires a genuinely previously valid state. Independently ran `editor-document.spec.ts` (9/9). + +The command queue, UI recovery controls and worker edits remain an uncommitted draft, excluded from the server-phase commit. Three reviewed incomplete fixes led to an explicitly approved narrower intent-journal replan. That arm reproduced discard losing sibling rejected edits, then added an ordered journal and reload/restore guards. Independently ran `editor-save-queue.spec.ts`, `editor-document.spec.ts`, `terrain-consumer.spec.ts` (43/43 total); edited JS syntax checks pass. Arm ran terrain build/publint and Biome successfully. These units do not prove the rendered editor lifecycle. Queue review remains open for reload-response races, queued whole-document saves after an external conflict, and synchronising replayed merged state with the editable terrain. AC-1/AC-2/AC-4/AC-5 and the queue phase remain open; no playtest has been run. diff --git a/packages/terrain/__tests__/editor-document.spec.ts b/packages/terrain/__tests__/editor-document.spec.ts index 2b84cd860..7af3f9a59 100644 --- a/packages/terrain/__tests__/editor-document.spec.ts +++ b/packages/terrain/__tests__/editor-document.spec.ts @@ -2,6 +2,7 @@ import { readFileSync, writeFileSync } from "node:fs"; import { createServer as createHttpServer, request } from "node:http"; import { join } from "node:path"; import { Terrain } from "@threenative/terrain"; +import { TerrainEditorController } from "@threenative/terrain/editor"; import { TerrainEditorDocument, terrainEditor } from "@threenative/terrain/editor/server"; import { type ViteDevServer, createServer } from "vite"; import { afterEach, describe, expect, it } from "vitest"; @@ -218,6 +219,65 @@ describe("project terrain editor document", () => { cancellation.abort(); await reader?.cancel().catch(() => {}); }); + + it("starts the dev server on a malformed world.json, reports diagnostic and recovers via reload after file is fixed", async () => { + const { root, path } = fixture(); + writeFileSync(path, "{broken json"); + const editor = terrainEditor({ documentPath: path }); + const server = await createServer({ + root, + configFile: false, + logLevel: "silent", + server: { host: "127.0.0.1", port: 0 }, + plugins: [editor], + }); + servers.push(server); + await server.listen(); + const address = server.httpServer?.address(); + const controller = new TerrainEditorController( + `http://127.0.0.1:${(address as { port: number }).port}/terrain-editor/`, + ); + + // Dev server started on malformed file in read-only diagnostic state + const initialSnapshot = await controller.snapshot(); + expect(initialSnapshot.diagnostic).toBeTruthy(); + + // Commit is rejected with 409 while document on disk is invalid + await expect( + controller.commit({ + baseRevision: initialSnapshot.revision, + commands: [{ op: "remove", id: "hills" }], + }), + ).rejects.toThrow("Disk document is invalid"); + + // Fix file on disk + const valid = { + version: 1, + recipe: new Terrain({ size: 512, resolution: 257, seed: 42 }).toJSON(), + }; + writeFileSync(path, JSON.stringify(valid, null, 2)); + + // Reload from disk recovers + const reloaded = await controller.reload(); + expect(reloaded.diagnostic).toBeNull(); + expect(reloaded.document.recipe.config.seed).toBe(42); + + // Overwrite with last valid state restores disk if corrupted again + writeFileSync(path, "corrupted again"); + const restored = await controller.restoreLastValid(); + expect(restored.diagnostic).toBeNull(); + expect(JSON.parse(readFileSync(path, "utf8")).recipe.config.seed).toBe(42); + }); + + it("rejects restore when the document never held a valid state and keeps the original", () => { + const { path } = fixture(); + writeFileSync(path, "{broken since the very first read"); + const store = new TerrainEditorDocument(path); + expect(store.snapshot().diagnostic).toBeTruthy(); + expect(() => store.restoreLastValid()).toThrow("No valid save to restore"); + expect(readFileSync(path, "utf8")).toBe("{broken since the very first read"); + }); + it("uses an explicitly configured private forward and rejects a failed forward", async () => { let target: string | undefined; const proxy = createHttpServer((incoming, response) => { diff --git a/packages/terrain/src/editor/index.ts b/packages/terrain/src/editor/index.ts index f57e46299..784ea0bcb 100644 --- a/packages/terrain/src/editor/index.ts +++ b/packages/terrain/src/editor/index.ts @@ -250,6 +250,12 @@ export class TerrainEditorController { ): Promise { return this.#request("document", transaction); } + reload(): Promise { + return this.#request("reload", {}); + } + restoreLastValid(): Promise { + return this.#request("restore", {}); + } subscribe(listener: (snapshot: IEditorSnapshot) => void, onError: () => void): () => void { const events = new EventSource(new URL("events", this.baseUrl)); events.onmessage = ({ data }) => { @@ -295,6 +301,8 @@ export async function mountTerrainEditor(options: { commit: (transaction: Parameters[0]) => controller.commit(transaction), getSnapshot: () => controller.snapshot(), + reload: () => controller.reload(), + restoreLastValid: () => controller.restoreLastValid(), cameraOperation: (operation: unknown, baseRevision: string) => controller.camera(operation as ICameraOperation, baseRevision), assetOperation: (operation: unknown, baseRevision: string) => diff --git a/packages/terrain/src/editor/server.ts b/packages/terrain/src/editor/server.ts index f259a17da..f55a369b7 100644 --- a/packages/terrain/src/editor/server.ts +++ b/packages/terrain/src/editor/server.ts @@ -150,12 +150,26 @@ class EditorError extends Error { export class TerrainEditorDocument { readonly path: string; #snapshot: IEditorSnapshot; + #lastValidDocument: IAuthoringDocument | null = null; #listeners = new Set<(snapshot: IEditorSnapshot) => void>(); constructor(path: string) { if (!isAbsolute(path)) throw new Error("Document path must be absolute"); this.path = resolve(path); - const document = this.#read(); - this.#snapshot = { document, revision: revision(document), diagnostic: null }; + try { + const document = this.#read(); + this.#lastValidDocument = document; + this.#snapshot = { document, revision: revision(document), diagnostic: null }; + } catch (error) { + const fallback: IAuthoringDocument = { + version: 1, + recipe: new Terrain().toJSON(), + }; + this.#snapshot = { + document: fallback, + revision: revision(fallback), + diagnostic: error instanceof Error ? error.message : String(error), + }; + } } #read(): IAuthoringDocument { const info = lstatSync(this.path); @@ -177,6 +191,7 @@ export class TerrainEditorDocument { try { const document = this.#read(); const nextRevision = revision(document); + this.#lastValidDocument = document; if (nextRevision === this.#snapshot.revision && this.#snapshot.diagnostic === null) return; this.#snapshot = { document, revision: nextRevision, diagnostic: null }; } catch (error) { @@ -184,6 +199,33 @@ export class TerrainEditorDocument { } this.#emit(); } + restoreLastValid(): IEditorSnapshot { + // Never overwrite a malformed original with a synthetic fallback: without a real valid save + // there is nothing to restore, so the caller must fix the file and reload instead. + if (!this.#lastValidDocument) + throw new EditorError(409, "No valid save to restore; the document has never been valid"); + const target = this.#lastValidDocument; + const temporary = `${this.path}.${randomUUID()}.tmp`; + try { + writeFileSync(temporary, `${JSON.stringify(target, null, 2)}\n`, { flag: "wx", mode: 0o600 }); + renameSync(temporary, this.path); + } catch (error) { + try { + unlinkSync(temporary); + } catch (cleanupError) { + if ((cleanupError as NodeJS.ErrnoException).code !== "ENOENT") + throw new AggregateError( + [error, cleanupError], + "Document save and temporary-file cleanup failed", + ); + } + throw error; + } + this.#lastValidDocument = target; + this.#snapshot = { document: target, revision: revision(target), diagnostic: null }; + this.#emit(); + return this.snapshot(); + } commit(input: unknown): IEditorSnapshot { this.refresh(); if (this.#snapshot.diagnostic) @@ -230,6 +272,7 @@ export class TerrainEditorDocument { throw error; } this.#snapshot = { document: next, revision: nextRevision, diagnostic: null }; + this.#lastValidDocument = next; this.#emit(); return this.snapshot(); } @@ -386,6 +429,15 @@ export function terrainEditor(options: { json(response, 200, document.snapshot()); return; } + if (request.method === "POST" && path === `${PREFIX}api/reload`) { + document.refresh(); + json(response, 200, document.snapshot()); + return; + } + if (request.method === "POST" && path === `${PREFIX}api/restore`) { + json(response, 200, document.restoreLastValid()); + return; + } if (request.method === "GET" && path === `${PREFIX}api/events`) { response.writeHead(200, { "content-type": "text/event-stream", From 7673267abc7a08d9d78a4e1925883ea25664339a Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 13:56:09 -0700 Subject: [PATCH 011/210] fix(terrain): keep recovery controller independent of draft UI --- .../PRD-587-the-terrain-editor-never-drops-an-edit.md | 8 +++++--- docs/PRDs/strata-terrain-hardening/README.md | 2 +- packages/terrain/src/editor/index.ts | 2 -- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md b/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md index e7f9c727c..9f3cc9e19 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md @@ -39,7 +39,7 @@ the last valid state". The worker is recreated after a crash. Typed arrays trans ## Acceptance Criteria - [ ] AC-1 [local]: Two strokes 50 ms apart both persist to `world.json`. proof: a stroke step in `examples/strata-terrain-preview/playtests/editor.playtest.json` — Evidence: pending. Not run this arm (playtest deferred). -- [ ] AC-2 [local]: A save answered 409 leaves the edit in a visible "not saved" list with retry and discard. proof: a playtest that forces a 409 and reads the rendered not-saved list — Evidence: untested UI. `editor-save-queue.spec.ts` (20/20) proves each refused edit stays in `unsent`, discarding one keeps the others, and retry/discard behave, but no test drives the rendered list, so this stays open. +- [ ] AC-2 [local]: A save answered 409 leaves the edit in a visible "not saved" list with retry and discard. proof: a playtest that forces a 409 and reads the rendered not-saved list — Evidence: untested UI. the isolated draft queue tests exercise retention, retry and selected discard, but no test drives the rendered list, so this stays open. - [x] AC-3 [local]: The dev server starts on a malformed `world.json`, and "reload from disk" recovers after the file is fixed. proof: `editor-document.spec.ts` case — Evidence: 9/9 passed; malformed start reports a diagnostic, reload recovers, and restore is refused when the file was never valid. - [ ] AC-4 [local]: After the worker is terminated mid-build, the next edit rebuilds terrain. proof: `editor.playtest.json` step — Evidence: pending. Not run this arm (worker/playtest deferred). - [ ] AC-5 [local]: The worker's height, normal and splat buffers arrive transferred (detached on the worker side). proof: `editor-save-queue.spec.ts` or a worker spec — Evidence: pending. Fabricated `state.normal` zeros deleted; transfer list kept for the next phase. @@ -54,10 +54,10 @@ the last valid state". The worker is recreated after a crash. Typed arrays trans ## Execution Phases #### Phase 1: commands and the queue -**Status:** PARTIAL — queue, config/whole-document fallback and refused-edit list landed; rapid-stroke and refused-list UI playtests deferred +**Status:** PARTIAL — server recovery is integrated; queue and refused-edit UI remain isolated drafts pending lifecycle review and playtests **Files:** `packages/terrain/src/editor/app.js`, `packages/terrain/src/editor/saveQueue.ts` (new), `packages/terrain/src/editor/server.ts`, `packages/terrain/__tests__/editor-save-queue.spec.ts` (new) - [ ] Rapid strokes persist. proof: `editor.playtest.json` — not run this arm. -- [ ] Refused edits are kept and never resent. proof: `editor-save-queue.spec.ts` — 17/17 passed; local-observation diffs, coalesce fix, config/asset as whole document, one 409 rebase-retry that merges unrelated external edits, per-field whole-document rebase, adopt-during-save, retry, and discard (no resend). +- [ ] Refused edits are kept and never resent. proof: `editor-save-queue.spec.ts` and rendered editor playtest — isolated draft units pass; asynchronous reload, whole-document rebasing and editable terrain synchronisation remain under review. No queue implementation is integrated yet. #### Phase 2: recovery paths **Status:** PARTIAL — malformed-file recovery and never-valid restore guard landed; worker/playtest deferred @@ -71,3 +71,5 @@ the last valid state". The worker is recreated after a crash. Typed arrays trans 2026-10-10: offline frozen dependencies installed in the isolated durability checkout; `pnpm prd:progress` runs (1/5 acceptance criteria verified). Server recovery/controller changes are the accepted phase: malformed startup remains diagnostic/read-only, reload recovers a repaired file, atomic restore requires a genuinely previously valid state. Independently ran `editor-document.spec.ts` (9/9). The command queue, UI recovery controls and worker edits remain an uncommitted draft, excluded from the server-phase commit. Three reviewed incomplete fixes led to an explicitly approved narrower intent-journal replan. That arm reproduced discard losing sibling rejected edits, then added an ordered journal and reload/restore guards. Independently ran `editor-save-queue.spec.ts`, `editor-document.spec.ts`, `terrain-consumer.spec.ts` (43/43 total); edited JS syntax checks pass. Arm ran terrain build/publint and Biome successfully. These units do not prove the rendered editor lifecycle. Queue review remains open for reload-response races, queued whole-document saves after an external conflict, and synchronising replayed merged state with the editable terrain. AC-1/AC-2/AC-4/AC-5 and the queue phase remain open; no playtest has been run. + +Parent integration: the initial focused run loaded stale terrain output (7/9), then rebuilding exposed premature recovery callbacks passed to the unchanged UI. Removed only those two mount bindings; the controller methods remain available. Rebuilt terrain successfully, including declarations/publint, then independently passed all 9 document recovery tests. UI bindings remain part of the isolated durability draft. diff --git a/docs/PRDs/strata-terrain-hardening/README.md b/docs/PRDs/strata-terrain-hardening/README.md index 73bc5e2be..be1e8fa23 100644 --- a/docs/PRDs/strata-terrain-hardening/README.md +++ b/docs/PRDs/strata-terrain-hardening/README.md @@ -36,4 +36,4 @@ Current baseline: `pnpm build` and `pnpm check:docs` passed; six documentation c ## Active worker checkouts -PRD-592 phase 1 is committed at `6c40d8757`; phase 2 remains open pending the 30% speed proof. Its queued benchmark was cancelled after the quiet lock proved unavailable; baseline and scripts are preserved. PRD-585 local work is integrated at `845442617`: 215 focused tests pass; real candidate CI and runtime acceptance remain open. Its released checkout `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-proof` is now owned by the PRD-585 scaffold bootstrap repair on branch `epic/strata-runtime-proof`; PRD-593 is queued, with its source and artifacts preserved. PRD-583 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-bake`, branch `epic/strata-terrain-bake`, based on `9e7a8cab0`. Its assets phase is integrated at `11e7fc71a`: frozen installation and 11 focused terrain-world tests pass on the EPIC candidate; GLB consumer, forest, LOD and runtime acceptance remain open. PRD-587 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-editor-durability`, branch `epic/strata-editor-durability`, based on `c1e421198`. Its queue correction remains isolated after review found discard/reload paths losing edits despite passing tests; the next bounded correction uses an ordered local-intent journal with explicit lifecycle regressions. Codex coordinates integration into PR #483. Default coding arms share a Gemini provider lock; routes explicitly excluding Gemini after a verified task quality failure can run separately. Cleanup must inspect ignored data and confirm each owner has stopped after integration. +PRD-592 phase 1 is committed at `6c40d8757`; phase 2 remains open pending the 30% speed proof. Its queued benchmark was cancelled after the quiet lock proved unavailable; baseline and scripts are preserved. PRD-585 local work is integrated at `845442617`: 215 focused tests pass; real candidate CI and runtime acceptance remain open. Its released checkout `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-proof` is now owned by the PRD-585 scaffold bootstrap repair on branch `epic/strata-runtime-proof`; PRD-593 is queued, with its source and artifacts preserved. PRD-583 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-bake`, branch `epic/strata-terrain-bake`, based on `9e7a8cab0`. Its assets phase is integrated at `11e7fc71a`: frozen installation and 11 focused terrain-world tests pass on the EPIC candidate; GLB consumer, forest, LOD and runtime acceptance remain open. PRD-587 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-editor-durability`, branch `epic/strata-editor-durability`, based on `c1e421198`. Its server recovery phase is integrated at `5b8fbfa60`; the parent terrain build and 9 document tests pass after separating premature UI bindings. A fresh owner is auditing the isolated intent journal for asynchronous reload, whole-document rebasing and editable terrain synchronisation; queue and worker acceptance remain open. Codex coordinates integration into PR #483. Default coding arms share a Gemini provider lock; routes explicitly excluding Gemini after a verified task quality failure can run separately. Cleanup must inspect ignored data and confirm each owner has stopped after integration. diff --git a/packages/terrain/src/editor/index.ts b/packages/terrain/src/editor/index.ts index 784ea0bcb..da161530a 100644 --- a/packages/terrain/src/editor/index.ts +++ b/packages/terrain/src/editor/index.ts @@ -301,8 +301,6 @@ export async function mountTerrainEditor(options: { commit: (transaction: Parameters[0]) => controller.commit(transaction), getSnapshot: () => controller.snapshot(), - reload: () => controller.reload(), - restoreLastValid: () => controller.restoreLastValid(), cameraOperation: (operation: unknown, baseRevision: string) => controller.camera(operation as ICameraOperation, baseRevision), assetOperation: (operation: unknown, baseRevision: string) => From dde88e30c6438b0d091e138d29af339962784b4c Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 13:57:17 -0700 Subject: [PATCH 012/210] fix(scaffold): pin supported pnpm before consumer installs --- .../PRD-585-strata-proof-runs-in-ci.md | 2 + .../__tests__/scaffold.spec.ts | 45 +++++++++++++------ packages/create-threenative/src/index.ts | 8 +++- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md index 66242d2af..fd81339da 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md @@ -64,3 +64,5 @@ The local job wiring is verified; AC-1/AC-2 need a real candidate CI run with su Local runtime attempt on candidate `845442617` (2026-10-10, Debian laptop, headed WebGPU, verified Intel `gen-12lp`): workspace build passed and bake was current; terrain playtest failed with 58/62 assertions passing. Failures: `afterFirstFrameTaskMs` 490 > 250 ms, `timeToReadyMs` 21,395.6 > 15,000 ms, `maxViewGpuMs` 326.69 > 12 ms, and a `MeshBasicNodeMaterial` pipeline multisampled-binding mismatch (group 1, binding 2). Thresholds remain unchanged. The independent follow-up passed `test:terrain:editor` on the same Intel WebGPU candidate, including transform, conflict/reload/undo, tool and environment checks. `test:consumer` failed during scaffold install: the old Corepack launcher targeted nonexistent pnpm 12 `bin/pnpm.cjs`. The cached package exposes a native `pnpm` launcher, verified at version 12.10.1; a retry uses that launcher on a job-local PATH, without changing system defaults. That retry reached dependency installation, then failed closed with `ERR_PNPM_IGNORED_BUILDS` for core, runtime-native, esbuild and sharp; the generated project build-approval policy needs repair before consumer behavior can be verified. No approval policy was bypassed. This is failed local evidence, not CI acceptance. A desktop comparison on the same executable candidate used verified NVIDIA Turing WebGPU and passed 59/62 assertions. Its failures were `afterFirstFrameTaskMs` 12,635 > 250 ms, `timeToReadyMs` 38,741.8 > 15,000 ms, and `maxViewGpuMs` 35.22 > 12 ms. The Intel shader error did not recur. This is another failed local performance result, not runtime or CI acceptance. Generated consumers currently omit `packageManager`, allowing Corepack to select ambient pnpm 12; the shared scaffolder repair is active and preserves explicit dependency-build approvals. +||||||| parent of 27566ccc1 (fix(scaffold): pin supported pnpm before consumer installs) +Bootstrap repair (2026-10-10): generated projects now pin the shipped `pnpm@10.25.0` before dependency installation, including both omitted and present local-package overrides. The build-script allowlist is unchanged. The new regression fails on the original source (missing `packageManager`); the repaired full scaffold suite passes 68 tests. Focused scaffolder TypeScript and Biome checks pass (two existing complexity warnings), and the scaffolder build passes. The actual Node 20/Corepack consumer rerun is pending the laptop thermal guard; AC-1, AC-2 and runtime phase boxes remain open until their required proof exists. diff --git a/packages/create-threenative/__tests__/scaffold.spec.ts b/packages/create-threenative/__tests__/scaffold.spec.ts index e1ef4417c..54a838983 100644 --- a/packages/create-threenative/__tests__/scaffold.spec.ts +++ b/packages/create-threenative/__tests__/scaffold.spec.ts @@ -231,20 +231,20 @@ const PRD_201_PARENT_SCAFFOLD_HASHES: Readonly> = { // strata branch: measured through createProject on the merged tree. // Recomputed 2026-10-10 after integrating develop 1a5314a1: measured through createProject on // the merged tree, including its latest template changes. - "action-rpg": "29a2e2f8a8e1dbe84bed6b9752f9b50bc359c0526649b74236cab66e3ce5beec", - minimal: "29de540167ba97c5fd3883c0cf8c3a5435686312a97826426871b21a76f18607", - platformer: "ca93bd64f1efedd927b9cc428e1a20742a89d82930f8296fe0054ddb1d934ab8", - puzzle: "6b6896141c66be49c8a1a519b6c53039cfacb337fa816ad212b80c65daa864d7", - racing: "fd9affe9069f72f1c3a93eb1b126caeb156cb56e342c0768c646cbf1a831af34", - rain: "38302a90e87c5c1e4f864a75a6d4e8bdf108059df470798741a74cc6d3d990e0", - rts: "d7f3f1e9c3664386d32f99a8bdb8b5267632843f5dd2ed1804128fe3ddd0752d", - runner: "11a74a807e5e2f80c703098f3059bef04d3b8b42170b9ba68e37f71c247f5b27", + "action-rpg": "72539cd4803384cbf8d1d13dcc80116740fd241e2122a19e7b9efd5b6023a145", + minimal: "286787918a1d8aa9e6a7d1ea7cddfeb6016b8582edf4947099771d7a4d5d77c2", + platformer: "fb3acdd77377eb38039aaa6fa634b0e301bb0b42b579260d90948256513d2fce", + puzzle: "d2aa9144346b81cad693db90c38e223c269baaaca00ac2015eb3a22d0c6b0373", + racing: "c0692610b232e12a659eeb591afc7dc03cff265cc278c773f1731b331bcd3382", + rain: "a5485e777dbf56e578641f078cb426821e1f64952dd8ec7ffb1674b1ee8586d0", + rts: "9e8f064a139ee84c0a30079e97f3ff910724b6a9a7bfe5e61d2a915091318dbe", + runner: "3c425e56f0ae0193fd4af6587ebf97e9b94102c3d70d644fc1692d8ce375051a", // Initial finite-height readiness plus its scene-owned lifecycle helper and mirrored docs. - sailing: "3303a84c672a02dbbb5100cc4d647940c83c8a119e55c8cebf498b710cdd3e43", - shooter: "b856407fa828dc9ac76991cbffc4da8d327179a7f901229ccb2670cd4a486bd8", - snow: "0d3c4003a00e9b0772a877359ee204cffde78d2be21449e97ccc6e93ef941248", - starter: "27e4be7581d9ba96af39e2263c7645678aa30de6cb7b8fda4e0abb8195d6ee0b", - "tower-defense": "ff1a494154b4d2db224143046d4b2ffb02dd9cf74ad602b38af7eb72071042b3", + sailing: "8255df7db8cd434c3b832ae7f9dc71f11145839ebbd0012f876fea766362d824", + shooter: "f29ccbc565b468c0c476078f39103469bc59cd074c4511e90a62cac4b52680cb", + snow: "7a49725bf82aaa6373851f4d326d71f0a0981562a0a011312e462696acea5bf9", + starter: "b2a367f2f73cd700bd5e1d447fa810ba823ce4a71f3b54e07d8bdf5e1c45726e", + "tower-defense": "fbb8c4f10c0ee48f244a53bc82920c6089440bc1d73a6ad33a1a65e383e7ccc6", }; const GENERATED_SCAFFOLD_METADATA = @@ -518,6 +518,25 @@ describe("create-threenative", () => { expect(source.match(/replaceAll\(placeholder, value\)/gu)).toHaveLength(1); }); + it.each([undefined, {}])( + "pins the supported packageManager in generated projects (%j)", + async (packageSources) => { + const root = await makeTempDir("threenative-scaffold-pm-"); + try { + const created = await createProject( + { install: false, packageSources, target: "pm-game", template: "starter" }, + root, + ); + const manifest = JSON.parse( + await readFile(path.join(created.target, "package.json"), "utf8"), + ) as { packageManager?: string }; + expect(manifest.packageManager).toBe("pnpm@10.25.0"); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, + ); + it("keeps every no-install scaffold tree byte-stable against the PRD parent", async () => { const root = await makeTempDir("threenative-scaffold-stability-"); try { diff --git a/packages/create-threenative/src/index.ts b/packages/create-threenative/src/index.ts index d33d41137..470823d29 100644 --- a/packages/create-threenative/src/index.ts +++ b/packages/create-threenative/src/index.ts @@ -64,6 +64,7 @@ const TEXT_FILE_EXTENSIONS = new Set([".css", ".html", ".json", ".md", ".svg", " const TEMPLATE_NAME = /^[a-z][a-z0-9-]*$/u; const SHARED_AGENT_MARKER_LINE = /^\r?\n?/gmu; const DEFAULT_TEMPLATE = "starter"; +const SUPPORTED_PACKAGE_MANAGER = "pnpm@10.25.0"; export function templateRoot(): string { return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../templates"); @@ -523,14 +524,19 @@ async function applyPackageSources( target: string, packageSources: IScaffoldOptions["packageSources"], ): Promise { - if (packageSources === undefined) return; const packagePath = path.join(target, "package.json"); const packageJson = JSON.parse(await readFile(packagePath, "utf8")) as { dependencies?: Record; devDependencies?: Record; optionalDependencies?: Record; + packageManager?: string; pnpm?: { overrides?: Record }; }; + packageJson.packageManager = SUPPORTED_PACKAGE_MANAGER; + if (packageSources === undefined) { + await writeFile(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`); + return; + } const localSource = (source: string): string => { const filePath = source.startsWith("file:") ? source.slice("file:".length) : source; return `file:${path.resolve(filePath)}`; From 4cf0664e7f2631a312c85588b0a3dd49170d7e98 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 13:48:44 -0700 Subject: [PATCH 013/210] feat(terrain): cook forest kit and verify vanilla GLB consumers --- ...-bakes-headlessly-into-an-instanced-glb.md | 22 ++--- .../scripts/verify-consumer.mjs | 70 +++++++++++++++- .../assets/__tests__/terrain-world.spec.ts | 32 ++++++++ packages/terrain/starter/forest/bake.mjs | 81 ++++++++++--------- 4 files changed, 158 insertions(+), 47 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md index f72d4f5f6..c56875a8b 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md @@ -59,10 +59,10 @@ flowchart LR ## Acceptance Criteria - [ ] AC-1 [local]: `cookTerrainWorld` runs under plain Node 22 with no DOM globals and writes `world.glb` for the forest kit. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 focused spec 7/10 passed; the cook writes `world.glb` for the evaluated-terrain fixture under Node. The forest-kit bake is not run, so this stays open. -- [ ] AC-2 [local]: Each species-and-LOD pair is one `EXT_mesh_gpu_instancing` node; node count does not grow with tree count. proof: same spec, glTF node census at 1 000 and 10 000 trees — Evidence: 2026-10-10 node count constant at 1k/10k and one node for a single-mesh species; a two-mesh species still yields one node per source mesh, which the source-GLB flatten step (next arm) must fix. -- [ ] AC-3 [local]: A vanilla three `GLTFLoader` loads `world.glb` into `InstancedMesh` objects whose instance count equals the bake's placement count. proof: `pnpm --filter strata-terrain-preview test:consumer` — Evidence: 2026-10-10 the spec's stock `GLTFLoader` reads instance count equal to placement count for the fixture; `test:consumer` is not run. -- [ ] AC-4 [local]: A placement mirrored in Z fails the cook with a named contact error. proof: spec case in `terrain-world.spec.ts` — Evidence: 2026-10-10 spec case green: `Placement '' of species 'fir' contact check failed ... mirrored-Z`. -- [ ] AC-5 [local]: The world package written next to it passes `validateWorldPackage` and lists a GLB and at least one LOD per species. proof: `terrain-world.spec.ts` — Evidence: 2026-10-10 `splat.rgba` and `placements.bin` byte-equal `bakeWorldPackage` output and explicit LOD files match source bytes; `validateWorldPackage` is not called by the spec. +- [ ] AC-2 [local]: Each species-and-LOD pair is one `EXT_mesh_gpu_instancing` node; node count does not grow with tree count. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 12 tests pass; node count constant 1k→10k, one instancing node for a single-mesh species and one flattened node (four primitives) for a two-mesh species. +- [x] AC-3 [local]: A vanilla three `GLTFLoader` loads `world.glb` into `InstancedMesh` objects whose instance count equals the bake's placement count. proof: `CONSUMER_GAME=skip pnpm --filter strata-terrain-preview test:consumer` — Evidence: 2026-10-10 exit 0 in 23 s; the cooked forest GLB loads in the isolated packed install as `fir_instances` (4 primitives × 3 100), `fir-c_instances` (4 × 1 400) and `boulder_instances` (1 × 320), every child count equal to its manifest run count and 0 per-tree mesh copies. The unskipped run's scaffold `pnpm install` failed here (exit 1), so the PRD-468 game-handoff stage stays unverified. +- [x] AC-4 [local]: A placement mirrored in Z fails the cook with a named contact error. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 12 tests pass, including `Placement '' of species 'fir' contact check failed ... mirrored-Z`. +- [x] AC-5 [local]: The world package written next to it passes `validateWorldPackage` and lists a GLB and at least one LOD per species. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 the spec reads the cook's `world.json` and `placements.bin`, calls core `validateWorldPackage` with `placementsByteLength` (and `heightmapByteLength`) and gets `{ ok: true, errors: [] }`, and every species lists `models/.glb` plus a LOD whose bytes exist on disk. ## Integration Ledger @@ -82,17 +82,17 @@ flowchart LR - [x] Mirrored placements fail by name. proof: same spec — 2026-10-10: mirrored-Z case throws `Placement '' of species 'fir' contact check failed`. #### Phase 2: LODs and the world package -**Status:** PARTIAL +**Status:** done **Files:** `packages/assets/src/world/terrain-world.ts`, `packages/terrain/starter/forest/bake.mjs` **Implementation:** prefer authored `*-mid` / `*-lod1` GLBs; fall back to `modelPass({ simplify })`; fill `assets[].glb` and `lods` in `world.json`. -- [ ] World package validates and carries LODs per species. proof: `terrain-world.spec.ts` -- [ ] The forest kit bake emits both outputs. proof: `node packages/terrain/starter/forest/bake.mjs` then the spec's file check +- [x] World package validates and carries LODs per species. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — 2026-10-10 12 tests pass; `validateWorldPackage` returns `{ ok: true, errors: [] }` and each species lists a GLB plus a LOD. +- [x] The forest kit bake emits both outputs. proof: `node packages/terrain/starter/forest/bake.mjs` — 2026-10-10 exit 0 in 166 ms; writes `world.glb` (15 874 700 B) and the world package (`world.json`, `heightmap.u16`, `placements.bin`, `splat.rgba`, `terrain-table.json`, `models/{fir,boulder,fir-c}.glb` with LODs); 3 100 fir, 1 400 fir-c and 320 boulder placements. #### Phase 3: consumer load -**Status:** PARTIAL +**Status:** done **Files:** `examples/strata-terrain-preview/scripts/verify-consumer.mjs` **Implementation:** load the cooked `world.glb` with stock `GLTFLoader`; assert instance counts and that no mesh is a per-tree copy. -- [ ] Vanilla loader gets `InstancedMesh` with the bake's counts. proof: `pnpm --filter strata-terrain-preview test:consumer` +- [x] Vanilla loader gets `InstancedMesh` with the bake's counts. proof: `CONSUMER_GAME=skip pnpm --filter strata-terrain-preview test:consumer` — 2026-10-10 exit 0; the generated consumer script reads the cooked `world.json`, derives each species' logical count from its runs, and checks each child `InstancedMesh` count (fir 4×3 100, fir-c 4×1 400, boulder 1×320) with 0 per-tree copies and no hardcoded values. ## Current plan @@ -103,3 +103,7 @@ flowchart LR Initial review: six weak unit tests passed; `node --check examples/strata-terrain-preview/scripts/verify-consumer.mjs` failed at line 584, and assets typecheck failed with three errors. Cook also dropped model textures/transforms, omitted terrain without custom LOD data and zeroed splat bytes. That initial completion claim was rejected; corrected focused results follow. 2026-10-10 focused correction: the cook uses `bakeTerrain` and `bakeWorldPackage`, preserves source image bytes and every source-node world transform through glTF-transform copying/flattening, rejects skinned or animated species, and produces one near-detail instance node per species. The 11-test spec passes (including reused source-mesh nodes, 1 000/10 000 placement census and exact splat/placement bytes); assets typecheck and scoped Biome pass, with one nonfatal cognitive-complexity warning. Node is v20.19.6, so Node 22 and the forest-kit claim remain unverified. Authored sibling LOD discovery, multi-layer terrain PBR appearance, actual forest bake/consumer and runtime/visual proof remain pending. No full-PRD completion is claimed. + +2026-10-10 bounded integration: rebuilt `packages/assets` and `packages/terrain` so their `dist` matches source, then re-ran the forest kit. `packages/terrain/starter/forest/bake.mjs` now imports the public `cookTerrainWorld`, passes the real evaluated state (no `state.lods` injection), lets the cook own `world.json` and the species models, and writes only the surface table, its textures and `sky.hdr` beside them. The runner passed Node 20.19.6: `node --check` on both scripts, scoped Biome, assets typecheck, and `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` (12 tests). The forest bake wrote `world.glb` (15 874 700 B) with 3 100 fir, 1 400 fir-c and 320 boulder placements in 166 ms. `CONSUMER_GAME=skip pnpm --filter strata-terrain-preview test:consumer` passed in 23 s and loaded that GLB through stock `GLTFLoader` at the manifest's per-species counts with 0 per-tree copies. The unskipped consumer run failed at the PRD-468 scaffold `pnpm install` (exit 1) in this environment. Authored sibling LOD discovery, full multi-layer surface appearance, Node 22 and runtime/visual proof remain pending; AC-1 stays open. No full-PRD completion is claimed. + +2026-10-10 integration: the forest bake produced a 15 874 700-byte GLB and a valid world package (3 100 fir, 1 400 fir-c, 320 boulder). The scoped parser proof ran with CONSUMER_GAME=skip; the ordinary test:consumer command still fails during the separate PRD-468 scaffold install and is not game acceptance. A missing cooked forest fixture now fails closed. All-scene species/LOD census and full layered terrain appearance remain pending. diff --git a/examples/strata-terrain-preview/scripts/verify-consumer.mjs b/examples/strata-terrain-preview/scripts/verify-consumer.mjs index 477ce6af1..5d4654353 100644 --- a/examples/strata-terrain-preview/scripts/verify-consumer.mjs +++ b/examples/strata-terrain-preview/scripts/verify-consumer.mjs @@ -15,6 +15,7 @@ import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; import { copyFileSync, + existsSync, mkdirSync, mkdtempSync, readFileSync, @@ -486,6 +487,62 @@ try { join(consumer, "recipes.json"), JSON.stringify(Object.fromEntries(worldNames.map((name) => [name, recipes[name].toJSON()]))), ); + + // The forest starter's cooked world, copied into this isolated install as the fixture its + // vanilla-loader proof reads. A missing fixture fails instead of silently skipping the proof. + const forestWorld = join(repo, "packages/terrain/starter/forest/world"); + const hasForest = + existsSync(join(forestWorld, "world.glb")) && existsSync(join(forestWorld, "world.json")); + if (!hasForest) + throw new Error( + "Missing cooked forest fixture: run node packages/terrain/starter/forest/bake.mjs first", + ); + if (hasForest) { + copyFileSync(join(forestWorld, "world.glb"), join(consumer, "forest-world.glb")); + copyFileSync(join(forestWorld, "world.json"), join(consumer, "forest-world.json")); + } + + // PRD-583 AC-3: the cooked forest `world.glb` read by the stock three loader in this install, + // inside the same generated script. The loader makes one `InstancedMesh` per source primitive, + // so every child mesh's count is compared to the manifest's logical placement count for its + // species — never a sum of primitive copies — and no species may draw per-tree mesh copies. + const forestBlock = hasForest + ? ` + const manifest = JSON.parse(readFileSync("forest-world.json", "utf8")); + const logical = {}; + for (const cell of manifest.cells) + for (const run of cell.runs) logical[run.asset] = (logical[run.asset] ?? 0) + run.count; + const forestBytes = readFileSync("forest-world.glb"); + const forestScene = await new Promise((resolve, reject) => + new GLTFLoader().parse(forestBytes.buffer.slice(forestBytes.byteOffset, forestBytes.byteOffset + forestBytes.byteLength), "", resolve, reject)); + const bySpecies = {}; + for (const species of Object.keys(logical)) { + const node = forestScene.scene.getObjectByName(species + "_instances"); + if (node === undefined) throw new Error("forest GLB has no instancing node for species " + species); + let primitives = 0; + node.traverse((child) => { + if (!child.isInstancedMesh) return; + primitives++; + if (child.count !== logical[species]) + throw new Error("forest GLB species " + species + ": InstancedMesh count " + child.count + " != placement count " + logical[species]); + }); + if (primitives === 0) throw new Error("forest GLB species " + species + " has no InstancedMesh"); + bySpecies[species] = { placements: logical[species], primitives }; + } + let perTreeCopies = 0; + forestScene.scene.traverse((object) => { + if (object.isMesh && !object.isInstancedMesh && !object.name.startsWith("terrain_")) perTreeCopies++; + }); + if (perTreeCopies > 0) throw new Error("forest GLB draws " + perTreeCopies + " per-tree mesh copies"); + const forest = { + species: bySpecies, + speciesCount: Object.keys(bySpecies).length, + placements: Object.values(logical).reduce((sum, count) => sum + count, 0), + perTreeCopies, + glbBytes: forestBytes.length, + };` + : "\n const forest = null;"; + const author = ` import { createHash } from "node:crypto"; import { readFileSync, writeFileSync } from "node:fs"; @@ -493,6 +550,7 @@ try { import * as editor from "@threenative/terrain/editor"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import { Box3 } from "three"; + Object.assign(globalThis, { self: globalThis }); if (typeof editor.mountTerrainEditor !== "function" || typeof editor.TerrainEditorController !== "function") throw new Error("The packed editor entry does not export the editor"); const recipes = JSON.parse(readFileSync("recipes.json", "utf8")); @@ -530,11 +588,18 @@ try { glbBytes: bytes.length, evaluateMs, exportMs, }); } - console.log(JSON.stringify(report)); +${forestBlock} + console.log(JSON.stringify({ report, forest })); `; writeFileSync(join(consumer, "author.mjs"), author); - const authored = JSON.parse(run("node", ["author.mjs"], { cwd: consumer })); + const authoredResult = JSON.parse(run("node", ["author.mjs"], { cwd: consumer })); + const authored = authoredResult.report; assert.equal(authored.length, worldNames.length); + if (hasForest) { + assert(authoredResult.forest, "the forest GLB fixture was not checked"); + assert.equal(authoredResult.forest.perTreeCopies, 0); + assert(authoredResult.forest.placements > 0); + } for (const entry of authored) { const sha = (heights) => createHash("sha256").update(JSON.stringify(heights)).digest("hex"); assert.equal( @@ -566,6 +631,7 @@ try { optionalTooling: optional, gameHandoff, fullWorlds, + forestWorld: authoredResult.forest, worlds: authored.map((entry) => ({ world: entry.world, heights: entry.heights, diff --git a/packages/assets/__tests__/terrain-world.spec.ts b/packages/assets/__tests__/terrain-world.spec.ts index e415a08a6..b1335cb3a 100644 --- a/packages/assets/__tests__/terrain-world.spec.ts +++ b/packages/assets/__tests__/terrain-world.spec.ts @@ -9,6 +9,7 @@ import { PNG } from "pngjs"; import { type Group, InstancedMesh, Mesh } from "three"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { type IWorldPackage, validateWorldPackage } from "../../core/src/world-package.js"; import { cookTerrainWorld } from "../src/world/terrain-world.js"; // GLTFLoader reads the global `self` while it parses. Node has none. @@ -394,6 +395,37 @@ describe("cookTerrainWorld", () => { expect(error?.message).toContain("mirrored-Z"); }); + it("writes a world package core's validator accepts, with a GLB and a LOD per species", async () => { + const result = await cookTerrainWorld({ + state: evaluatedTerrain(), + species: { fir: { glb: await simpleSpecies() } }, + out: dir, + }); + + const manifest = JSON.parse( + await readFile(path.join(dir, "world.json"), "utf8"), + ) as IWorldPackage; + const placements = new Uint8Array(await readFile(path.join(dir, "placements.bin"))); + const heightmap = new Uint8Array(await readFile(path.join(dir, "heightmap.u16"))); + const validation = validateWorldPackage(manifest, { + placementsByteLength: placements.byteLength, + heightmapByteLength: heightmap.byteLength, + }); + expect(validation.errors).toEqual([]); + expect(validation.ok).toBe(true); + + // Every species lists a model, at least one LOD, and the bytes the manifest names exist. + for (const [id, asset] of Object.entries(manifest.assets)) { + expect(asset.glb).toBe(`models/${id}.glb`); + expect(asset.lods?.length ?? 0).toBeGreaterThanOrEqual(1); + } + for (const asset of Object.values(result.manifest.assets)) { + expect((await stat(path.join(dir, asset.glb))).size).toBeGreaterThan(0); + for (const lod of asset.lods ?? []) + expect((await stat(path.join(dir, lod.glb))).size).toBeGreaterThan(0); + } + }); + it("keeps the node count constant from 1 000 to 10 000 placements", async () => { const base = evaluatedTerrain(); const withPlacements = (count: number): ITerrainState => ({ diff --git a/packages/terrain/starter/forest/bake.mjs b/packages/terrain/starter/forest/bake.mjs index 789c2a862..313a5061e 100644 --- a/packages/terrain/starter/forest/bake.mjs +++ b/packages/terrain/starter/forest/bake.mjs @@ -2,10 +2,12 @@ // project, then serve the folder it writes: // node src/terrain/forest/bake.mjs [--assets ] [--out ] // The engine's `WorldCells` streams `world/world.json` and `loadTerrainSplat` textures its ground; -// the game never evaluates terrain at runtime. +// the game never evaluates terrain at runtime. The cook in `@threenative/assets` writes the +// instanced `world.glb` and the species models the manifest names. import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { cookTerrainWorld } from "@threenative/assets"; import { Terrain, applyPlacementOverrides, bakeWorldPackage } from "@threenative/terrain"; const here = dirname(fileURLToPath(import.meta.url)); @@ -31,40 +33,51 @@ const state = applyPlacementOverrides( recipe.placementOverrides ?? {}, ); -const assets = Object.fromEntries( +const species = Object.fromEntries( Object.entries(assetTable).map(([id, spec]) => [ id, { + glb: join(assetsDir, spec.near), bounds: spec.bounds, - glb: `models/${spec.near.split("/").at(-1)}`, - ...(spec.mid - ? { lods: [{ distance: spec.midDistance, glb: `models/${spec.mid.split("/").at(-1)}` }] } - : {}), + lods: spec.mid ? [{ distance: spec.midDistance ?? 40, glb: join(assetsDir, spec.mid) }] : [], maxDistance: spec.maxDistance, }, ]), ); -const started = performance.now(); -const { manifest, splat, files } = bakeWorldPackage(state, { - assets, - layers: { table: "terrain-table.json" }, -}); -const written = new Map(); - async function write(relative, bytes) { await mkdir(join(outDir, dirname(relative)), { recursive: true }); await writeFile(join(outDir, relative), bytes); - written.set(relative, bytes.length ?? bytes.byteLength); } async function copy(from, to) { await mkdir(join(outDir, dirname(to)), { recursive: true }); await copyFile(join(assetsDir, from), join(outDir, to)); - written.set(to, (await readFile(join(outDir, to))).byteLength); } -await write("world.json", `${JSON.stringify(manifest, null, 2)}\n`); +const started = performance.now(); +// The cook writes `world.json`, `world.glb`, the heightmap, placements and splat, and every +// species model under `models/.glb`, all from the real evaluated state. +const cooked = await cookTerrainWorld({ + state, + species, + // The cook embeds the ground's base albedo and normal in `world.glb`; the full multilayer + // surface appearance is still to come, so only the base layer is handed over here. + materials: { + layers: [ + { + id: surface.base.id, + diff: join(assetsDir, surface.sources[surface.base.id].diff), + nrm: join(assetsDir, surface.sources[surface.base.id].nrm), + diffMimeType: "image/jpeg", + nrmMimeType: "image/jpeg", + }, + ], + }, + out: outDir, + extraLayers: { table: "terrain-table.json" }, +}); + // The lake and river the generator evaluated, so the game draws the water the bake shaped. const round = (value) => Math.round(value * 1_000) / 1_000; await write( @@ -87,32 +100,28 @@ await write( 2, )}\n`, ); -for (const [name, bytes] of Object.entries(files)) await write(name, bytes); -for (const spec of Object.values(assetTable)) { - await copy(spec.near, `models/${spec.near.split("/").at(-1)}`); - if (spec.mid) await copy(spec.mid, `models/${spec.mid.split("/").at(-1)}`); -} -// The sky the props are lit by (CC0 Poly Haven Kloofendal); world.ts gives it to their materials. -await copy("hdri/kloofendal_48d_partly_cloudy_1k.hdr", "sky.hdr"); -for (const layer of [surface.base, ...surface.layers]) { - const source = surface.sources[layer.id]; - await copy(source.diff, `${surface.textures}/${layer.id}_diff.jpg`); - await copy(source.nrm, `${surface.textures}/${layer.id}_nrm.jpg`); -} + +// The cook owns the manifest and the models; only the surface table and its images are left. +// `bakeWorldPackage` is the one public source of the splat mask layout the table needs, and its +// asset map is the cooked manifest's, so no stale basename reaches the table. +const { splat } = bakeWorldPackage(state, { assets: cooked.manifest.assets }); // `sources` is the bake's own lookup: the runtime reads only base/layers/splat/textures. await write( "terrain-table.json", `${JSON.stringify({ ...surface, splat, textures: "textures" }, null, 2)}\n`, ); +for (const layer of [surface.base, ...surface.layers]) { + const source = surface.sources[layer.id]; + await copy(source.diff, `${surface.textures}/${layer.id}_diff.jpg`); + await copy(source.nrm, `${surface.textures}/${layer.id}_nrm.jpg`); +} +// The sky the props are lit by (CC0 Poly Haven Kloofendal); world.ts gives it to their materials. +await copy("hdri/kloofendal_48d_partly_cloudy_1k.hdr", "sky.hdr"); -const perAsset = {}; -for (const cell of manifest.cells) - for (const run of cell.runs) perAsset[run.asset] = (perAsset[run.asset] ?? 0) + run.count; -const total = [...written.values()].reduce((sum, bytes) => sum + bytes, 0); console.log( - `${Object.entries(perAsset) + `${Object.entries(cooked.placementCounts) .map(([asset, count]) => `${asset} ${String(count)}`) - .join(", ")} placements; ${[...written] - .map(([name, bytes]) => `${name} ${String(bytes)} B`) - .join(", ")}; ${String(total)} B total; ${Math.round(performance.now() - started)} ms`, + .join( + ", ", + )} placements; world.glb ${String(cooked.worldGlb.length)} B; ${Math.round(performance.now() - started)} ms`, ); From 95fc18ccebac8212d69b4989b308cb38fe9e6117 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 14:01:09 -0700 Subject: [PATCH 014/210] fix(assets): preserve terrain species LODs and validate baked contact --- ...-bakes-headlessly-into-an-instanced-glb.md | 6 +- .../assets/__tests__/terrain-world.spec.ts | 231 ++++++++++- packages/assets/src/world/terrain-world.ts | 387 ++++++++++++------ 3 files changed, 482 insertions(+), 142 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md index c56875a8b..6c0841000 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md @@ -59,7 +59,7 @@ flowchart LR ## Acceptance Criteria - [ ] AC-1 [local]: `cookTerrainWorld` runs under plain Node 22 with no DOM globals and writes `world.glb` for the forest kit. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 focused spec 7/10 passed; the cook writes `world.glb` for the evaluated-terrain fixture under Node. The forest-kit bake is not run, so this stays open. -- [ ] AC-2 [local]: Each species-and-LOD pair is one `EXT_mesh_gpu_instancing` node; node count does not grow with tree count. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 12 tests pass; node count constant 1k→10k, one instancing node for a single-mesh species and one flattened node (four primitives) for a two-mesh species. +- [x] AC-2 [local]: Each species-and-LOD pair is one `EXT_mesh_gpu_instancing` node; node count does not grow with tree count. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 17 tests pass; the default `TerrainWorld` scene holds near nodes only and each species-and-LOD pair is one node in its own named scene (`fir_lod1`, `boulder_lod1`), every node carrying all logical placements; the all-scene census is constant 1 000→10 000, one instancing node for a single-mesh species and one flattened node (four primitives) for a two-mesh species. `instancedMeshCount` stays the default-scene count for compatibility; the all-scene test walk is authoritative and `nodeCount` counts every scene. - [x] AC-3 [local]: A vanilla three `GLTFLoader` loads `world.glb` into `InstancedMesh` objects whose instance count equals the bake's placement count. proof: `CONSUMER_GAME=skip pnpm --filter strata-terrain-preview test:consumer` — Evidence: 2026-10-10 exit 0 in 23 s; the cooked forest GLB loads in the isolated packed install as `fir_instances` (4 primitives × 3 100), `fir-c_instances` (4 × 1 400) and `boulder_instances` (1 × 320), every child count equal to its manifest run count and 0 per-tree mesh copies. The unskipped run's scaffold `pnpm install` failed here (exit 1), so the PRD-468 game-handoff stage stays unverified. - [x] AC-4 [local]: A placement mirrored in Z fails the cook with a named contact error. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 12 tests pass, including `Placement '' of species 'fir' contact check failed ... mirrored-Z`. - [x] AC-5 [local]: The world package written next to it passes `validateWorldPackage` and lists a GLB and at least one LOD per species. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 the spec reads the cook's `world.json` and `placements.bin`, calls core `validateWorldPackage` with `placementsByteLength` (and `heightmapByteLength`) and gets `{ ok: true, errors: [] }`, and every species lists `models/.glb` plus a LOD whose bytes exist on disk. @@ -69,7 +69,7 @@ flowchart LR | Capability | Reachable consumer/trigger | Replaces / disposition | Evidence | |---|---|---|---| | Headless world GLB | `cookTerrainWorld` from `@threenative/assets`; kit `bake.mjs`; PRD-584 MCP tool | Browser `exportWorldGLB` stays for the editor until PRD-588 | AC-1, AC-3 (unverified) | -| Instanced scatter in GLB | Any `GLTFLoader` consumer | Per-tree mesh nodes | AC-2 (unverified) | +| Instanced scatter in GLB | Any `GLTFLoader` consumer | Per-tree mesh nodes | AC-2 | ## Execution Phases @@ -107,3 +107,5 @@ Initial review: six weak unit tests passed; `node --check examples/strata-terrai 2026-10-10 bounded integration: rebuilt `packages/assets` and `packages/terrain` so their `dist` matches source, then re-ran the forest kit. `packages/terrain/starter/forest/bake.mjs` now imports the public `cookTerrainWorld`, passes the real evaluated state (no `state.lods` injection), lets the cook own `world.json` and the species models, and writes only the surface table, its textures and `sky.hdr` beside them. The runner passed Node 20.19.6: `node --check` on both scripts, scoped Biome, assets typecheck, and `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` (12 tests). The forest bake wrote `world.glb` (15 874 700 B) with 3 100 fir, 1 400 fir-c and 320 boulder placements in 166 ms. `CONSUMER_GAME=skip pnpm --filter strata-terrain-preview test:consumer` passed in 23 s and loaded that GLB through stock `GLTFLoader` at the manifest's per-species counts with 0 per-tree copies. The unskipped consumer run failed at the PRD-468 scaffold `pnpm install` (exit 1) in this environment. Authored sibling LOD discovery, full multi-layer surface appearance, Node 22 and runtime/visual proof remain pending; AC-1 stays open. No full-PRD completion is claimed. 2026-10-10 integration: the forest bake produced a 15 874 700-byte GLB and a valid world package (3 100 fir, 1 400 fir-c, 320 boulder). The scoped parser proof ran with CONSUMER_GAME=skip; the ordinary test:consumer command still fails during the separate PRD-468 scaffold install and is not game acceptance. A missing cooked forest fixture now fails closed. All-scene species/LOD census and full layered terrain appearance remain pending. + +2026-10-10 cook follow-up: 17 focused tests pass (336 ms), assets typecheck passes and scoped Biome passes with one complexity warning. Alternate LODs live in named scenes while the default scene renders near detail once; every species/LOD pair has one instancing node. Empty species fail before output. Contact samples the baked uint16 heightmap with one quantization-step tolerance plus float rounding. Node 22.23.3 was resolved by an isolated npm launcher, but its forest cook failed because the standalone kit could not resolve @threenative/assets; AC-1 stays open until the kit/tarball dependency closure is wired. Multi-layer material appearance and runtime/visual proof remain pending. diff --git a/packages/assets/__tests__/terrain-world.spec.ts b/packages/assets/__tests__/terrain-world.spec.ts index b1335cb3a..de06fd146 100644 --- a/packages/assets/__tests__/terrain-world.spec.ts +++ b/packages/assets/__tests__/terrain-world.spec.ts @@ -1,10 +1,16 @@ import assert from "node:assert/strict"; -import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; +import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { Document, type Buffer as GltfBuffer, type Material, NodeIO } from "@gltf-transform/core"; import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; -import { type ITerrainState, Terrain, bakeTerrain, bakeWorldPackage } from "@threenative/terrain"; +import { + type ITerrainState, + Terrain, + bakeTerrain, + bakeWorldPackage, + sampleHeight, +} from "@threenative/terrain"; import { PNG } from "pngjs"; import { type Group, InstancedMesh, Mesh } from "three"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; @@ -16,10 +22,12 @@ import { cookTerrainWorld } from "../src/world/terrain-world.js"; Object.assign(globalThis, { self: globalThis }); /** A real evaluated terrain. The fixed seed fixes every scatter position. */ -function evaluatedTerrain(): ITerrainState { - const state = new Terrain({ resolution: 33, seed: 73, size: 256 }) - .scatter({ id: "firs", asset: "fir", count: 40, minDistance: 3 }) - .evaluate(); +function evaluatedTerrain(assets: readonly string[] = ["fir"]): ITerrainState { + let terrain = new Terrain({ resolution: 33, seed: 73, size: 256 }); + for (const asset of assets) { + terrain = terrain.scatter({ id: `${asset}s`, asset, count: 40, minDistance: 3 }); + } + const state = terrain.evaluate(); if (state.instances.length === 0) throw new Error("fixture terrain has no placements"); return state; } @@ -124,6 +132,22 @@ async function skinnedSpecies(): Promise { return io().writeBinary(doc); } +/** A species with a scene but no mesh at all. The cook must refuse it by name. */ +async function noMeshSpecies(): Promise { + const doc = new Document(); + const scene = doc.createScene("Scene"); + scene.addChild(doc.createNode("prop")); + return io().writeBinary(doc); +} + +/** A species whose one mesh has no primitive, so it draws nothing. The cook must refuse it. */ +async function emptyMeshSpecies(): Promise { + const doc = new Document(); + const scene = doc.createScene("Scene"); + scene.addChild(doc.createNode("emptyNode").setMesh(doc.createMesh("empty"))); + return io().writeBinary(doc); +} + /** Parses a GLB with the vanilla three loader, the same reader a game uses. */ function loadWorld(glb: Uint8Array): Promise { return new Promise((resolve, reject) => { @@ -131,6 +155,27 @@ function loadWorld(glb: Uint8Array): Promise { }); } +/** Every scene of a cooked GLB, with the default scene and each scene's instancing nodes. */ +function loadScenes(glb: Uint8Array): Promise<{ scene: Group; scenes: Group[] }> { + return new Promise((resolve, reject) => { + new GLTFLoader().parse( + glb.slice().buffer, + "", + (gltf) => resolve({ scene: gltf.scene, scenes: gltf.scenes as Group[] }), + reject, + ); + }); +} + +/** The instancing nodes directly in a loaded scene, each with its instance count. */ +function instancedNodes(scene: Group): { name: string; instances: number }[] { + const found: { name: string; instances: number }[] = []; + scene.traverse((object) => { + if (object instanceof InstancedMesh) found.push({ name: object.name, instances: object.count }); + }); + return found.sort((a, b) => a.name.localeCompare(b.name)); +} + describe("cookTerrainWorld", () => { let dir: string; @@ -364,6 +409,54 @@ describe("cookTerrainWorld", () => { await expect(stat(path.join(dir, "world.glb"))).rejects.toThrow(); }); + it("refuses a meshless or empty-draw species by name before writing", async () => { + // Positive placement counts and zero source bounds must not hide a species with nothing to draw. + for (const glb of [await noMeshSpecies(), await emptyMeshSpecies()]) { + await expect( + cookTerrainWorld({ state: evaluatedTerrain(), species: { fir: { glb } }, out: dir }), + ).rejects.toThrow(/species 'fir' .*empty draw/); + } + await expect(stat(path.join(dir, "world.glb"))).rejects.toThrow(); + }); + + it("refuses an authored LOD that declares a skin, same as the near model", async () => { + await expect( + cookTerrainWorld({ + state: evaluatedTerrain(), + species: { + fir: { + glb: await simpleSpecies(), + lods: [{ distance: 40, glb: await skinnedSpecies() }], + }, + }, + out: dir, + }), + ).rejects.toThrow(/species 'fir' .*skin/); + await expect(stat(path.join(dir, "world.glb"))).rejects.toThrow(); + }); + + it("discovers an authored -mid sibling before falling back to simplification", async () => { + const nearPath = path.join(dir, "pine.glb"); + const midPath = path.join(dir, "pine-mid.glb"); + const near = await simpleSpecies(); + const mid = await twoMeshSpecies(); + await writeFile(nearPath, near); + await writeFile(midPath, mid.glb); + + const result = await cookTerrainWorld({ + state: evaluatedTerrain(), + species: { fir: { glb: nearPath } }, + out: dir, + }); + + expect(result.manifest.assets.fir?.lods).toHaveLength(1); + expect(result.manifest.assets.fir?.lods?.[0]?.glb).toBe("models/fir_lod1.glb"); + // The written level is the sibling's bytes, not a simplification of the near model. + expect(new Uint8Array(await readFile(path.join(dir, "models", "fir_lod1.glb")))).toEqual( + new Uint8Array(mid.glb), + ); + }); + it("names a placement that is mirrored in Z as a contact failure", async () => { const state = evaluatedTerrain(); const spacing = state.size / (state.resolution - 1); @@ -395,6 +488,78 @@ describe("cookTerrainWorld", () => { expect(error?.message).toContain("mirrored-Z"); }); + it("checks ground contact against the quantized heightmap, not the horizontal spacing", async () => { + const state = evaluatedTerrain(); + // A real relief, so one uint16 height step is a meaningful, non-zero tolerance. + const spacing = state.size / (state.resolution - 1); + state.height.set( + Array.from(state.height, (_, i) => Math.floor(i / state.resolution) * spacing), + ); + const species = { fir: { glb: await simpleSpecies() } }; + let min = Number.POSITIVE_INFINITY; + let max = Number.NEGATIVE_INFINITY; + for (const height of state.height) { + min = Math.min(min, height); + max = Math.max(max, height); + } + // One uint16 step of the heightmap range the bake writes. + const quantStep = (max - min) / 65_535; + const first = state.instances[0]; + assert.ok(first); + const groundY = sampleHeight(state, first.position[0], first.position[2]); + const at = (delta: number): [number, number, number] => [ + first.position[0], + groundY + delta, + first.position[2], + ]; + const posed = (delta: number, grounding: boolean): ITerrainState => ({ + ...state, + instances: [ + { + ...first, + transform: { + position: at(delta), + quaternion: [0, 0, 0, 1], + scale: [1, 1, 1], + grounding, + }, + }, + ], + }); + + // A sub-texel hop passes: below one uint16 height step. + await expect( + cookTerrainWorld({ + state: posed(quantStep * 0.25, true), + species, + out: path.join(dir, "sub"), + }), + ).resolves.toBeDefined(); + + // A real offset above one height step fails by name, with no output written. + const error = await cookTerrainWorld({ + state: posed(quantStep * 4, true), + species, + out: path.join(dir, "lift"), + }).then( + () => null, + (caught: Error) => caught, + ); + expect(error?.message).toContain( + `Placement '${first.id}' of species 'fir' contact check failed`, + ); + await expect(stat(path.join(dir, "lift", "world.glb"))).rejects.toThrow(); + + // The authored grounding override is honored: the same lifted pose is skipped. + await expect( + cookTerrainWorld({ + state: posed(quantStep * 4, false), + species, + out: path.join(dir, "override"), + }), + ).resolves.toBeDefined(); + }); + it("writes a world package core's validator accepts, with a GLB and a LOD per species", async () => { const result = await cookTerrainWorld({ state: evaluatedTerrain(), @@ -426,7 +591,7 @@ describe("cookTerrainWorld", () => { } }); - it("keeps the node count constant from 1 000 to 10 000 placements", async () => { + it("keeps the node count and all-scene census constant from 1 000 to 10 000 placements", async () => { const base = evaluatedTerrain(); const withPlacements = (count: number): ITerrainState => ({ ...base, @@ -453,5 +618,57 @@ describe("cookTerrainWorld", () => { expect(small.instancedMeshCount).toBe(1); expect(large.instancedMeshCount).toBe(1); expect(large.placementCounts.fir).toBe(10_000); + + // The authoritative walk: the default scene plus the generated alternate-LOD scene. + const census = await loadScenes(large.worldGlb); + expect(census.scenes.map((scene) => scene.name)).toEqual(["TerrainWorld", "fir_lod1"]); + const allNodes = census.scenes.flatMap((scene) => instancedNodes(scene)); + expect(allNodes).toEqual([ + { name: "fir_instances", instances: 10_000 }, + { name: "fir_lod1_instances", instances: 10_000 }, + ]); + // The default scene carries the near LOD only. + expect(instancedNodes(census.scene)).toEqual([{ name: "fir_instances", instances: 10_000 }]); + }); + + it("holds near nodes in the default scene and one node per species-and-LOD pair in named scenes", async () => { + const state = evaluatedTerrain(["fir", "boulder"]); + const near = await simpleSpecies(); + const authoredLod = await simpleSpecies(); + const result = await cookTerrainWorld({ + state, + species: { + fir: { glb: near }, // generated LOD + boulder: { glb: near, lods: [{ distance: 45, glb: authoredLod }] }, // authored LOD + }, + out: dir, + }); + + const counts: Record = { + fir: state.instances.filter((i) => i.asset === "fir").length, + boulder: state.instances.filter((i) => i.asset === "boulder").length, + }; + + const census = await loadScenes(result.worldGlb); + // One default scene plus one named scene per alternate LOD. + expect(census.scenes.map((scene) => scene.name)).toEqual([ + "TerrainWorld", + "fir_lod1", + "boulder_lod1", + ]); + // Default scene: only the near nodes, each rendering every placement once. + expect(instancedNodes(census.scene)).toEqual([ + { name: "boulder_instances", instances: counts.boulder }, + { name: "fir_instances", instances: counts.fir }, + ]); + // Every species-and-LOD pair is exactly one node, with all logical placements. + expect(instancedNodes(census.scenes[1] as Group)).toEqual([ + { name: "fir_lod1_instances", instances: counts.fir }, + ]); + expect(instancedNodes(census.scenes[2] as Group)).toEqual([ + { name: "boulder_lod1_instances", instances: counts.boulder }, + ]); + // The compatibility stat counts the default scene only; the all-scene walk is authoritative. + expect(result.instancedMeshCount).toBe(2); }); }); diff --git a/packages/assets/src/world/terrain-world.ts b/packages/assets/src/world/terrain-world.ts index c9e9fe259..82e7e1482 100644 --- a/packages/assets/src/world/terrain-world.ts +++ b/packages/assets/src/world/terrain-world.ts @@ -1,6 +1,13 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; import path from "node:path"; -import { Document, type Mesh, type Node, NodeIO } from "@gltf-transform/core"; +import { + Document, + type Buffer as GltfBuffer, + type Mesh, + type Node, + NodeIO, + type Scene, +} from "@gltf-transform/core"; import { ALL_EXTENSIONS, EXTMeshGPUInstancing } from "@gltf-transform/extensions"; import { copyToDocument, @@ -10,7 +17,7 @@ import { transformMesh, unpartition, } from "@gltf-transform/functions"; -import { bakeTerrain, bakeWorldPackage, sampleHeight } from "@threenative/terrain"; +import { bakeTerrain, bakeWorldPackage, decodeRAW16, sampleHeight } from "@threenative/terrain"; import type { IPlacement, ITerrainState } from "@threenative/terrain"; import { createGltfReader, readGltfDocument } from "../gltf-io.js"; import { modelPass } from "../passes/model.js"; @@ -61,7 +68,13 @@ export interface ICookTerrainWorldResult { readonly worldGlb: Uint8Array; readonly manifest: IWorldPackageManifest; readonly placementCounts: Readonly>; + /** Every node in the document, across the default and every alternate-LOD scene. */ readonly nodeCount: number; + /** + * Instancing nodes in the default `TerrainWorld` scene only, so an existing consumer keeps + * reading one node per species. Alternate LODs live in their own named scenes and are not + * counted here; walk `getRoot().listScenes()` for the authoritative all-scene census. + */ readonly instancedMeshCount: number; } @@ -115,6 +128,137 @@ function sourceBounds(speciesDoc: Document): { return { min: [0, 0, 0], max: [0, 0, 0] }; } +/** The mesh-bearing nodes under a species' default scene, in traversal order. */ +function meshNodesOf(speciesDoc: Document): { node: Node; mesh: Mesh }[] { + const root = speciesDoc.getRoot(); + const scene = root.getDefaultScene() ?? root.listScenes()[0] ?? null; + const meshNodes: { node: Node; mesh: Mesh }[] = []; + scene?.traverse((node: Node) => { + const mesh = node.getMesh(); + if (mesh) meshNodes.push({ node, mesh }); + }); + return meshNodes; +} + +/** + * Refuse a species that cannot instance, by name, before a byte is written: a deformer, an + * animation replay, or an actually-empty draw (no mesh, no primitive, or no positioned vertices). + * A positive placement count and zero source bounds must not hide a species with nothing to draw. + */ +function assertInstancable(speciesId: string, speciesDoc: Document): void { + if (speciesDoc.getRoot().listSkins().length > 0) { + throw new Error( + `cookTerrainWorld: species '${speciesId}' declares a skin; only static species can be instanced.`, + ); + } + if (speciesDoc.getRoot().listAnimations().length > 0) { + throw new Error( + `cookTerrainWorld: species '${speciesId}' declares an animation; only static species can be instanced.`, + ); + } + const meshNodes = meshNodesOf(speciesDoc); + const primitives = meshNodes.flatMap(({ mesh }) => mesh.listPrimitives()); + const positioned = primitives.some((primitive) => { + const positions = primitive.getAttribute("POSITION")?.getArray(); + return positions !== undefined && positions !== null && positions.length > 0; + }); + if (meshNodes.length === 0 || primitives.length === 0 || !positioned) { + throw new Error( + `cookTerrainWorld: species '${speciesId}' has no instancable mesh geometry (empty draw); a static species must ship a positioned primitive.`, + ); + } +} + +/** Parse one species or LOD GLB. The same bytes are parsed exactly once per level. */ +async function parseSpecies(bytes: Uint8Array): Promise { + const reader = await createGltfReader(Buffer.from(bytes)); + return readGltfDocument(reader, Buffer.from(bytes)); +} + +/** + * Authored sibling LODs beside a path species: `-mid.glb`, else `-lod1.glb`, where + * `` drops one trailing detail marker (`-near`, `-lod1`, `-lod2`, …) from the file name. + * Returns nothing when the caller authored `lods` explicitly or no sibling exists; the caller + * then falls back to `modelPass({ simplify })`. + */ +async function authoredSiblingLods(glbPath: string): Promise<{ distance: number; glb: string }[]> { + const dir = path.dirname(glbPath); + const base = path.basename(glbPath).replace(/\.glb$/i, ""); + const root = base.replace(/-(near|hi|lod\d+)$/i, ""); + for (const candidate of [`${root}-mid.glb`, `${root}-lod1.glb`]) { + const file = path.join(dir, candidate); + if (file === glbPath) continue; + try { + await stat(file); + return [{ distance: 40, glb: file }]; + } catch { + // Absent sibling: try the next name. + } + } + return []; +} + +/** + * One `EXT_mesh_gpu_instancing` node for one species-and-LOD pair, holding every logical + * placement. Source meshes, materials, textures and each source node's world transform are + * preserved by the fresh-copy-per-node path: one resolver per node keeps shared geometry private. + */ +function addInstancedSpeciesNode( + doc: Document, + buffer: GltfBuffer, + speciesDoc: Document, + items: IPlacement[], + targetScene: Scene, + instancingExt: EXTMeshGPUInstancing, + nodeName: string, +): void { + const count = items.length; + const translations = new Float32Array(count * 3); + const rotations = new Float32Array(count * 4); + const scales = new Float32Array(count * 3); + for (const [i, item] of items.entries()) { + const pose = poseOf(item); + translations.set(pose.position, i * 3); + rotations.set(pose.quaternion, i * 4); + scales.set(pose.scale, i * 3); + } + + const batch = instancingExt + .createInstancedMesh() + .setAttribute( + "TRANSLATION", + doc.createAccessor().setType("VEC3").setArray(translations).setBuffer(buffer), + ) + .setAttribute( + "ROTATION", + doc.createAccessor().setType("VEC4").setArray(rotations).setBuffer(buffer), + ) + .setAttribute("SCALE", doc.createAccessor().setType("VEC3").setArray(scales).setBuffer(buffer)); + + // Any extension the source uses must exist on the target document before its properties copy. + type ExtensionCtor = Parameters[0]; + for (const sourceExtension of speciesDoc.getRoot().listExtensionsUsed()) { + doc.createExtension(sourceExtension.constructor as unknown as ExtensionCtor); + } + + const combined = doc.createMesh(`${nodeName}_mesh`); + for (const { node, mesh } of meshNodesOf(speciesDoc)) { + const resolve = createDefaultPropertyResolver(doc, speciesDoc); + const targetMesh = copyToDocument(doc, speciesDoc, [mesh], resolve).get(mesh) as Mesh; + transformMesh(targetMesh, node.getWorldMatrix()); + for (const primitive of targetMesh.listPrimitives()) { + targetMesh.removePrimitive(primitive); + combined.addPrimitive(primitive); + } + } + + const instancedNode = doc + .createNode(nodeName) + .setMesh(combined) + .setExtension("EXT_mesh_gpu_instancing", batch); + targetScene.addChild(instancedNode); +} + export async function cookTerrainWorld( options: ICookTerrainWorldOptions, ): Promise { @@ -144,68 +288,46 @@ export async function cookTerrainWorld( } } - // Contact check: a grounded placement must meet the sampled terrain surface, within the - // quantisation the uint16 heightmap costs. A mirrored-Z placement reads the wrong row and misses. - const spacing = state.size / (state.resolution - 1); - let heightMin = Number.POSITIVE_INFINITY; - let heightMax = Number.NEGATIVE_INFINITY; - for (const height of state.height) { - if (height < heightMin) heightMin = height; - if (height > heightMax) heightMax = height; - } - const range = heightMax - heightMin; - const tolerance = spacing + (Number.isFinite(range) ? range / 65_535 : 0); - for (const item of state.instances) { - const pose = poseOf(item); - if (!pose.grounding) continue; - const groundY = sampleHeight(state, pose.position[0], pose.position[2]); - const difference = Math.abs(pose.position[1] - groundY); - if (Number.isFinite(difference) && difference > tolerance) { - throw new Error( - `Placement '${item.id}' of species '${item.asset}' contact check failed: base Y=${pose.position[1]} misses terrain ground Y=${groundY} by ${difference.toFixed(3)}m (> tolerance ${tolerance.toFixed(3)}m). Check for mirrored-Z or invalid coordinates.`, - ); - } - } - // Build every species' manifest entry (and its LOD bytes) before the shared bake. Each species - // model is parsed exactly once here: the same document later flattens into its instancing node. + // model and each of its LODs is parsed exactly once here: the same documents later flatten into + // their instancing nodes, and every level is refused by name if it cannot instance. const assets: Record = {}; const speciesFiles: { speciesId: string; lod0: Uint8Array; - speciesDoc: Document; - lods: { distance: number; glb: Uint8Array; filename: string }[]; + nearDoc: Document; + lods: { distance: number; glb: Uint8Array; filename: string; doc: Document }[]; }[] = []; for (const [speciesId, spec] of Object.entries(species)) { const lod0 = await resolveBytes(spec.glb); - - // Parse once, and refuse anything that deforms or replays before a single byte is written. - const reader = await createGltfReader(Buffer.from(lod0)); - const speciesDoc = await readGltfDocument(reader, Buffer.from(lod0)); - if (speciesDoc.getRoot().listSkins().length > 0) { - throw new Error( - `cookTerrainWorld: species '${speciesId}' declares a skin; only static species can be instanced.`, - ); - } - if (speciesDoc.getRoot().listAnimations().length > 0) { - throw new Error( - `cookTerrainWorld: species '${speciesId}' declares an animation; only static species can be instanced.`, - ); + const nearDoc = await parseSpecies(lod0); + assertInstancable(speciesId, nearDoc); + + // Authored LODs first: explicit `spec.lods`, then a sibling `-mid`/`-lod1` file beside a path + // species. Only when neither exists does simplification invent a level. + const authored = + spec.lods && spec.lods.length > 0 + ? spec.lods + : typeof spec.glb === "string" + ? await authoredSiblingLods(spec.glb) + : []; + + const lods: { distance: number; glb: Uint8Array; filename: string; doc: Document }[] = []; + for (const [i, level] of authored.entries()) { + if (level.glb === undefined) continue; + const bytes = await resolveBytes(level.glb); + const doc = await parseSpecies(bytes); + assertInstancable(speciesId, doc); + lods.push({ + distance: level.distance, + glb: bytes, + filename: `${speciesId}_lod${i + 1}.glb`, + doc, + }); } - const lods: { distance: number; glb: Uint8Array; filename: string }[] = []; - if (spec.lods && spec.lods.length > 0) { - for (const [i, level] of spec.lods.entries()) { - if (level.glb === undefined) continue; - const bytes = await resolveBytes(level.glb); - lods.push({ - distance: level.distance, - glb: bytes, - filename: `${speciesId}_lod${i + 1}.glb`, - }); - } - } else { + if (lods.length === 0) { const pass = modelPass({ simplify: { ratio: 0.5 }, textures: "none", @@ -213,14 +335,16 @@ export async function cookTerrainWorld( }); const simplified = await pass.apply(Buffer.from(lod0), `${speciesId}_lod1.glb`); const bytes = Uint8Array.from(Buffer.isBuffer(simplified) ? simplified : simplified.buffer); - lods.push({ distance: 40, glb: bytes, filename: `${speciesId}_lod1.glb` }); + const doc = await parseSpecies(bytes); + assertInstancable(speciesId, doc); + lods.push({ distance: 40, glb: bytes, filename: `${speciesId}_lod1.glb`, doc }); } - speciesFiles.push({ speciesId, lod0, speciesDoc, lods }); + speciesFiles.push({ speciesId, lod0, nearDoc, lods }); assets[speciesId] = { glb: `models/${speciesId}.glb`, - bounds: spec.bounds ?? sourceBounds(speciesDoc), + bounds: spec.bounds ?? sourceBounds(nearDoc), lods: lods.map((level) => ({ distance: level.distance, glb: `models/${level.filename}` })), ...(spec.maxDistance === undefined ? {} : { maxDistance: spec.maxDistance }), }; @@ -234,7 +358,44 @@ export async function cookTerrainWorld( ...(options.extraLayers === undefined ? {} : { layers: options.extraLayers }), }); - // Setup glTF document + // Contact check against the bytes the bake actually wrote, not the float grid: decode the + // quantized uint16 heightmap back to metres and sample that. Tolerance is one uint16 step of the + // written range plus float32 rounding — a sub-texel hop passes; a real offset above one step, + // and a mirrored-Z placement that reads the wrong row, both fail. `grounding: false` is an + // authored override and is skipped honestly. + const heightmapBytes = files[manifest.terrain.heightmap]; + if (heightmapBytes === undefined) { + throw new Error("cookTerrainWorld: the world package wrote no heightmap to sample."); + } + const decoded = decodeRAW16(heightmapBytes, { + width: manifest.terrain.columns, + height: manifest.terrain.rows, + min: manifest.terrain.heightMin, + max: manifest.terrain.heightMax, + }); + const decodedGrid = { + height: decoded.values, + resolution: manifest.terrain.columns, + size: state.size, + }; + const terrainRange = manifest.terrain.heightMax - manifest.terrain.heightMin; + const heightQuantStep = terrainRange / 65_535; + // 2^-23 is float32's machine epsilon; the sampled and decoded values share that rounding. + const tolerance = heightQuantStep + terrainRange * 1.2e-7; + for (const item of state.instances) { + const pose = poseOf(item); + if (!pose.grounding) continue; + const groundY = sampleHeight(decodedGrid, pose.position[0], pose.position[2]); + const difference = Math.abs(pose.position[1] - groundY); + if (Number.isFinite(difference) && difference > tolerance) { + throw new Error( + `Placement '${item.id}' of species '${item.asset}' contact check failed: base Y=${pose.position[1]} misses terrain ground Y=${groundY} by ${difference.toFixed(4)}m (> tolerance ${tolerance.toFixed(4)}m). Check for mirrored-Z or invalid coordinates.`, + ); + } + } + + // Setup glTF document. The default scene holds only the near LOD of every species; each + // alternate LOD gets its own named scene, so no two LODs render at once. const doc = new Document(); const scene = doc.createScene("TerrainWorld"); const buffer = doc.createBuffer(); @@ -313,83 +474,44 @@ export async function cookTerrainWorld( placementCounts[item.asset] = (placementCounts[item.asset] ?? 0) + 1; } - // One EXT_mesh_gpu_instancing node per species: every logical placement is one instance, and the - // species' source meshes flatten into a single mesh so the node count stays constant. - for (const { speciesId, speciesDoc } of speciesFiles) { + // Near LOD: one instancing node per species in the default scene, every logical placement one + // instance, so the node count stays constant while the instance count grows. + for (const { speciesId, nearDoc } of speciesFiles) { const items = placementsBySpecies.get(speciesId) ?? []; if (items.length === 0) continue; + addInstancedSpeciesNode( + doc, + buffer, + nearDoc, + items, + scene, + instancingExt, + `${speciesId}_instances`, + ); + } - const count = items.length; - const translations = new Float32Array(count * 3); - const rotations = new Float32Array(count * 4); - const scales = new Float32Array(count * 3); - - for (const [i, item] of items.entries()) { - const pose = poseOf(item); - translations[i * 3 + 0] = pose.position[0]; - translations[i * 3 + 1] = pose.position[1]; - translations[i * 3 + 2] = pose.position[2]; - - rotations[i * 4 + 0] = pose.quaternion[0]; - rotations[i * 4 + 1] = pose.quaternion[1]; - rotations[i * 4 + 2] = pose.quaternion[2]; - rotations[i * 4 + 3] = pose.quaternion[3]; - - scales[i * 3 + 0] = pose.scale[0]; - scales[i * 3 + 1] = pose.scale[1]; - scales[i * 3 + 2] = pose.scale[2]; - } - - const transAccessor = doc - .createAccessor() - .setType("VEC3") - .setArray(translations) - .setBuffer(buffer); - const rotAccessor = doc.createAccessor().setType("VEC4").setArray(rotations).setBuffer(buffer); - const scaleAccessor = doc.createAccessor().setType("VEC3").setArray(scales).setBuffer(buffer); - - const batch = instancingExt - .createInstancedMesh() - .setAttribute("TRANSLATION", transAccessor) - .setAttribute("ROTATION", rotAccessor) - .setAttribute("SCALE", scaleAccessor); - - // Any extension the source uses must exist on the target document before its properties copy. - type ExtensionCtor = Parameters[0]; - for (const sourceExtension of speciesDoc.getRoot().listExtensionsUsed()) { - doc.createExtension(sourceExtension.constructor as unknown as ExtensionCtor); - } - - // Bring meshes, materials, textures and accessors across, then bake each source node's world - // matrix into its geometry. A fresh resolver per node keeps geometry and accessors private, so - // one source mesh reused by several nodes keeps each node's own transform. `transformMesh` - // keeps every attribute and material. - const rootScene = - speciesDoc.getRoot().getDefaultScene() ?? speciesDoc.getRoot().listScenes()[0] ?? null; - const meshNodes: { node: Node; mesh: Mesh }[] = []; - rootScene?.traverse((node: Node) => { - const mesh = node.getMesh(); - if (mesh) meshNodes.push({ node, mesh }); - }); - - const combined = doc.createMesh(`${speciesId}_mesh`); - for (const { node, mesh } of meshNodes) { - const resolve = createDefaultPropertyResolver(doc, speciesDoc); - const targetMesh = copyToDocument(doc, speciesDoc, [mesh], resolve).get(mesh) as Mesh; - transformMesh(targetMesh, node.getWorldMatrix()); - for (const primitive of targetMesh.listPrimitives()) { - targetMesh.removePrimitive(primitive); - combined.addPrimitive(primitive); - } + // Alternate LODs: one instancing node per species-and-LOD pair, each in its own named scene, + // with the same placements as the near node. glTF renders only the default scene, so no two + // LODs draw at once. + for (const { speciesId, lods } of speciesFiles) { + const items = placementsBySpecies.get(speciesId) ?? []; + if (items.length === 0) continue; + for (const [i, level] of lods.entries()) { + const lodScene = doc.createScene(`${speciesId}_lod${i + 1}`); + addInstancedSpeciesNode( + doc, + buffer, + level.doc, + items, + lodScene, + instancingExt, + `${speciesId}_lod${i + 1}_instances`, + ); } - - const instancedNode = doc - .createNode(`${speciesId}_instances`) - .setMesh(combined) - .setExtension("EXT_mesh_gpu_instancing", batch); - scene.addChild(instancedNode); } + doc.getRoot().setDefaultScene(scene); + await doc.transform(unpartition()); // Keep solid-texture removal off: species materials are authored, and a flat image is not ours to drop. await doc.transform(prune({ keepSolidTextures: true })); @@ -407,8 +529,7 @@ export async function cookTerrainWorld( } for (const { speciesId, lod0, lods } of speciesFiles) { - const nearGlbName = `${speciesId}.glb`; - await writeFile(path.join(out, "models", nearGlbName), lod0); + await writeFile(path.join(out, "models", `${speciesId}.glb`), lod0); for (const level of lods) { await writeFile(path.join(out, "models", level.filename), level.glb); } @@ -418,8 +539,8 @@ export async function cookTerrainWorld( const finalNodes = doc.getRoot().listNodes(); let instancedMeshCount = 0; - for (const n of finalNodes) { - if (n.getExtension("EXT_mesh_gpu_instancing") !== null) instancedMeshCount++; + for (const child of scene.listChildren()) { + if (child.getExtension("EXT_mesh_gpu_instancing") !== null) instancedMeshCount++; } return { From 55d64bb315d3fcc748a328c6125b77aceeb6e335 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 14:18:16 -0700 Subject: [PATCH 015/210] fix(terrain): include terrain in isolated asset cook installs --- ...-bakes-headlessly-into-an-instanced-glb.md | 6 +++-- .../scripts/verify-game-handoff.mjs | 25 ++++++++++++++++--- .../scripts/verify-starter-kit.mjs | 9 ++++++- packages/terrain/starter/forest/README.md | 7 ++++-- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md index 6c0841000..7db684af0 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md @@ -58,7 +58,7 @@ flowchart LR ## Acceptance Criteria -- [ ] AC-1 [local]: `cookTerrainWorld` runs under plain Node 22 with no DOM globals and writes `world.glb` for the forest kit. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 focused spec 7/10 passed; the cook writes `world.glb` for the evaluated-terrain fixture under Node. The forest-kit bake is not run, so this stays open. +- [x] AC-1 [local]: `cookTerrainWorld` runs under plain Node 22 with no DOM globals and writes `world.glb` for the forest kit. proof: `npm exec --yes --package=node@22 -- node src/terrain/forest/bake.mjs --out assets/terrain/forest` in an isolated kit install — Evidence: 2026-10-10 Node v22.23.3, an isolated minimal-template game installed from the packed core/physics/assets/playtest/CLI/terrain tarballs (terrain override resolves the asset pipeline's transitive edge), with `document`/`window`/`FileReader`/`HTMLCanvasElement` all undefined: `bake.mjs` exit 0 and `world.glb` 24 641 592 B. The closure fix is in `examples/strata-terrain-preview/scripts/verify-starter-kit.mjs` (terrain tarball added to the scaffold `packageSources`, which writes the pnpm override) and `verify-game-handoff.mjs` (same tarball as an override-only entry). - [x] AC-2 [local]: Each species-and-LOD pair is one `EXT_mesh_gpu_instancing` node; node count does not grow with tree count. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 17 tests pass; the default `TerrainWorld` scene holds near nodes only and each species-and-LOD pair is one node in its own named scene (`fir_lod1`, `boulder_lod1`), every node carrying all logical placements; the all-scene census is constant 1 000→10 000, one instancing node for a single-mesh species and one flattened node (four primitives) for a two-mesh species. `instancedMeshCount` stays the default-scene count for compatibility; the all-scene test walk is authoritative and `nodeCount` counts every scene. - [x] AC-3 [local]: A vanilla three `GLTFLoader` loads `world.glb` into `InstancedMesh` objects whose instance count equals the bake's placement count. proof: `CONSUMER_GAME=skip pnpm --filter strata-terrain-preview test:consumer` — Evidence: 2026-10-10 exit 0 in 23 s; the cooked forest GLB loads in the isolated packed install as `fir_instances` (4 primitives × 3 100), `fir-c_instances` (4 × 1 400) and `boulder_instances` (1 × 320), every child count equal to its manifest run count and 0 per-tree mesh copies. The unskipped run's scaffold `pnpm install` failed here (exit 1), so the PRD-468 game-handoff stage stays unverified. - [x] AC-4 [local]: A placement mirrored in Z fails the cook with a named contact error. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 12 tests pass, including `Placement '' of species 'fir' contact check failed ... mirrored-Z`. @@ -68,7 +68,7 @@ flowchart LR | Capability | Reachable consumer/trigger | Replaces / disposition | Evidence | |---|---|---|---| -| Headless world GLB | `cookTerrainWorld` from `@threenative/assets`; kit `bake.mjs`; PRD-584 MCP tool | Browser `exportWorldGLB` stays for the editor until PRD-588 | AC-1, AC-3 (unverified) | +| Headless world GLB | `cookTerrainWorld` from `@threenative/assets`; kit `bake.mjs`; PRD-584 MCP tool | Browser `exportWorldGLB` stays for the editor until PRD-588 | AC-1 (Node 22 kit), AC-3 | | Instanced scatter in GLB | Any `GLTFLoader` consumer | Per-tree mesh nodes | AC-2 | ## Execution Phases @@ -109,3 +109,5 @@ Initial review: six weak unit tests passed; `node --check examples/strata-terrai 2026-10-10 integration: the forest bake produced a 15 874 700-byte GLB and a valid world package (3 100 fir, 1 400 fir-c, 320 boulder). The scoped parser proof ran with CONSUMER_GAME=skip; the ordinary test:consumer command still fails during the separate PRD-468 scaffold install and is not game acceptance. A missing cooked forest fixture now fails closed. All-scene species/LOD census and full layered terrain appearance remain pending. 2026-10-10 cook follow-up: 17 focused tests pass (336 ms), assets typecheck passes and scoped Biome passes with one complexity warning. Alternate LODs live in named scenes while the default scene renders near detail once; every species/LOD pair has one instancing node. Empty species fail before output. Contact samples the baked uint16 heightmap with one quantization-step tolerance plus float rounding. Node 22.23.3 was resolved by an isolated npm launcher, but its forest cook failed because the standalone kit could not resolve @threenative/assets; AC-1 stays open until the kit/tarball dependency closure is wired. Multi-layer material appearance and runtime/visual proof remain pending. + +2026-10-10 caller/dependency closure repair: the red failures had one cause — `@threenative/assets@0.3.5` depends on `@threenative/terrain`, which was absent from the local tarball closures, so both installs tried the registry and got `ERR_PNPM_FETCH_404 @threenative/terrain`. `verify-starter-kit.mjs` now packs the terrain tarball into the scaffold `packageSources` (writing the pnpm override for assets' transitive edge) before the kit's own `pnpm add -D`; `verify-game-handoff.mjs` packs the same tarball and writes it as an override-only entry, so the game still declares and installs no authoring package. `packages/terrain/starter/forest/README.md` names both `@threenative/terrain` and `@threenative/assets` plus the `three` peer. Node 22.23.3 forest proof in an isolated kit: `bake.mjs` exit 0, `world.glb` 24 641 592 B, no DOM globals — AC-1 ticked. `node --check` and scoped Biome pass on both scripts. The unskipped full consumer (scaffold build + WebGPU playtest) was not run; the root job runs it after commit. Multi-layer surface appearance and runtime/visual proof remain pending. diff --git a/examples/strata-terrain-preview/scripts/verify-game-handoff.mjs b/examples/strata-terrain-preview/scripts/verify-game-handoff.mjs index 6281e93d2..04f5af582 100644 --- a/examples/strata-terrain-preview/scripts/verify-game-handoff.mjs +++ b/examples/strata-terrain-preview/scripts/verify-game-handoff.mjs @@ -74,12 +74,18 @@ export async function verifyGameHandoff({ repo, temporary, polished, withPage }) ...templateManifest.optionalDependencies, }), ); - const packages = JSON.parse( + const workspacePackages = JSON.parse( execFileSync("pnpm", ["exec", "tsx", "scripts/workspace-packages.ts", "--json"], { cwd: repo, encoding: "utf8", }), - ).filter(({ name }) => dependencies.has(name)); + ); + // `@threenative/assets` (the template's dev-only asset pipeline) depends on `@threenative/terrain`, + // so the terrain tarball is packed to supply the pnpm override for that transitive edge. The game + // itself must not declare or install the authoring package (asserted below). + const packages = workspacePackages.filter( + ({ name }) => dependencies.has(name) || name === "@threenative/terrain", + ); assert(packages.length > 0, "minimal template has no workspace packages to pack"); for (const { name, directory } of packages) { const before = new Set(readdirSync(packs)); @@ -93,10 +99,23 @@ export async function verifyGameHandoff({ repo, temporary, polished, withPage }) pathToFileURL(join(repo, "packages/create-threenative/dist/index.js")).href ); const scaffolded = await createProject( - { install: true, packageSources: archives, target: "handoff-game", template: "minimal" }, + { install: false, packageSources: archives, target: "handoff-game", template: "minimal" }, temporary, ); const game = scaffolded.target; + // `packageSources` writes the terrain source into `dependencies`; the game must not depend on it, + // so it is removed and the install runs with the pnpm override alone. The override still resolves + // the asset pipeline's transitive edge to the local tarball, without linking terrain into the + // game's own tree. + const gameManifestPath = join(game, "package.json"); + const gameManifest = JSON.parse(readFileSync(gameManifestPath, "utf8")); + gameManifest.dependencies = Object.fromEntries( + Object.entries(gameManifest.dependencies ?? {}).filter( + ([name]) => name !== "@threenative/terrain", + ), + ); + writeFileSync(gameManifestPath, `${JSON.stringify(gameManifest, null, 2)}\n`); + run("pnpm", ["install"], { cwd: game }); // --- no authoring package in the game, by its manifest, its install and its source ------------------ const manifest = JSON.parse(readFileSync(join(game, "package.json"), "utf8")); diff --git a/examples/strata-terrain-preview/scripts/verify-starter-kit.mjs b/examples/strata-terrain-preview/scripts/verify-starter-kit.mjs index db0db1491..d3afa5594 100644 --- a/examples/strata-terrain-preview/scripts/verify-starter-kit.mjs +++ b/examples/strata-terrain-preview/scripts/verify-starter-kit.mjs @@ -335,7 +335,14 @@ async function main() { await createProject( { install: true, - packageSources: Object.fromEntries(GAME_PACKAGES.map(({ name }) => [name, archives[name]])), + // The terrain tarball is a source here too, not only the later `pnpm add -D`: the minimal + // template's `@threenative/assets` dev dependency now depends on `@threenative/terrain`, + // and the override this writes is what resolves that transitive edge to the local tarball + // instead of the registry (which 404s). The kit's own `pnpm add -D` then moves it to the + // devDependency the README names. + packageSources: Object.fromEntries( + [...GAME_PACKAGES, TERRAIN_PACKAGE].map(({ name }) => [name, archives[name]]), + ), target: "kit-game", template: "minimal", }, diff --git a/packages/terrain/starter/forest/README.md b/packages/terrain/starter/forest/README.md index e1554511b..e4ca88204 100644 --- a/packages/terrain/starter/forest/README.md +++ b/packages/terrain/starter/forest/README.md @@ -18,8 +18,11 @@ Art is CC0 (Poly Haven); provenance is in `@threenative/terrain/starter-assets/c ## Three steps 1. Copy `node_modules/@threenative/terrain/starter/forest` to `src/terrain/forest`. -2. Run `node src/terrain/forest/bake.mjs --out assets/terrain/forest` (needs `@threenative/terrain` - as a devDependency). The same recipe always writes the same bytes. +2. Run `node src/terrain/forest/bake.mjs --out assets/terrain/forest`. The bake imports both + `@threenative/terrain` and `@threenative/assets`, so install them as devDependencies + (`npm install --save-dev @threenative/terrain @threenative/assets`); `three` must be installed + too, because `@threenative/terrain` declares it as a peer dependency. The same recipe always + writes the same bytes. 3. Add it to a scene: ```ts From 38c83ef483ca963cb52bfe0f11cc52bf2ab3d57f Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 14:19:15 -0700 Subject: [PATCH 016/210] fix(terrain): declare root forest cook dependency --- ...83-a-strata-world-bakes-headlessly-into-an-instanced-glb.md | 2 ++ package.json | 1 + pnpm-lock.yaml | 3 +++ 3 files changed, 6 insertions(+) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md index 7db684af0..596b58b7f 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md @@ -111,3 +111,5 @@ Initial review: six weak unit tests passed; `node --check examples/strata-terrai 2026-10-10 cook follow-up: 17 focused tests pass (336 ms), assets typecheck passes and scoped Biome passes with one complexity warning. Alternate LODs live in named scenes while the default scene renders near detail once; every species/LOD pair has one instancing node. Empty species fail before output. Contact samples the baked uint16 heightmap with one quantization-step tolerance plus float rounding. Node 22.23.3 was resolved by an isolated npm launcher, but its forest cook failed because the standalone kit could not resolve @threenative/assets; AC-1 stays open until the kit/tarball dependency closure is wired. Multi-layer material appearance and runtime/visual proof remain pending. 2026-10-10 caller/dependency closure repair: the red failures had one cause — `@threenative/assets@0.3.5` depends on `@threenative/terrain`, which was absent from the local tarball closures, so both installs tried the registry and got `ERR_PNPM_FETCH_404 @threenative/terrain`. `verify-starter-kit.mjs` now packs the terrain tarball into the scaffold `packageSources` (writing the pnpm override for assets' transitive edge) before the kit's own `pnpm add -D`; `verify-game-handoff.mjs` packs the same tarball and writes it as an override-only entry, so the game still declares and installs no authoring package. `packages/terrain/starter/forest/README.md` names both `@threenative/terrain` and `@threenative/assets` plus the `three` peer. Node 22.23.3 forest proof in an isolated kit: `bake.mjs` exit 0, `world.glb` 24 641 592 B, no DOM globals — AC-1 ticked. `node --check` and scoped Biome pass on both scripts. The unskipped full consumer (scaffold build + WebGPU playtest) was not run; the root job runs it after commit. Multi-layer surface appearance and runtime/visual proof remain pending. + +2026-10-10 direct checkout command: root now declares the existing @threenative/assets workspace package as a dev dependency (terrain does not depend on assets). node packages/terrain/starter/forest/bake.mjs also passes from the checkout and emits the instanced GLB plus package, so the documented direct command has an explicit dependency closure. diff --git a/package.json b/package.json index 205a2bfc5..b5c716068 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "@gltf-transform/extensions": "catalog:", "@gltf-transform/functions": "catalog:", "@playwright/test": "1.62.1", + "@threenative/assets": "workspace:*", "@types/node": "20.19.43", "@types/pngjs": "6.0.5", "@vitest/coverage-v8": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ea3a2507..a2a01277b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,6 +145,9 @@ importers: '@playwright/test': specifier: 1.62.1 version: 1.62.1 + '@threenative/assets': + specifier: workspace:* + version: link:packages/assets '@types/node': specifier: 20.19.43 version: 20.19.43 From f46902d0507b2acd3b666c90cf24409476f1de61 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 14:42:06 -0700 Subject: [PATCH 017/210] docs(strata): record packed consumer pass and pending visual scope --- ...a-world-bakes-headlessly-into-an-instanced-glb.md | 12 +++++++++--- .../PRD-585-strata-proof-runs-in-ci.md | 4 ++++ docs/PRDs/strata-terrain-hardening/README.md | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md index 596b58b7f..a76e705ca 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md @@ -60,7 +60,7 @@ flowchart LR - [x] AC-1 [local]: `cookTerrainWorld` runs under plain Node 22 with no DOM globals and writes `world.glb` for the forest kit. proof: `npm exec --yes --package=node@22 -- node src/terrain/forest/bake.mjs --out assets/terrain/forest` in an isolated kit install — Evidence: 2026-10-10 Node v22.23.3, an isolated minimal-template game installed from the packed core/physics/assets/playtest/CLI/terrain tarballs (terrain override resolves the asset pipeline's transitive edge), with `document`/`window`/`FileReader`/`HTMLCanvasElement` all undefined: `bake.mjs` exit 0 and `world.glb` 24 641 592 B. The closure fix is in `examples/strata-terrain-preview/scripts/verify-starter-kit.mjs` (terrain tarball added to the scaffold `packageSources`, which writes the pnpm override) and `verify-game-handoff.mjs` (same tarball as an override-only entry). - [x] AC-2 [local]: Each species-and-LOD pair is one `EXT_mesh_gpu_instancing` node; node count does not grow with tree count. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 17 tests pass; the default `TerrainWorld` scene holds near nodes only and each species-and-LOD pair is one node in its own named scene (`fir_lod1`, `boulder_lod1`), every node carrying all logical placements; the all-scene census is constant 1 000→10 000, one instancing node for a single-mesh species and one flattened node (four primitives) for a two-mesh species. `instancedMeshCount` stays the default-scene count for compatibility; the all-scene test walk is authoritative and `nodeCount` counts every scene. -- [x] AC-3 [local]: A vanilla three `GLTFLoader` loads `world.glb` into `InstancedMesh` objects whose instance count equals the bake's placement count. proof: `CONSUMER_GAME=skip pnpm --filter strata-terrain-preview test:consumer` — Evidence: 2026-10-10 exit 0 in 23 s; the cooked forest GLB loads in the isolated packed install as `fir_instances` (4 primitives × 3 100), `fir-c_instances` (4 × 1 400) and `boulder_instances` (1 × 320), every child count equal to its manifest run count and 0 per-tree mesh copies. The unskipped run's scaffold `pnpm install` failed here (exit 1), so the PRD-468 game-handoff stage stays unverified. +- [x] AC-3 [local]: A vanilla three `GLTFLoader` loads `world.glb` into `InstancedMesh` objects whose instance count equals the bake's placement count. proof: `CONSUMER_GAME=skip pnpm --filter strata-terrain-preview test:consumer` — Evidence: 2026-10-10 exit 0 in 23 s; the cooked forest GLB loads in the isolated packed install as `fir_instances` (4 primitives × 3 100), `fir-c_instances` (4 × 1 400) and `boulder_instances` (1 × 320), every child count equal to its manifest run count and 0 per-tree mesh copies. The parent subsequently passed the unskipped packed-game install, build and WebGPU playtest on NVIDIA Turing (2026-10-10); that game stage does not prove the cooked forest's layered material appearance. - [x] AC-4 [local]: A placement mirrored in Z fails the cook with a named contact error. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 12 tests pass, including `Placement '' of species 'fir' contact check failed ... mirrored-Z`. - [x] AC-5 [local]: The world package written next to it passes `validateWorldPackage` and lists a GLB and at least one LOD per species. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — Evidence: 2026-10-10 the spec reads the cook's `world.json` and `placements.bin`, calls core `validateWorldPackage` with `placementsByteLength` (and `heightmapByteLength`) and gets `{ ok: true, errors: [] }`, and every species lists `models/.glb` plus a LOD whose bytes exist on disk. @@ -74,13 +74,15 @@ flowchart LR ## Execution Phases #### Phase 1: terrain and scatter into one instanced GLB -**Status:** done +**Status:** PARTIAL — layered surface appearance remains pending **Files:** `packages/assets/src/world/terrain-world.ts` (new), `packages/assets/src/index.ts`, `packages/assets/__tests__/terrain-world.spec.ts` **Implementation:** read `bakeTerrain` chunks and placements; group by species and LOD; write `EXT_mesh_gpu_instancing` nodes with gltf-transform; pass existing image bytes through; contact check per placement. - [x] Node-only cook writes `world.glb`. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` — 2026-10-10: 7/10 passed; cook writes `world.glb` for the evaluated-terrain fixture under Node. - [x] Node count is constant from 1 000 to 10 000 trees. proof: same spec — 2026-10-10: node count equal at 1k and 10k, one instancing node for a single-mesh species. - [x] Mirrored placements fail by name. proof: same spec — 2026-10-10: mirrored-Z case throws `Placement '' of species 'fir' contact check failed`. +- [ ] Preserve the authored layered PBR surface and resolve `texturesDir` headlessly. proof: layered cook spec plus headed before/after visual judge — pending; the current GLB uses only the first layer. + #### Phase 2: LODs and the world package **Status:** done **Files:** `packages/assets/src/world/terrain-world.ts`, `packages/terrain/starter/forest/bake.mjs` @@ -89,11 +91,13 @@ flowchart LR - [x] The forest kit bake emits both outputs. proof: `node packages/terrain/starter/forest/bake.mjs` — 2026-10-10 exit 0 in 166 ms; writes `world.glb` (15 874 700 B) and the world package (`world.json`, `heightmap.u16`, `placements.bin`, `splat.rgba`, `terrain-table.json`, `models/{fir,boulder,fir-c}.glb` with LODs); 3 100 fir, 1 400 fir-c and 320 boulder placements. #### Phase 3: consumer load -**Status:** done +**Status:** PARTIAL — headed runtime and visual proof remain pending **Files:** `examples/strata-terrain-preview/scripts/verify-consumer.mjs` **Implementation:** load the cooked `world.glb` with stock `GLTFLoader`; assert instance counts and that no mesh is a per-tree copy. - [x] Vanilla loader gets `InstancedMesh` with the bake's counts. proof: `CONSUMER_GAME=skip pnpm --filter strata-terrain-preview test:consumer` — 2026-10-10 exit 0; the generated consumer script reads the cooked `world.json`, derives each species' logical count from its runs, and checks each child `InstancedMesh` count (fir 4×3 100, fir-c 4×1 400, boulder 1×320) with 0 per-tree copies and no hardcoded values. +- [ ] Render the cooked world in the headed WebGPU consumer and pass the visual judge. proof: two same-pose runs per arm with checked adapter and labelled images — pending; the first baseline capture was black and is rejected. + ## Current plan 1. Reuse `bakeTerrain` and `bakeWorldPackage`; preserve source glTF textures/transforms and verify real terrain, splat and species batching. @@ -113,3 +117,5 @@ Initial review: six weak unit tests passed; `node --check examples/strata-terrai 2026-10-10 caller/dependency closure repair: the red failures had one cause — `@threenative/assets@0.3.5` depends on `@threenative/terrain`, which was absent from the local tarball closures, so both installs tried the registry and got `ERR_PNPM_FETCH_404 @threenative/terrain`. `verify-starter-kit.mjs` now packs the terrain tarball into the scaffold `packageSources` (writing the pnpm override for assets' transitive edge) before the kit's own `pnpm add -D`; `verify-game-handoff.mjs` packs the same tarball and writes it as an override-only entry, so the game still declares and installs no authoring package. `packages/terrain/starter/forest/README.md` names both `@threenative/terrain` and `@threenative/assets` plus the `three` peer. Node 22.23.3 forest proof in an isolated kit: `bake.mjs` exit 0, `world.glb` 24 641 592 B, no DOM globals — AC-1 ticked. `node --check` and scoped Biome pass on both scripts. The unskipped full consumer (scaffold build + WebGPU playtest) was not run; the root job runs it after commit. Multi-layer surface appearance and runtime/visual proof remain pending. 2026-10-10 direct checkout command: root now declares the existing @threenative/assets workspace package as a dev dependency (terrain does not depend on assets). node packages/terrain/starter/forest/bake.mjs also passes from the checkout and emits the instanced GLB plus package, so the documented direct command has an explicit dependency closure. + +2026-10-10 parent integration: offline frozen installation, assets build/publint and 17 terrain-world tests pass (1.05 s test time). The direct forest command emits the 24 641 592 B GLB in 595 ms. The full consumer first stopped before the game stage because the default cooked fixture was absent; after the documented bake, the unskipped rerun passes (exit 0). Its scaffolded game playtest reports WebGPU, NVIDIA Turing and no direct authoring-package dependency; the forest stock-loader census reports 4 820 placements and zero per-tree copies. Layered-surface and headed forest visual acceptance remain open. diff --git a/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md index fd81339da..c8a01d9da 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md @@ -66,3 +66,7 @@ Local runtime attempt on candidate `845442617` (2026-10-10, Debian laptop, heade A desktop comparison on the same executable candidate used verified NVIDIA Turing WebGPU and passed 59/62 assertions. Its failures were `afterFirstFrameTaskMs` 12,635 > 250 ms, `timeToReadyMs` 38,741.8 > 15,000 ms, and `maxViewGpuMs` 35.22 > 12 ms. The Intel shader error did not recur. This is another failed local performance result, not runtime or CI acceptance. Generated consumers currently omit `packageManager`, allowing Corepack to select ambient pnpm 12; the shared scaffolder repair is active and preserves explicit dependency-build approvals. ||||||| parent of 27566ccc1 (fix(scaffold): pin supported pnpm before consumer installs) Bootstrap repair (2026-10-10): generated projects now pin the shipped `pnpm@10.25.0` before dependency installation, including both omitted and present local-package overrides. The build-script allowlist is unchanged. The new regression fails on the original source (missing `packageManager`); the repaired full scaffold suite passes 68 tests. Focused scaffolder TypeScript and Biome checks pass (two existing complexity warnings), and the scaffolder build passes. The actual Node 20/Corepack consumer rerun is pending the laptop thermal guard; AC-1, AC-2 and runtime phase boxes remain open until their required proof exists. + +Parent bootstrap integration: all 68 scaffold tests pass on dde88e30c; scaffolder and assets builds pass, including declarations and package lint. The full consumer retry progressed beyond the earlier pnpm failure but stopped before gameplay: assets now depends on terrain, which the existing game-handoff tarball set does not supply, causing a registry 404. PRD-583 is wiring that local dependency closure. The game stage was not skipped; consumer and CI acceptance remain open. + +2026-10-10 parent consumer repair verified: after integrating the PRD-583 terrain tarball/override closure and baking the required default forest fixture, the full unskipped consumer passes (exit 0). Packed-game scaffold, install, build and WebGPU playtest report NVIDIA Turing; the stock forest loader checks 4 820 placements with zero per-tree copies. This closes the consumer bootstrap failure; the separate showcase runtime budgets and real-CI acceptance remain open. diff --git a/docs/PRDs/strata-terrain-hardening/README.md b/docs/PRDs/strata-terrain-hardening/README.md index be1e8fa23..ed6d3fa9a 100644 --- a/docs/PRDs/strata-terrain-hardening/README.md +++ b/docs/PRDs/strata-terrain-hardening/README.md @@ -36,4 +36,4 @@ Current baseline: `pnpm build` and `pnpm check:docs` passed; six documentation c ## Active worker checkouts -PRD-592 phase 1 is committed at `6c40d8757`; phase 2 remains open pending the 30% speed proof. Its queued benchmark was cancelled after the quiet lock proved unavailable; baseline and scripts are preserved. PRD-585 local work is integrated at `845442617`: 215 focused tests pass; real candidate CI and runtime acceptance remain open. Its released checkout `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-proof` is now owned by the PRD-585 scaffold bootstrap repair on branch `epic/strata-runtime-proof`; PRD-593 is queued, with its source and artifacts preserved. PRD-583 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-terrain-bake`, branch `epic/strata-terrain-bake`, based on `9e7a8cab0`. Its assets phase is integrated at `11e7fc71a`: frozen installation and 11 focused terrain-world tests pass on the EPIC candidate; GLB consumer, forest, LOD and runtime acceptance remain open. PRD-587 owns `/home/joao/projects/threenative/threenative-engine/.worktrees/strata-editor-durability`, branch `epic/strata-editor-durability`, based on `c1e421198`. Its server recovery phase is integrated at `5b8fbfa60`; the parent terrain build and 9 document tests pass after separating premature UI bindings. A fresh owner is auditing the isolated intent journal for asynchronous reload, whole-document rebasing and editable terrain synchronisation; queue and worker acceptance remain open. Codex coordinates integration into PR #483. Default coding arms share a Gemini provider lock; routes explicitly excluding Gemini after a verified task quality failure can run separately. Cleanup must inspect ignored data and confirm each owner has stopped after integration. +PRD-592 validation and golden bytes are integrated at `6c40d8757`; its new candidate passes 33 focused tests and preserves all 21 forest files, but the prescribed same-host timing still reaches only 23.33%, so the 30% target remains open. PRD-585 local coverage and the scaffold package-manager pin are integrated; 68 scaffold tests pass, while real CI and runtime acceptance remain open. PRD-583 forest cook, alternate LODs and both isolated-install dependency closures are integrated through `38c83ef48`; parent assets build and 17 cook tests pass, and the unskipped packed-game consumer passes on NVIDIA Turing; layered-material visual proof remains pending. PRD-587 server recovery is integrated at `5b8fbfa60`; its queue remains under review for a baseline concurrency risk, with UI and worker proofs open. PRD-593 owns the reused proof checkout and is building one pinned historical source state; 16 predecessor blobs still need source coverage. The active owner checkouts remain `.worktrees/strata-terrain-bake`, `.worktrees/strata-editor-durability` and `.worktrees/strata-terrain-proof`; cleanup follows owner release and tracked, untracked and ignored-data checks. Codex coordinates this entire batch through PR #483. From e0a53dc1435fe5c7d7d22a3d3d10e5e84642d2b4 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 14:42:56 -0700 Subject: [PATCH 018/210] perf(terrain): remove repeated hydraulic and placement work --- ...ls-closed-and-erodes-without-allocating.md | 2 + packages/terrain/src/core/erosion.ts | 171 +++++++++++------- packages/terrain/src/core/world-package.ts | 34 ++-- 3 files changed, 124 insertions(+), 83 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md b/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md index f79907910..496379b2b 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md @@ -61,3 +61,5 @@ Verification (2026-10-10): the initial allocation-removal candidate passed 254 t Bound repair plan: existing layer validation caps explicitly supplied droplets at 200000 (`validation.ts:449–455`). The first guard incorrectly capped automatic `n*n` defaults; removing the cap entirely left the work bound unproven. Preserve the supplied/default distinction and derive the automatic bound from the existing supported grid limit, with regression tests for large default grids and excessive explicit counts. Bound repair implemented (2026-10-10): `hydraulic` now distinguishes a supplied `droplets` (held to the 200000 recipe limit) from the automatic default (one droplet per cell, capped at the largest supported grid 1025²). `erosion-bounds.spec.ts` proves the 513/1025 defaults, the accepted 200000 limit, rejected 200001 and unsafe-integer counts, and equal exact capped work at 1025/2048 grids. + +2026-10-10 parent candidate review: the bounded profiling arm timed out after saving erosion-loop and placement-record optimizations; no fallback was started. Rebuilt terrain passes publint and 33 focused erosion/defaults/spikes/world-package/starter tests, including all 20 pinned kit hashes. Scoped Biome passes with four complexity warnings. A Python wall-clock sample suggested 45.45%, but the prescribed GNU Time `-v` three-run rerun contradicts that: baseline 0.84 / 0.63 / 0.63 s versus candidate 0.52 / 0.53 / 0.56 s, 23.33% faster. All 21 forest files remain byte-identical. AC-5 and phase 2 stay open; no best-sample speed claim. Raw timing: `/tmp/strata592-time-v.rfj5oeor`. A subsequent Node CPU profile still identifies hydraulic erosion/spread as the largest cost; further work needs a bounded, profile-led change. diff --git a/packages/terrain/src/core/erosion.ts b/packages/terrain/src/core/erosion.ts index d27a3d0d1..cf91f2526 100644 --- a/packages/terrain/src/core/erosion.ts +++ b/packages/terrain/src/core/erosion.ts @@ -169,6 +169,9 @@ export function hydraulic( const h = Float64Array.from(height); const rnd = random(seed); const cell = size / (n - 1); + const minSlope = 0.005 * cell; + const oneMinusInertia = 1 - inertia; + const oneMinusEvaporation = 1 - evaporation; const brushCells = brush(brushRadius); const brushDx = brushCells.dx; const brushDz = brushCells.dz; @@ -181,6 +184,9 @@ export function hydraulic( const brushOffset = new Int32Array(brushCount); for (let k = 0; k < brushCount; k += 1) brushOffset[k] = (brushDz[k] as number) * n + (brushDx[k] as number); + const obsFlow = observations?.flow; + const obsSediment = observations?.sediment; + const obsDeposition = observations?.deposition; /** Adds `amount` over the brush around one cell, renormalised where the field ends. */ const spread = ( x: number, @@ -188,6 +194,7 @@ export function hydraulic( amount: number, floor = Number.NEGATIVE_INFINITY, ): number => { + if (amount === 0) return 0; // A brush fully inside the field sums every weight; only a brush at the edge needs the // partial total, which the first pass computes in the same order as `full`. const interior = x >= brushEdge && x < n - brushEdge && z >= brushEdge && z < n - brushEdge; @@ -196,24 +203,34 @@ export function hydraulic( if (interior) { const total = brushFull; if (total <= 0) return 0; - let moved = 0; const base = z * n + x; - const negative = amount < 0; - const depositing = observations !== undefined && amount > 0; - for (let k = 0; k < brushCount; k += 1) { - const i = base + (brushOffset[k] as number); - const share = (amount * (brushWeight[k] as number)) / total; - // A brush cell below the downstream bed has no soil the droplet can pick up. Taking it - // anyway digs pits deeper than the channel and leaves unsupported pillars between paths. - const change = negative ? -Math.min(-share, Math.max(0, (h[i] as number) - floor)) : share; - h[i] = (h[i] as number) + change; - moved += change; - if (depositing) - (observations as IErosionMaps).deposition[i] = - ((observations as IErosionMaps).deposition[i] as number) + - (amount * (brushWeight[k] as number)) / total; + if (amount < 0) { + let moved = 0; + for (let k = 0; k < brushCount; k += 1) { + const i = base + (brushOffset[k] as number); + const share = (amount * (brushWeight[k] as number)) / total; + const avail = (h[i] as number) - floor; + const change = avail <= 0 ? -0 : -share < avail ? share : -avail; + h[i] = (h[i] as number) + change; + moved += change; + } + return moved; } - return moved; + if (obsDeposition !== undefined) { + for (let k = 0; k < brushCount; k += 1) { + const i = base + (brushOffset[k] as number); + const share = (amount * (brushWeight[k] as number)) / total; + h[i] = (h[i] as number) + share; + obsDeposition[i] = (obsDeposition[i] as number) + share; + } + } else { + for (let k = 0; k < brushCount; k += 1) { + const i = base + (brushOffset[k] as number); + const share = (amount * (brushWeight[k] as number)) / total; + h[i] = (h[i] as number) + share; + } + } + return amount; } let total = 0; for (let k = 0; k < brushCount; k += 1) { @@ -222,26 +239,42 @@ export function hydraulic( if (ix >= 0 && ix < n && iz >= 0 && iz < n) total += brushWeight[k] as number; } if (total <= 0) return 0; - let moved = 0; - const negative = amount < 0; - const depositing = observations !== undefined && amount > 0; - for (let k = 0; k < brushCount; k += 1) { - const ix = x + (brushDx[k] as number); - const iz = z + (brushDz[k] as number); - if (ix < 0 || ix >= n || iz < 0 || iz >= n) continue; - const i = iz * n + ix; - const share = (amount * (brushWeight[k] as number)) / total; - // A brush cell below the downstream bed has no soil the droplet can pick up. Taking it - // anyway digs pits deeper than the channel and leaves unsupported pillars between paths. - const change = negative ? -Math.min(-share, Math.max(0, (h[i] as number) - floor)) : share; - h[i] = (h[i] as number) + change; - moved += change; - if (depositing) - (observations as IErosionMaps).deposition[i] = - ((observations as IErosionMaps).deposition[i] as number) + - (amount * (brushWeight[k] as number)) / total; + if (amount < 0) { + let moved = 0; + for (let k = 0; k < brushCount; k += 1) { + const ix = x + (brushDx[k] as number); + const iz = z + (brushDz[k] as number); + if (ix < 0 || ix >= n || iz < 0 || iz >= n) continue; + const i = iz * n + ix; + const share = (amount * (brushWeight[k] as number)) / total; + const avail = (h[i] as number) - floor; + const change = avail <= 0 ? -0 : -share < avail ? share : -avail; + h[i] = (h[i] as number) + change; + moved += change; + } + return moved; + } + if (obsDeposition !== undefined) { + for (let k = 0; k < brushCount; k += 1) { + const ix = x + (brushDx[k] as number); + const iz = z + (brushDz[k] as number); + if (ix < 0 || ix >= n || iz < 0 || iz >= n) continue; + const i = iz * n + ix; + const share = (amount * (brushWeight[k] as number)) / total; + h[i] = (h[i] as number) + share; + obsDeposition[i] = (obsDeposition[i] as number) + share; + } + } else { + for (let k = 0; k < brushCount; k += 1) { + const ix = x + (brushDx[k] as number); + const iz = z + (brushDz[k] as number); + if (ix < 0 || ix >= n || iz < 0 || iz >= n) continue; + const i = iz * n + ix; + const share = (amount * (brushWeight[k] as number)) / total; + h[i] = (h[i] as number) + share; + } } - return moved; + return amount; }; // Two reusable probes: `get` fills one instead of allocating a fresh object twice per step. const probeOld: IHydraulicProbe = { value: 0, dx: 0, dz: 0, ix: 0, iz: 0 }; @@ -252,23 +285,17 @@ export function hydraulic( const tx = x - ix; const tz = z - iz; const i = iz * n + ix; - out.value = lerp( - lerp(h[i] as number, h[i + 1] as number, tx), - lerp(h[i + n] as number, h[i + n + 1] as number, tx), - tz, - ); - out.dx = - lerp( - (h[i + 1] as number) - (h[i] as number), - (h[i + n + 1] as number) - (h[i + n] as number), - tz, - ) / cell; - out.dz = - lerp( - (h[i + n] as number) - (h[i] as number), - (h[i + n + 1] as number) - (h[i + 1] as number), - tx, - ) / cell; + const h00 = h[i] as number; + const h10 = h[i + 1] as number; + const h01 = h[i + n] as number; + const h11 = h[i + n + 1] as number; + const g10 = h10 - h00; + const g01 = h01 - h00; + const v0 = h00 + g10 * tx; + const v1 = h01 + (h11 - h01) * tx; + out.value = v0 + (v1 - v0) * tz; + out.dx = (g10 + (h11 - h01 - g10) * tz) / cell; + out.dz = (g01 + (h11 - h10 - g01) * tx) / cell; out.ix = ix; out.iz = iz; return out; @@ -283,22 +310,25 @@ export function hydraulic( let sediment = 0; for (let step = 0; step < maxSteps; step += 1) { const old = get(x, z, probeOld); - if (observations) { + if (obsFlow !== undefined && obsSediment !== undefined) { const tx = x - old.ix; const tz = z - old.iz; const i = old.iz * n + old.ix; - for (let oz = 0; oz < 2; oz++) { - for (let ox = 0; ox < 2; ox++) { - const index = i + oz * n + ox; - const weight = (ox ? tx : 1 - tx) * (oz ? tz : 1 - tz); - observations.flow[index] = (observations.flow[index] as number) + water * weight; - observations.sediment[index] = - (observations.sediment[index] as number) + sediment * weight; - } - } + const w00 = (1 - tx) * (1 - tz); + const w10 = tx * (1 - tz); + const w01 = (1 - tx) * tz; + const w11 = tx * tz; + obsFlow[i] = (obsFlow[i] as number) + water * w00; + obsSediment[i] = (obsSediment[i] as number) + sediment * w00; + obsFlow[i + 1] = (obsFlow[i + 1] as number) + water * w10; + obsSediment[i + 1] = (obsSediment[i + 1] as number) + sediment * w10; + obsFlow[i + n] = (obsFlow[i + n] as number) + water * w01; + obsSediment[i + n] = (obsSediment[i + n] as number) + sediment * w01; + obsFlow[i + n + 1] = (obsFlow[i + n + 1] as number) + water * w11; + obsSediment[i + n + 1] = (obsSediment[i + n + 1] as number) + sediment * w11; } - dx = dx * inertia - old.dx * (1 - inertia); - dz = dz * inertia - old.dz * (1 - inertia); + dx = dx * inertia - old.dx * oneMinusInertia; + dz = dz * inertia - old.dz * oneMinusInertia; let length = Math.hypot(dx, dz); if (length < 1e-9) { const angle = rnd() * Math.PI * 2; @@ -316,17 +346,20 @@ export function hydraulic( } const next = get(nx, nz, probeNext); const dh = next.value - old.value; - const cap = Math.max(-dh, 0.005 * cell) * speed * water * capacity; + const slope = -dh; + const cap = (slope > minSlope ? slope : minSlope) * speed * water * capacity; if (dh > 0 || sediment > cap) { - const amount = dh > 0 ? Math.min(dh, sediment) : (sediment - cap) * deposition; + const amount = dh > 0 ? (dh < sediment ? dh : sediment) : (sediment - cap) * deposition; spread(old.ix, old.iz, amount); sediment -= amount; } else { - const amount = Math.max(0, Math.min((cap - sediment) * erosion, -dh)); + const bite = (cap - sediment) * erosion; + const amount = bite < slope ? bite : slope; sediment -= spread(old.ix, old.iz, -amount, next.value); } - speed = Math.sqrt(Math.max(0.01, speed * speed - dh * 3)); - water *= 1 - evaporation; + const sq = speed * speed - dh * 3; + speed = Math.sqrt(sq > 0.01 ? sq : 0.01); + water *= oneMinusEvaporation; x = nx; z = nz; if (water < 0.02) break; diff --git a/packages/terrain/src/core/world-package.ts b/packages/terrain/src/core/world-package.ts index bd5bd2957..bc0078ea0 100644 --- a/packages/terrain/src/core/world-package.ts +++ b/packages/terrain/src/core/world-package.ts @@ -133,7 +133,12 @@ function uniformScale(asset: string, id: string, scale: readonly [number, number return mean; } -function placementRecord(item: IPlacement, extent: IWorldPackageManifest["extent"]): number[] { +function writePlacementRecord( + out: Float32Array, + offset: number, + item: IPlacement, + extent: IWorldPackageManifest["extent"], +): void { if (item.transform === undefined && !Number.isFinite(item.rotation)) throw Error(`World package: placement '${item.id}' has a non-finite rotation.`); const { position, quaternion, scale } = poseOf(item); @@ -156,16 +161,14 @@ function placementRecord(item: IPlacement, extent: IWorldPackageManifest["extent ); const length = Math.hypot(...quaternion); if (!(length > 0)) throw Error(`World package: placement '${item.id}' has a zero quaternion.`); - return [ - position[0], - position[1], - position[2], - quaternion[0] / length, - quaternion[1] / length, - quaternion[2] / length, - quaternion[3] / length, - uniformScale(item.asset, item.id, scale), - ]; + out[offset] = position[0]; + out[offset + 1] = position[1]; + out[offset + 2] = position[2]; + out[offset + 3] = quaternion[0] / length; + out[offset + 4] = quaternion[1] / length; + out[offset + 5] = quaternion[2] / length; + out[offset + 6] = quaternion[3] / length; + out[offset + 7] = uniformScale(item.asset, item.id, scale); } /** @@ -215,7 +218,7 @@ function placementCells( for (const [asset, items] of [...cell.items.entries()].sort(byId)) { runs.push({ asset, count: items.length, offset: written }); for (const item of items) { - records.set(placementRecord(item, extent), written * RECORD_VALUES); + writePlacementRecord(records, written * RECORD_VALUES, item, extent); written += 1; } } @@ -322,12 +325,15 @@ function splatBytes(state: ITerrainState, resolution: number): Uint8Array { const bytes = new Uint8Array(planeBytes * PLANES); for (let row = 0; row < resolution; row += 1) { const source = (resolution - 1 - row) * resolution; + const rowOffset = row * resolution * 4; for (let column = 0; column < resolution; column += 1) { const sample = (source + column) * MATERIAL_IDS.length; + const rowCol = rowOffset + column * 4; for (const slot of SLOTS) { const weight = state.splat[sample + slot.channel] as number; - bytes[slot.plane * planeBytes + row * resolution * 4 + column * 4 + slot.componentIndex] = - Math.round(Math.min(1, Math.max(0, weight)) * 255); + bytes[slot.plane * planeBytes + rowCol + slot.componentIndex] = Math.round( + (weight <= 0 ? 0 : weight >= 1 ? 1 : weight) * 255, + ); } } } From 5ad00f9bcacc747c14c8f7c94d8fc0916ccb0a55 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 14:44:28 -0700 Subject: [PATCH 019/210] chore(terrain): ignore generated forest world output --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 1ca5b32c9..42dd246b3 100644 --- a/.gitignore +++ b/.gitignore @@ -208,3 +208,6 @@ examples/native-css-hud/corpus/out/ examples/native-css-hud/corpus/out-interaction/ examples/native-css-hud/corpus/out-desktop/ examples/native-css-hud/bench/out/ + +# Forest kit headless bake output (authored recipe and assets remain tracked). +/packages/terrain/starter/forest/world/ From 69f3ce3b03e8dfc25a06c2de4564d67744fd7f9c Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 14:17:03 -0700 Subject: [PATCH 020/210] fix(terrain): preserve ordered save intents across document rebases --- ...-the-terrain-editor-never-drops-an-edit.md | 8 +- .../__tests__/editor-save-queue.spec.ts | 529 ++++++++++++++++++ packages/terrain/src/editor/saveQueue.ts | 450 +++++++++++++++ 3 files changed, 984 insertions(+), 3 deletions(-) create mode 100644 packages/terrain/__tests__/editor-save-queue.spec.ts create mode 100644 packages/terrain/src/editor/saveQueue.ts diff --git a/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md b/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md index 9f3cc9e19..b82a29717 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md @@ -39,7 +39,7 @@ the last valid state". The worker is recreated after a crash. Typed arrays trans ## Acceptance Criteria - [ ] AC-1 [local]: Two strokes 50 ms apart both persist to `world.json`. proof: a stroke step in `examples/strata-terrain-preview/playtests/editor.playtest.json` — Evidence: pending. Not run this arm (playtest deferred). -- [ ] AC-2 [local]: A save answered 409 leaves the edit in a visible "not saved" list with retry and discard. proof: a playtest that forces a 409 and reads the rendered not-saved list — Evidence: untested UI. the isolated draft queue tests exercise retention, retry and selected discard, but no test drives the rendered list, so this stays open. +- [ ] AC-2 [local]: A save answered 409 leaves the edit in a visible "not saved" list with retry and discard. proof: a playtest that forces a 409 and reads the rendered not-saved list — Evidence: untested UI. `editor-save-queue.spec.ts` (22/22) proves each refused edit stays in `unsent`, discarding one keeps the others, and retry/discard behave, but no test drives the rendered list, so this stays open. - [x] AC-3 [local]: The dev server starts on a malformed `world.json`, and "reload from disk" recovers after the file is fixed. proof: `editor-document.spec.ts` case — Evidence: 9/9 passed; malformed start reports a diagnostic, reload recovers, and restore is refused when the file was never valid. - [ ] AC-4 [local]: After the worker is terminated mid-build, the next edit rebuilds terrain. proof: `editor.playtest.json` step — Evidence: pending. Not run this arm (worker/playtest deferred). - [ ] AC-5 [local]: The worker's height, normal and splat buffers arrive transferred (detached on the worker side). proof: `editor-save-queue.spec.ts` or a worker spec — Evidence: pending. Fabricated `state.normal` zeros deleted; transfer list kept for the next phase. @@ -54,10 +54,10 @@ the last valid state". The worker is recreated after a crash. Typed arrays trans ## Execution Phases #### Phase 1: commands and the queue -**Status:** PARTIAL — server recovery is integrated; queue and refused-edit UI remain isolated drafts pending lifecycle review and playtests +**Status:** PARTIAL — isolated queue units implemented; browser wiring and refused-edit list remain a draft, with runtime proof pending **Files:** `packages/terrain/src/editor/app.js`, `packages/terrain/src/editor/saveQueue.ts` (new), `packages/terrain/src/editor/server.ts`, `packages/terrain/__tests__/editor-save-queue.spec.ts` (new) - [ ] Rapid strokes persist. proof: `editor.playtest.json` — not run this arm. -- [ ] Refused edits are kept and never resent. proof: `editor-save-queue.spec.ts` and rendered editor playtest — isolated draft units pass; asynchronous reload, whole-document rebasing and editable terrain synchronisation remain under review. No queue implementation is integrated yet. +- [ ] Refused edits are kept and never resent. proof: `editor-save-queue.spec.ts` — 22/22 passed in isolation; ordered journal, retry/discard, whole-document rebase against the latest confirmed document, and incompatible replay refusal. Browser integration remains uncommitted and unverified. #### Phase 2: recovery paths **Status:** PARTIAL — malformed-file recovery and never-valid restore guard landed; worker/playtest deferred @@ -73,3 +73,5 @@ the last valid state". The worker is recreated after a crash. Typed arrays trans The command queue, UI recovery controls and worker edits remain an uncommitted draft, excluded from the server-phase commit. Three reviewed incomplete fixes led to an explicitly approved narrower intent-journal replan. That arm reproduced discard losing sibling rejected edits, then added an ordered journal and reload/restore guards. Independently ran `editor-save-queue.spec.ts`, `editor-document.spec.ts`, `terrain-consumer.spec.ts` (43/43 total); edited JS syntax checks pass. Arm ran terrain build/publint and Biome successfully. These units do not prove the rendered editor lifecycle. Queue review remains open for reload-response races, queued whole-document saves after an external conflict, and synchronising replayed merged state with the editable terrain. AC-1/AC-2/AC-4/AC-5 and the queue phase remain open; no playtest has been run. Parent integration: the initial focused run loaded stale terrain output (7/9), then rebuilding exposed premature recovery callbacks passed to the unchanged UI. Removed only those two mount bindings; the controller methods remain available. Rebuilt terrain successfully, including declarations/publint, then independently passed all 9 document recovery tests. UI bindings remain part of the isolated durability draft. + +2026-10-10 follow-up: reproduced a queued whole-document save dropping an unrelated remote layer after an earlier 409 rebase, then fixed transaction construction to reuse the existing three-way merge. A real remote layer removal also reproduces an incompatible queued toggle; transaction-construction errors now retain that intent as refused and let the queue settle. Queue units are 22/22; focused strict source/spec typechecks pass. These checks cover the isolated queue only. Reload/restore response races, editable-terrain synchronisation, browser evidence and worker transfer/restart proof remain open. diff --git a/packages/terrain/__tests__/editor-save-queue.spec.ts b/packages/terrain/__tests__/editor-save-queue.spec.ts new file mode 100644 index 000000000..49d474a69 --- /dev/null +++ b/packages/terrain/__tests__/editor-save-queue.spec.ts @@ -0,0 +1,529 @@ +import type { PatchCommand } from "@threenative/terrain"; +import { Terrain } from "@threenative/terrain"; +import { describe, expect, it } from "vitest"; +import { + SaveQueue, + coalesceCommands, + diffLayers, + externalSnapshotAction, +} from "../src/editor/saveQueue.js"; +import type { + ISaveDocument, + ISaveSnapshot, + ISaveTransaction, + SaveLayer, +} from "../src/editor/saveQueue.js"; + +function recipe() { + return new Terrain({ size: 512, resolution: 129, seed: 73 }) + .noise({ id: "hills", amplitude: 16 }) + .toJSON(); +} +function doc(recipeValue: unknown): ISaveDocument { + return { version: 1, recipe: recipeValue } as ISaveDocument; +} +function ids(document_: ISaveDocument): string[] { + return (document_.recipe.layers ?? []).map((layer) => layer.id); +} + +/** + * A real in-process document authority: validates nothing itself but applies the same patch + * commands through the shipped `Terrain`, so a queue test exercises the actual command semantics. + * It can hold a commit open (to prove edits made while the first save is pending) and reject a + * chosen number of commits with a real 409, and an external writer can move the revision too. + */ +function makeServer(initial: ISaveDocument) { + let current: ISaveDocument = structuredClone(initial); + let revision = "r0"; + let counter = 0; + let holdCount = 0; + let rejectCount = 0; + const gates: (() => void)[] = []; + const commits: ISaveTransaction[] = []; + return { + commits, + get revision() { + return revision; + }, + get document() { + return structuredClone(current); + }, + snapshot(): ISaveSnapshot { + return { revision, document: structuredClone(current) }; + }, + hold(count: number) { + holdCount = count; + }, + release() { + holdCount = 0; + for (const gate of gates.splice(0)) gate(); + }, + reject(count: number) { + rejectCount = count; + }, + externalAdd(id: string) { + const terrain = Terrain.fromJSON(current.recipe as never).noise({ id, amplitude: 3 }); + current = { ...current, recipe: terrain.toJSON() as never }; + revision = `r${++counter}`; + }, + externalRemove(id: string) { + const terrain = Terrain.fromJSON(current.recipe as never).remove(id); + current = { ...current, recipe: terrain.toJSON() as never }; + revision = `r${++counter}`; + }, + externalMetadata(patch: Record) { + current = { ...current, ...patch }; + revision = `r${++counter}`; + }, + async commit(transaction: ISaveTransaction): Promise { + commits.push(transaction); + if (holdCount > 0) { + holdCount--; + await new Promise((resolve) => gates.push(resolve)); + } + if (rejectCount > 0) { + rejectCount--; + throw new Error("409: Stale base revision"); + } + if (transaction.baseRevision !== revision) throw new Error("409: Stale base revision"); + if (transaction.document) current = structuredClone(transaction.document); + else { + const terrain = Terrain.fromJSON(current.recipe as never); + terrain.applyPatch(transaction.commands ?? []); + current = { ...current, recipe: terrain.toJSON() as never }; + } + revision = `r${++counter}`; + return { revision, document: structuredClone(current) }; + }, + }; +} +type FakeServer = ReturnType; + +function makeQueue(server: FakeServer): SaveQueue { + return new SaveQueue({ + base: server.snapshot(), + commit: server.commit, + getSnapshot: async () => server.snapshot(), + }); +} + +function grow(recipeValue: unknown, id: string, amplitude: number) { + return Terrain.fromJSON(recipeValue as never) + .noise({ id, amplitude }) + .toJSON(); +} + +describe("terrain editor save queue", () => { + it("keeps a second edit made while the first save is pending", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + server.hold(1); + let local = grow(server.document.recipe, "a", 5); + queue.record(doc(local)); + expect(server.commits.length).toBe(1); + local = grow(local, "b", 7); + queue.record(doc(local)); + server.release(); + await queue.flush(); + expect(server.commits.length).toBe(2); + expect(ids(server.document).sort()).toEqual(["a", "b", "hills"]); + expect(queue.unsent.length).toBe(0); + }); + + it("keeps a layer added after a pending whole-document config change", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + server.hold(1); + const first = grow(server.document.recipe, "a", 5); + queue.record(doc(first)); + expect(server.commits.length).toBe(1); + const config = structuredClone(first); + config.config = { ...config.config, seed: 999 }; + queue.record(doc(config)); + const layered = grow(config, "b", 7); + queue.record(doc(layered)); + server.release(); + await queue.flush(); + expect(server.commits.length).toBe(2); + expect(ids(server.document).sort()).toEqual(["a", "b", "hills"]); + expect(server.document.recipe.config.seed).toBe(999); + expect(queue.unsent.length).toBe(0); + }); + + it("honours an undo made before the first save answers", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + const base = structuredClone(server.document.recipe); + server.hold(1); + queue.record(doc(grow(base, "a", 5))); + queue.record(doc(structuredClone(base))); + server.release(); + await queue.flush(); + expect(ids(server.document)).toEqual(["hills"]); + }); + + it("retries a real 409 once, then keeps the refused edit visible", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + const local = grow(server.document.recipe, "a", 5); + server.reject(2); + queue.record(doc(local)); + await queue.flush(); + expect(server.commits.length).toBe(2); + expect(queue.unsent.length).toBe(1); + const refused = queue.unsent[0]; + if (!refused) throw new Error("expected a refused edit"); + expect(refused.label).toContain("rejected"); + // Discarding drops the change from the local observation, so a later edit never resends it. + queue.discard(refused); + expect(queue.unsent.length).toBe(0); + expect(ids(queue.localDocument)).toEqual(["hills"]); + queue.record(doc(grow(queue.localDocument.recipe, "c", 9))); + await queue.flush(); + expect(ids(server.document).sort()).toEqual(["c", "hills"]); + }); + + it("does not resend a discarded edit in a later whole-document save", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + server.reject(2); + queue.record(doc(grow(server.document.recipe, "a", 5))); + await queue.flush(); + expect(queue.unsent.length).toBe(1); + queue.discard(editFor(queue, "a")); + const local = structuredClone(queue.localDocument.recipe); + local.config = { ...local.config, seed: 999 }; + queue.record(doc(local)); + await queue.flush(); + expect(ids(server.document)).toEqual(["hills"]); + expect(server.document.recipe.config.seed).toBe(999); + expect(queue.unsent.length).toBe(0); + }); + + it("persists a refused edit when the user retries it", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + server.reject(2); + queue.record(doc(grow(server.document.recipe, "a", 5))); + await queue.flush(); + expect(queue.unsent.length).toBe(1); + const refused = queue.unsent[0]; + if (!refused) throw new Error("expected a refused edit"); + queue.retry(refused); + await queue.flush(); + expect(ids(server.document).sort()).toEqual(["a", "hills"]); + expect(queue.unsent.length).toBe(0); + }); + + it("merges an unrelated external change when it rebases a conflicted save", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + server.hold(1); + queue.record(doc(grow(server.document.recipe, "a", 5))); + expect(server.commits.length).toBe(1); + server.externalAdd("b"); + server.release(); + await queue.flush(); + expect(ids(server.document).sort()).toEqual(["a", "b", "hills"]); + expect(server.commits.length).toBe(2); + }); + + it("keeps local config, layer and asset changes over unrelated external edits", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + const localRecipe = structuredClone(server.document.recipe); + localRecipe.config = { ...localRecipe.config, seed: 999 }; + const local = { ...grow(localRecipe, "a", 5), assets: [{ id: "tex", kind: "image" }] }; + server.hold(1); + queue.record(doc(local)); + expect(server.commits.length).toBe(1); + server.externalAdd("b"); + server.externalMetadata({ note: "external" }); + server.release(); + await queue.flush(); + const disk = server.document; + expect(disk.recipe.config.seed).toBe(999); + expect(ids(disk).sort()).toEqual(["a", "b", "hills"]); + expect(disk.recipe.assets).toEqual([{ id: "tex", kind: "image" }]); + expect(disk.note).toBe("external"); + }); + + it("keeps local edits when an external snapshot arrives during a held save", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + server.hold(1); + let local = grow(server.document.recipe, "a", 5); + queue.record(doc(local)); + expect(server.commits.length).toBe(1); + server.externalAdd("b"); + queue.adopt(server.snapshot()); + local = grow(local, "c", 7); + queue.record(doc(local)); + server.release(); + await queue.flush(); + expect(ids(server.document).sort()).toEqual(["a", "b", "c", "hills"]); + expect(queue.unsent.length).toBe(0); + }); + + it("sends every layer mutation route", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + let local = grow(server.document.recipe, "a", 5); + queue.record(doc(local)); + await queue.flush(); + expect(ids(server.document)).toContain("a"); + local = Terrain.fromJSON(local as never) + .update("hills", { params: { amplitude: 42 } }) + .toJSON(); + queue.record(doc(local)); + await queue.flush(); + expect(server.document.recipe.layers.find((l) => l.id === "hills")?.params?.amplitude).toBe(42); + local = Terrain.fromJSON(local as never) + .toggle("hills", false) + .toJSON(); + queue.record(doc(local)); + await queue.flush(); + expect(server.document.recipe.layers.find((l) => l.id === "hills")?.enabled).toBe(false); + local = Terrain.fromJSON(local as never) + .move("a", 0) + .toJSON(); + queue.record(doc(local)); + await queue.flush(); + expect(ids(server.document)[0]).toBe("a"); + local = Terrain.fromJSON(local as never) + .remove("a") + .toJSON(); + queue.record(doc(local)); + await queue.flush(); + expect(ids(server.document)).toEqual(["hills"]); + }); + + it("sends a config change as the whole document", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + const next = structuredClone(server.document.recipe); + next.config = { ...next.config, seed: 999 }; + queue.record(doc(next)); + await queue.flush(); + expect(server.document.recipe.config.seed).toBe(999); + const first = server.commits[0]; + if (!first) throw new Error("expected a commit"); + expect(first.document).toBeTruthy(); + expect(first.commands).toBeUndefined(); + expect(ids(server.document)).toEqual(["hills"]); + }); + + it("sends a recipe asset change as the whole document", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + const next = structuredClone(server.document.recipe); + next.assets = [{ id: "tex", kind: "image" }]; + queue.record(doc(next)); + await queue.flush(); + expect(server.document.recipe.assets).toEqual([{ id: "tex", kind: "image" }]); + expect(ids(server.document)).toEqual(["hills"]); + }); + + it("deletes a changed existing layer removed before its save, and cancels a new one", () => { + const changed = { id: "hills", enabled: true, params: { amplitude: 99 } } as SaveLayer; + expect( + coalesceCommands( + [ + { op: "upsert", layer: changed }, + { op: "remove", id: "hills" }, + ] as unknown as PatchCommand[], + new Set(["hills"]), + ), + ).toEqual([{ op: "remove", id: "hills" }]); + expect( + coalesceCommands( + [ + { op: "upsert", layer: { id: "new" } as SaveLayer }, + { op: "remove", id: "new" }, + ] as unknown as PatchCommand[], + new Set(["hills"]), + ), + ).toEqual([]); + }); + + it("diffs layers into order-preserving commands", () => { + const base = [{ id: "a" }, { id: "b" }] as unknown as SaveLayer[]; + const next = [{ id: "b" }, { id: "a" }, { id: "c" }] as unknown as SaveLayer[]; + const commands = diffLayers(base, next); + expect(commands).toContainEqual({ op: "upsert", layer: { id: "c" } }); + expect(commands.filter((command) => command.op === "move")).toEqual([ + { op: "move", id: "b", index: 0 }, + { op: "move", id: "a", index: 1 }, + { op: "move", id: "c", index: 2 }, + ]); + }); + + it("moves a layer inserted before an existing layer onto disk in order", () => { + const baseRecipe = new Terrain({ size: 512, resolution: 129, seed: 73 }) + .noise({ id: "a", amplitude: 1 }) + .noise({ id: "b", amplitude: 2 }) + .toJSON(); + const inserted = Terrain.fromJSON(baseRecipe as never) + .noise({ id: "c", amplitude: 3 }) + .move("c", 1); + const commands = diffLayers( + baseRecipe.layers as unknown as SaveLayer[], + inserted.toJSON().layers as unknown as SaveLayer[], + ); + const applied = Terrain.fromJSON(baseRecipe as never); + applied.applyPatch(commands as never); + expect(applied.toJSON().layers.map((layer) => layer.id)).toEqual(["a", "c", "b"]); + }); + + it("emits a reorder for a removed then re-added layer with the same id", () => { + const baseRecipe = new Terrain({ size: 512, resolution: 129, seed: 73 }) + .noise({ id: "a", amplitude: 1 }) + .noise({ id: "b", amplitude: 2 }) + .noise({ id: "c", amplitude: 3 }) + .toJSON(); + const layerB = baseRecipe.layers.find((layer) => layer.id === "b"); + if (!layerB) throw new Error("expected layer b"); + const readded = Terrain.fromJSON(baseRecipe as never) + .remove("b") + .add(structuredClone(layerB)); + const commands = diffLayers( + baseRecipe.layers as unknown as SaveLayer[], + readded.toJSON().layers as unknown as SaveLayer[], + ); + const applied = Terrain.fromJSON(baseRecipe as never); + applied.applyPatch(commands as never); + expect(applied.toJSON().layers.map((layer) => layer.id)).toEqual(["a", "c", "b"]); + }); + + it("coalesces a toggle after a layer change without dropping the change", () => { + const changed = { id: "hills", enabled: true, params: { amplitude: 99 } } as SaveLayer; + expect( + coalesceCommands([ + { op: "upsert", layer: changed }, + { op: "toggle", id: "hills", enabled: false }, + ] as unknown as PatchCommand[]), + ).toEqual([{ op: "upsert", layer: { ...changed, enabled: false } }]); + }); + + function editFor(queue: SaveQueue, layerId: string) { + const edit = queue.unsent.find((item) => + item.commands.some((command) => command.op === "upsert" && command.layer.id === layerId), + ); + if (!edit) throw new Error(`expected a refused edit that adds ${layerId}`); + return edit; + } + + it("keeps a second refused edit when the first is discarded", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + server.reject(2); + queue.record(doc(grow(server.document.recipe, "a", 5))); + await queue.flush(); + server.reject(2); + queue.record(doc(grow(queue.localDocument.recipe, "b", 7))); + await queue.flush(); + expect(queue.unsent.length).toBe(2); + queue.discard(editFor(queue, "a")); + expect(queue.unsent.length).toBe(1); + expect(ids(queue.localDocument).sort()).toEqual(["b", "hills"]); + }); + + it("keeps an in-flight edit when an earlier refused edit is discarded, then acks only it", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + server.reject(2); + queue.record(doc(grow(server.document.recipe, "a", 5))); + await queue.flush(); + expect(queue.unsent.length).toBe(1); + server.hold(1); + queue.record(doc(grow(queue.localDocument.recipe, "b", 7))); + expect(queue.hasLocalIntent()).toBe(true); + queue.discard(editFor(queue, "a")); + expect(ids(queue.localDocument).sort()).toEqual(["b", "hills"]); + server.release(); + await queue.flush(); + expect(ids(server.document).sort()).toEqual(["b", "hills"]); + expect(queue.unsent.length).toBe(0); + expect(ids(queue.localDocument).sort()).toEqual(["b", "hills"]); + const commits = server.commits.length; + await queue.flush(); + expect(server.commits.length).toBe(commits); + }); + + it("defers an external snapshot while any local intent exists", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + server.hold(1); + queue.record(doc(grow(server.document.recipe, "a", 5))); + expect(externalSnapshotAction(queue, server.snapshot())).toBe("defer"); + server.release(); + await queue.flush(); + server.reject(2); + queue.record(doc(grow(server.document.recipe, "b", 7))); + await queue.flush(); + expect(queue.unsent.length).toBe(1); + expect(externalSnapshotAction(queue, server.snapshot())).toBe("defer"); + queue.discard(editFor(queue, "b")); + expect(externalSnapshotAction(queue, { ...server.snapshot(), revision: "newer" })).toBe( + "apply", + ); + }); + + it("keeps remote fields and layers through a 409 rebase and a later whole save", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + server.hold(1); + const first = grow(server.document.recipe, "a", 5); + queue.record(doc(first)); + expect(server.commits.length).toBe(1); + const config = structuredClone(first); + config.config = { ...config.config, seed: 999 }; + queue.record(doc(config)); + // While the first commit waits, a remote writer adds a layer, a config field and an asset. + server.externalAdd("b"); + const remote = server.document.recipe; + remote.config = { ...remote.config, size: 1024 }; + server.externalMetadata({ recipe: remote, assets: [{ id: "remote-tex", kind: "image" }] }); + server.release(); + await queue.flush(); + expect(server.commits.length).toBe(3); + const sent = server.commits[2]?.document; + if (!sent) throw new Error("expected a commit for the second intent"); + for (const document_ of [sent, server.document, queue.base.document, queue.localDocument]) { + expect(ids(document_).sort()).toEqual(["a", "b", "hills"]); + expect(document_.recipe.config).toMatchObject({ size: 1024, seed: 999 }); + expect(document_.assets).toEqual([{ id: "remote-tex", kind: "image" }]); + } + expect(queue.unsent.length).toBe(0); + }); + + it("refuses a queued toggle of a layer an external writer removed, then settles", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + server.hold(1); + queue.record(doc(grow(server.document.recipe, "a", 5))); + expect(server.commits.length).toBe(1); + const next = structuredClone(queue.localDocument.recipe); + next.config = { ...next.config, seed: 999 }; + queue.record( + doc( + Terrain.fromJSON(next as never) + .toggle("hills", false) + .toJSON(), + ), + ); + // The external writer removes "hills" while the first commit waits. + // The 409 rebase still adds "a". The queued toggle of "hills" then targets a missing layer. + server.externalRemove("hills"); + server.release(); + await queue.flush(); + expect(queue.hasPending()).toBe(false); + expect(queue.unsent.length).toBe(1); + const refused = queue.unsent[0]; + if (!refused) throw new Error("expected a refused edit"); + expect(refused.label).toContain("rejected"); + expect(server.commits.length).toBe(2); + expect(ids(server.document)).toEqual(["a"]); + expect(server.document.recipe.config.seed).toBe(73); + }); +}); diff --git a/packages/terrain/src/editor/saveQueue.ts b/packages/terrain/src/editor/saveQueue.ts new file mode 100644 index 000000000..07f1143bc --- /dev/null +++ b/packages/terrain/src/editor/saveQueue.ts @@ -0,0 +1,450 @@ +import { type PatchCommand, Terrain } from "../core/terrain.js"; + +/** The document fields the save queue compares; validated deeply by the server, read shallowly here. */ +export interface ISaveRecipe { + config: Record; + layers: SaveLayer[]; + [key: string]: unknown; +} +export interface SaveLayer { + id: string; + enabled?: boolean; + params?: Record; + [key: string]: unknown; +} +export interface ISaveDocument { + version: 1; + recipe: ISaveRecipe; + [key: string]: unknown; +} +export interface ISaveSnapshot { + revision: string; + document: ISaveDocument; + diagnostic?: string | null; +} +/** + * One local intent in the ordered journal: the change from the document shown just before it + * (`baseline`) to the document shown just after (`target`), plus the coalesced commands when a + * plain layer diff expresses it. A refused intent keeps its exact change so it can be retried. + */ +export interface IUnsentEdit { + id: string; + label: string; + error: string; + commands: PatchCommand[]; + document: ISaveDocument | null; +} +interface ISaveIntent extends IUnsentEdit { + baseline: ISaveDocument; + target: ISaveDocument; + wholeDocument: boolean; + state: "queued" | "inflight" | "rejected"; +} +export interface ISaveTransaction { + baseRevision: string; + commands?: readonly PatchCommand[]; + document?: ISaveDocument; +} +/** What an external snapshot (reload, restore, SSE) does to the local view. */ +export type ExternalSnapshotAction = "ignore" | "defer" | "adopt" | "apply"; + +/** + * The net layer commands between two recipes. A layer only present in the next recipe is a new + * `upsert`; one changed in place keeps every field through an `upsert`; a reorder emits one `move` + * per layer so the server sees the exact stack order. `existingIds` is what tells a later + * `remove` apart from an undone brand-new layer during coalescing. + * + * The order comparison uses the disk order the upserts and removes will produce — base layers + * that survive, then brand-new layers appended in next order — so a layer inserted before an + * existing one still gets its moves and is not silently appended to the end. + */ +export function diffLayers(baseLayers: SaveLayer[], nextLayers: SaveLayer[]): PatchCommand[] { + const baseMap = new Map((baseLayers ?? []).map((layer) => [layer.id, layer])); + const nextMap = new Map((nextLayers ?? []).map((layer) => [layer.id, layer])); + const commands: PatchCommand[] = []; + for (const id of baseMap.keys()) if (!nextMap.has(id)) commands.push({ op: "remove", id }); + for (const layer of nextLayers ?? []) { + const before = baseMap.get(layer.id); + if (!before) { + commands.push({ op: "upsert", layer: layer as never }); + } else if (JSON.stringify(before) !== JSON.stringify(layer)) { + const onlyToggle = + before.enabled !== layer.enabled && + JSON.stringify({ ...before, enabled: layer.enabled }) === JSON.stringify(layer); + commands.push( + onlyToggle + ? { op: "toggle", id: layer.id, enabled: layer.enabled } + : { op: "upsert", layer: layer as never }, + ); + } + } + const projected = [ + ...(baseLayers ?? []).filter((layer) => nextMap.has(layer.id)).map((layer) => layer.id), + ...(nextLayers ?? []).filter((layer) => !baseMap.has(layer.id)).map((layer) => layer.id), + ]; + if (JSON.stringify(projected) !== JSON.stringify((nextLayers ?? []).map((layer) => layer.id))) + for (const [index, layer] of (nextLayers ?? []).entries()) + commands.push({ op: "move", id: layer.id, index }); + return commands; +} + +/** + * Fold a command stream into the smallest equivalent batch. A changed existing layer arrives as + * `upsert` (a full replace); if a later `remove` follows it, the disk layer must still be deleted — + * `existingIds` carries which layers the base document owns. Two edits to the same layer merge; an + * undo of a brand-new layer cancels both commands. `move` and any other command pass through in + * order, because the layer diff emits full `upsert`/`toggle` commands and never a partial `update`. + */ +export function coalesceCommands( + commands: readonly PatchCommand[], + existingIds: ReadonlySet = new Set(), +): PatchCommand[] { + const layerMap = new Map(); + const others: PatchCommand[] = []; + for (const command of commands) { + if (command.op === "upsert") { + layerMap.set(command.layer.id, { op: "upsert", layer: command.layer }); + } else if (command.op === "remove") { + const current = layerMap.get(command.id); + if (current?.op === "upsert") + if (existingIds.has(command.id)) layerMap.set(command.id, { op: "remove", id: command.id }); + else layerMap.delete(command.id); + else layerMap.set(command.id, { op: "remove", id: command.id }); + } else if (command.op === "toggle") { + const current = layerMap.get(command.id); + if (current?.op === "upsert") + layerMap.set(command.id, { + op: "upsert", + layer: { ...current.layer, enabled: command.enabled } as never, + }); + else layerMap.set(command.id, { op: "toggle", id: command.id, enabled: command.enabled }); + } else others.push(command); + } + return [...layerMap.values(), ...others]; +} + +function recipeOf(document: ISaveDocument | undefined): ISaveRecipe { + return (document?.recipe ?? { config: {}, layers: [] }) as ISaveRecipe; +} +function layersOf(document: ISaveDocument | undefined): SaveLayer[] { + return recipeOf(document).layers ?? []; +} +function withoutRecipe(document: ISaveDocument): Record { + const { recipe: _recipe, ...rest } = document; + return rest; +} +/** Every recipe field a patch command cannot express: config, assets and anything a game adds. */ +function withoutLayers(document: ISaveDocument): Record { + const { layers: _layers, ...rest } = recipeOf(document); + return rest; +} +function isPlainObject(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} +function same(left: unknown, right: unknown): boolean { + return JSON.stringify(left) === JSON.stringify(right); +} +function isConflict(error: unknown): boolean { + const status = (error as { status?: number } | undefined)?.status; + const message = error instanceof Error ? error.message : String(error); + return status === 409 || /(^|\D)409(\D|$)|Stale base revision/u.test(message); +} +function applyToRecipe(recipe: ISaveRecipe, commands: readonly PatchCommand[]): ISaveRecipe { + if (commands.length === 0) return structuredClone(recipe); + const terrain = Terrain.fromJSON(recipe as never); + terrain.applyPatch(commands as never); + return terrain.toJSON() as unknown as ISaveRecipe; +} + +/** + * Three-way merge of plain JSON. Objects merge key by key; arrays and primitives are leaves, so a + * field the local user changed wins, and a field only the disk changed keeps the disk value. This + * is what lets a whole-document save rebase onto a fresh revision without dropping an unrelated + * external edit. + */ +function threeWayMerge(base: unknown, local: unknown, remote: unknown): unknown { + if (isPlainObject(base) || isPlainObject(local) || isPlainObject(remote)) { + const b = isPlainObject(base) ? base : {}; + const l = isPlainObject(local) ? local : {}; + const r = isPlainObject(remote) ? remote : {}; + const merged: Record = {}; + for (const key of new Set([...Object.keys(b), ...Object.keys(l), ...Object.keys(r)])) + merged[key] = threeWayMerge(b[key], l[key], r[key]); + return merged; + } + return same(local, base) ? remote : local; +} + +/** + * Merge a whole-document intent (baseline -> target) onto the fresh disk document. Unchanged + * fields keep the fresh value; changed fields take the local value; the layer intent replays as + * commands so an external layer change survives alongside the local one. + */ +function mergeWholeDocument( + baseline: ISaveDocument, + target: ISaveDocument, + fresh: ISaveDocument, +): ISaveDocument { + const merged = threeWayMerge(baseline, target, fresh) as ISaveDocument; + const commands = diffLayers(layersOf(baseline), layersOf(target)); + if (commands.length) { + const terrain = Terrain.fromJSON(recipeOf(fresh) as never); + terrain.applyPatch(commands as never); + merged.recipe = { ...merged.recipe, layers: terrain.layers as never }; + } + return merged; +} + +/** + * Decide what an external snapshot does to the editor. A diagnostic snapshot is ignored here (the + * caller shows the banner). Any local intent — queued, in flight or refused — defers the snapshot + * so a reload, restore or SSE event can never roll local work back; an unchanged revision only + * needs the queue to adopt it, a newer one also reloads the preview. + */ +export function externalSnapshotAction( + queue: SaveQueue, + snapshot: ISaveSnapshot, +): ExternalSnapshotAction { + if (snapshot.diagnostic) return "ignore"; + if (queue.hasLocalIntent()) return "defer"; + return snapshot.revision === queue.revision ? "adopt" : "apply"; +} + +/** + * Client-side ordered intent journal for the project terrain document. + * + * The queue owns one journal of local intents and one confirmed disk snapshot. Each `record` diffs + * the new observation against the last displayed document and appends one intent; the displayed + * document is always the confirmed snapshot replayed with every intent still in the journal. A + * commit acknowledges exactly one intent, so a second stroke, a refusal or a discard never drops a + * sibling: discarding a refused edit removes only that intent and replays the rest. A 409 rebases + * the intent on the fresh revision and retries once (a command batch replays its layer commands, a + * whole-document batch merges only the fields it changed). A refusal stays visible for retry or + * discard. A metadata lane hands back its own committed document through `adopt`. + * + * @example const queue = new SaveQueue({ base, commit, getSnapshot }); queue.record(nextDocument); + */ +export class SaveQueue { + #commit: (transaction: ISaveTransaction) => Promise; + #getSnapshot: () => Promise; + #onSaved: ((snapshot: ISaveSnapshot, local: ISaveDocument) => void) | undefined; + #onNotify: ((message: string) => void) | undefined; + #onChange: (() => void) | undefined; + #confirmed: ISaveSnapshot; + #journal: ISaveIntent[] = []; + #saving = false; + #flushing: Promise = Promise.resolve(); + + constructor(options: { + base: ISaveSnapshot; + commit: (transaction: ISaveTransaction) => Promise; + getSnapshot: () => Promise; + onSaved?: (snapshot: ISaveSnapshot, local: ISaveDocument) => void; + onNotify?: (message: string) => void; + onChange?: () => void; + }) { + this.#confirmed = options.base; + this.#commit = options.commit; + this.#getSnapshot = options.getSnapshot; + this.#onSaved = options.onSaved; + this.#onNotify = options.onNotify; + this.#onChange = options.onChange; + } + + get base(): ISaveSnapshot { + return this.#confirmed; + } + get revision(): string { + return this.#confirmed.revision; + } + /** The local document as the queue sees it: the confirmed snapshot with every intent replayed. */ + get localDocument(): ISaveDocument { + return this.#replay(); + } + /** Refused intents, kept visible with retry and discard. */ + get unsent(): readonly IUnsentEdit[] { + return this.#journal.filter((intent) => intent.state === "rejected"); + } + get saving(): boolean { + return this.#saving; + } + hasPending(): boolean { + return this.#journal.some((intent) => intent.state === "queued" || intent.state === "inflight"); + } + /** True while any local intent exists — queued, in flight or refused — or a save is running. */ + hasLocalIntent(): boolean { + return this.#saving || this.#journal.length > 0; + } + + /** + * Record one local observation. The intent diffs the new document against the last displayed + * local document — the previous intent's target, or the confirmed snapshot when the journal is + * empty — never against the disk, so a second edit before the first save answers is queued rather + * than reverted. While one queued whole-document intent is pending every later record extends it, + * so a config change and the strokes after it still reach disk in one save. + */ + record(document: ISaveDocument): void { + const last = this.#journal[this.#journal.length - 1]; + if (last && last.state === "queued" && last.wholeDocument) { + if (same(last.target, document)) return; + last.target = structuredClone(document); + last.document = structuredClone(document); + this.#onChange?.(); + void this.flush(); + return; + } + const baseline = last ? last.target : this.#confirmed.document; + if (same(baseline, document)) return; + const whole = + !same(withoutLayers(baseline), withoutLayers(document)) || + !same(withoutRecipe(baseline), withoutRecipe(document)); + const commands = whole ? [] : diffLayers(layersOf(baseline), layersOf(document)); + if (!whole && commands.length === 0) return; + this.#journal.push({ + id: `intent-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, + label: `${whole ? 1 : commands.length} edit(s)`, + error: "", + commands, + document: whole ? structuredClone(document) : null, + baseline: structuredClone(baseline), + target: structuredClone(document), + wholeDocument: whole, + state: "queued", + }); + this.#onChange?.(); + void this.flush(); + } + + /** + * Adopt a snapshot the queue did not request (SSE, reload, restore or a metadata lane commit). + * The base revision always follows disk; the displayed document stays the confirmed snapshot + * replayed with any local intent, so nothing in flight is dropped. + */ + adopt(snapshot: ISaveSnapshot): void { + this.#confirmed = snapshot; + this.#onChange?.(); + } + + /** Re-queue a refused intent in place, so retry keeps its order in the journal. */ + retry(edit: IUnsentEdit): void { + const intent = this.#intent(edit.id); + if (!intent) return; + intent.state = "queued"; + intent.error = ""; + this.#onChange?.(); + void this.flush(); + } + + /** Drop one refused intent and replay the rest, so a later save never resends it. */ + discard(edit: IUnsentEdit): void { + this.#journal = this.#journal.filter((intent) => intent.id !== edit.id); + this.#onChange?.(); + void this.flush(); + } + + async flush(): Promise { + if (this.#saving) return this.#flushing; + if (!this.hasPending()) return; + this.#saving = true; + this.#flushing = this.#run(); + return this.#flushing; + } + + #intent(id: string): ISaveIntent | undefined { + return this.#journal.find((intent) => intent.id === id); + } + + /** The confirmed snapshot with every journal intent replayed in order. */ + #replay(): ISaveDocument { + let document = structuredClone(this.#confirmed.document); + for (const intent of this.#journal) { + try { + document = this.#apply(document, intent); + } catch (error) { + // A rejected sibling can make a dependent intent un-replayable: keep it visible instead + // of silently dropping it, so the user can retry or discard it. + if (intent.state !== "rejected") this.#reject(intent, error); + } + } + return document; + } + + #apply(document: ISaveDocument, intent: ISaveIntent): ISaveDocument { + if (intent.wholeDocument) return mergeWholeDocument(intent.baseline, intent.target, document); + const recipe = applyToRecipe(recipeOf(document), intent.commands); + return { ...document, recipe }; + } + + #reject(intent: ISaveIntent, error: unknown): void { + intent.state = "rejected"; + intent.error = error instanceof Error ? error.message : String(error); + intent.label = `${intent.wholeDocument ? 1 : intent.commands.length} edit(s) rejected: ${intent.error}`; + this.#onNotify?.(intent.error); + } + + #transaction(intent: ISaveIntent, snapshot: ISaveSnapshot): ISaveTransaction { + if (intent.wholeDocument) + return { + baseRevision: snapshot.revision, + document: mergeWholeDocument(intent.baseline, intent.target, snapshot.document), + }; + return { baseRevision: snapshot.revision, commands: intent.commands }; + } + + #rebase(intent: ISaveIntent, snapshot: ISaveSnapshot): ISaveTransaction { + if (intent.wholeDocument) + return { + baseRevision: snapshot.revision, + document: mergeWholeDocument(intent.baseline, intent.target, snapshot.document), + }; + const recipe = applyToRecipe(recipeOf(snapshot.document), intent.commands); + return { + baseRevision: snapshot.revision, + commands: diffLayers(layersOf(snapshot.document), recipe.layers ?? []), + }; + } + + async #run(): Promise { + try { + for (;;) { + const intent = this.#journal.find((entry) => entry.state === "queued"); + if (!intent) break; + intent.state = "inflight"; + this.#onChange?.(); + let transaction: ISaveTransaction; + try { + transaction = this.#transaction(intent, this.#confirmed); + } catch (error) { + this.#reject(intent, error); + this.#onChange?.(); + continue; + } + for (let attempt = 1; attempt <= 2; attempt++) { + try { + const next = await this.#commit(transaction); + this.#confirmed = next; + this.#journal = this.#journal.filter((entry) => entry.id !== intent.id); + this.#onSaved?.(next, this.localDocument); + this.#onChange?.(); + break; + } catch (error) { + if (isConflict(error) && attempt === 1) { + try { + const fresh = await this.#getSnapshot(); + this.#confirmed = fresh; + transaction = this.#rebase(intent, fresh); + continue; + } catch {} + } + this.#reject(intent, error); + this.#onChange?.(); + break; + } + } + } + } finally { + this.#saving = false; + } + if (this.hasPending()) void this.flush(); + } +} From f95fc72a1d39966e003b788bd54646e014a87531 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 14:45:43 -0700 Subject: [PATCH 021/210] fix(terrain): record intents against the displayed document --- ...-the-terrain-editor-never-drops-an-edit.md | 2 + .../__tests__/editor-save-queue.spec.ts | 94 +++++++++++-------- packages/terrain/src/editor/saveQueue.ts | 64 ++----------- 3 files changed, 64 insertions(+), 96 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md b/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md index b82a29717..753fe3e7e 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md @@ -75,3 +75,5 @@ The command queue, UI recovery controls and worker edits remain an uncommitted d Parent integration: the initial focused run loaded stale terrain output (7/9), then rebuilding exposed premature recovery callbacks passed to the unchanged UI. Removed only those two mount bindings; the controller methods remain available. Rebuilt terrain successfully, including declarations/publint, then independently passed all 9 document recovery tests. UI bindings remain part of the isolated durability draft. 2026-10-10 follow-up: reproduced a queued whole-document save dropping an unrelated remote layer after an earlier 409 rebase, then fixed transaction construction to reuse the existing three-way merge. A real remote layer removal also reproduces an incompatible queued toggle; transaction-construction errors now retain that intent as refused and let the queue settle. Queue units are 22/22; focused strict source/spec typechecks pass. These checks cover the isolated queue only. Reload/restore response races, editable-terrain synchronisation, browser evidence and worker transfer/restart proof remain open. + +2026-10-10 queue review correction: both adopted-snapshot regressions fail against the prior queue (remote amplitude 3 replaces newer amplitude 11), including a queued config intent. Recording against the displayed confirmed snapshot plus replayed journal preserves remote ownership; later edits remain separate intents. Removed the unused command coalescer. Independently reran queue units (22/22), focused strict source/spec typecheck and Biome (pass with existing complexity warnings). Browser wiring and runtime acceptance remain open. diff --git a/packages/terrain/__tests__/editor-save-queue.spec.ts b/packages/terrain/__tests__/editor-save-queue.spec.ts index 49d474a69..0ca83422e 100644 --- a/packages/terrain/__tests__/editor-save-queue.spec.ts +++ b/packages/terrain/__tests__/editor-save-queue.spec.ts @@ -1,12 +1,6 @@ -import type { PatchCommand } from "@threenative/terrain"; import { Terrain } from "@threenative/terrain"; import { describe, expect, it } from "vitest"; -import { - SaveQueue, - coalesceCommands, - diffLayers, - externalSnapshotAction, -} from "../src/editor/saveQueue.js"; +import { SaveQueue, diffLayers, externalSnapshotAction } from "../src/editor/saveQueue.js"; import type { ISaveDocument, ISaveSnapshot, @@ -71,6 +65,13 @@ function makeServer(initial: ISaveDocument) { current = { ...current, recipe: terrain.toJSON() as never }; revision = `r${++counter}`; }, + externalAmplitude(id: string, amplitude: number) { + const terrain = Terrain.fromJSON(current.recipe as never).update(id, { + params: { amplitude }, + }); + current = { ...current, recipe: terrain.toJSON() as never }; + revision = `r${++counter}`; + }, externalMetadata(patch: Record) { current = { ...current, ...patch }; revision = `r${++counter}`; @@ -144,7 +145,8 @@ describe("terrain editor save queue", () => { queue.record(doc(layered)); server.release(); await queue.flush(); - expect(server.commits.length).toBe(2); + // The stroke after a queued config save is its own save now, so the config save is not extended. + expect(server.commits.length).toBe(3); expect(ids(server.document).sort()).toEqual(["a", "b", "hills"]); expect(server.document.recipe.config.seed).toBe(999); expect(queue.unsent.length).toBe(0); @@ -257,7 +259,7 @@ describe("terrain editor save queue", () => { expect(server.commits.length).toBe(1); server.externalAdd("b"); queue.adopt(server.snapshot()); - local = grow(local, "c", 7); + local = grow(queue.localDocument.recipe, "c", 7); queue.record(doc(local)); server.release(); await queue.flush(); @@ -265,6 +267,48 @@ describe("terrain editor save queue", () => { expect(queue.unsent.length).toBe(0); }); + it("keeps the latest remote amplitude when a later edit follows an adopted snapshot", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + server.hold(1); + queue.record(doc(grow(server.document.recipe, "a", 5))); + expect(server.commits.length).toBe(1); + server.externalAdd("b"); + queue.adopt(server.snapshot()); + queue.record(doc(grow(queue.localDocument.recipe, "c", 7))); + // The user never edited "b", so the remote amplitude must survive the held save's rebase. + server.externalAmplitude("b", 11); + server.release(); + await queue.flush(); + expect(ids(server.document).sort()).toEqual(["a", "b", "c", "hills"]); + expect(server.document.recipe.layers.find((layer) => layer.id === "b")?.params?.amplitude).toBe( + 11, + ); + expect(queue.unsent.length).toBe(0); + }); + + it("does not make a remote layer local-owned through a config intent queued before adopt", async () => { + const server = makeServer(doc(recipe())); + const queue = makeQueue(server); + server.hold(1); + queue.record(doc(grow(server.document.recipe, "a", 5))); + const config = structuredClone(queue.localDocument.recipe); + config.config = { ...config.config, seed: 999 }; + queue.record(doc(config)); + server.externalAdd("b"); + queue.adopt(server.snapshot()); + queue.record(doc(grow(queue.localDocument.recipe, "c", 7))); + server.externalAmplitude("b", 11); + server.release(); + await queue.flush(); + expect(ids(server.document).sort()).toEqual(["a", "b", "c", "hills"]); + expect(server.document.recipe.layers.find((layer) => layer.id === "b")?.params?.amplitude).toBe( + 11, + ); + expect(server.document.recipe.config.seed).toBe(999); + expect(queue.unsent.length).toBe(0); + }); + it("sends every layer mutation route", async () => { const server = makeServer(doc(recipe())); const queue = makeQueue(server); @@ -324,28 +368,6 @@ describe("terrain editor save queue", () => { expect(ids(server.document)).toEqual(["hills"]); }); - it("deletes a changed existing layer removed before its save, and cancels a new one", () => { - const changed = { id: "hills", enabled: true, params: { amplitude: 99 } } as SaveLayer; - expect( - coalesceCommands( - [ - { op: "upsert", layer: changed }, - { op: "remove", id: "hills" }, - ] as unknown as PatchCommand[], - new Set(["hills"]), - ), - ).toEqual([{ op: "remove", id: "hills" }]); - expect( - coalesceCommands( - [ - { op: "upsert", layer: { id: "new" } as SaveLayer }, - { op: "remove", id: "new" }, - ] as unknown as PatchCommand[], - new Set(["hills"]), - ), - ).toEqual([]); - }); - it("diffs layers into order-preserving commands", () => { const base = [{ id: "a" }, { id: "b" }] as unknown as SaveLayer[]; const next = [{ id: "b" }, { id: "a" }, { id: "c" }] as unknown as SaveLayer[]; @@ -395,16 +417,6 @@ describe("terrain editor save queue", () => { expect(applied.toJSON().layers.map((layer) => layer.id)).toEqual(["a", "c", "b"]); }); - it("coalesces a toggle after a layer change without dropping the change", () => { - const changed = { id: "hills", enabled: true, params: { amplitude: 99 } } as SaveLayer; - expect( - coalesceCommands([ - { op: "upsert", layer: changed }, - { op: "toggle", id: "hills", enabled: false }, - ] as unknown as PatchCommand[]), - ).toEqual([{ op: "upsert", layer: { ...changed, enabled: false } }]); - }); - function editFor(queue: SaveQueue, layerId: string) { const edit = queue.unsent.find((item) => item.commands.some((command) => command.op === "upsert" && command.layer.id === layerId), diff --git a/packages/terrain/src/editor/saveQueue.ts b/packages/terrain/src/editor/saveQueue.ts index 07f1143bc..9711646b1 100644 --- a/packages/terrain/src/editor/saveQueue.ts +++ b/packages/terrain/src/editor/saveQueue.ts @@ -6,12 +6,12 @@ export interface ISaveRecipe { layers: SaveLayer[]; [key: string]: unknown; } -export interface SaveLayer { +export type SaveLayer = { id: string; enabled?: boolean; params?: Record; [key: string]: unknown; -} +}; export interface ISaveDocument { version: 1; recipe: ISaveRecipe; @@ -24,7 +24,7 @@ export interface ISaveSnapshot { } /** * One local intent in the ordered journal: the change from the document shown just before it - * (`baseline`) to the document shown just after (`target`), plus the coalesced commands when a + * (`baseline`) to the document shown just after (`target`), plus the layer commands when a * plain layer diff expresses it. A refused intent keeps its exact change so it can be retried. */ export interface IUnsentEdit { @@ -51,8 +51,7 @@ export type ExternalSnapshotAction = "ignore" | "defer" | "adopt" | "apply"; /** * The net layer commands between two recipes. A layer only present in the next recipe is a new * `upsert`; one changed in place keeps every field through an `upsert`; a reorder emits one `move` - * per layer so the server sees the exact stack order. `existingIds` is what tells a later - * `remove` apart from an undone brand-new layer during coalescing. + * per layer so the server sees the exact stack order. * * The order comparison uses the disk order the upserts and removes will produce — base layers * that survive, then brand-new layers appended in next order — so a layer inserted before an @@ -88,41 +87,6 @@ export function diffLayers(baseLayers: SaveLayer[], nextLayers: SaveLayer[]): Pa return commands; } -/** - * Fold a command stream into the smallest equivalent batch. A changed existing layer arrives as - * `upsert` (a full replace); if a later `remove` follows it, the disk layer must still be deleted — - * `existingIds` carries which layers the base document owns. Two edits to the same layer merge; an - * undo of a brand-new layer cancels both commands. `move` and any other command pass through in - * order, because the layer diff emits full `upsert`/`toggle` commands and never a partial `update`. - */ -export function coalesceCommands( - commands: readonly PatchCommand[], - existingIds: ReadonlySet = new Set(), -): PatchCommand[] { - const layerMap = new Map(); - const others: PatchCommand[] = []; - for (const command of commands) { - if (command.op === "upsert") { - layerMap.set(command.layer.id, { op: "upsert", layer: command.layer }); - } else if (command.op === "remove") { - const current = layerMap.get(command.id); - if (current?.op === "upsert") - if (existingIds.has(command.id)) layerMap.set(command.id, { op: "remove", id: command.id }); - else layerMap.delete(command.id); - else layerMap.set(command.id, { op: "remove", id: command.id }); - } else if (command.op === "toggle") { - const current = layerMap.get(command.id); - if (current?.op === "upsert") - layerMap.set(command.id, { - op: "upsert", - layer: { ...current.layer, enabled: command.enabled } as never, - }); - else layerMap.set(command.id, { op: "toggle", id: command.id, enabled: command.enabled }); - } else others.push(command); - } - return [...layerMap.values(), ...others]; -} - function recipeOf(document: ISaveDocument | undefined): ISaveRecipe { return (document?.recipe ?? { config: {}, layers: [] }) as ISaveRecipe; } @@ -277,23 +241,13 @@ export class SaveQueue { } /** - * Record one local observation. The intent diffs the new document against the last displayed - * local document — the previous intent's target, or the confirmed snapshot when the journal is - * empty — never against the disk, so a second edit before the first save answers is queued rather - * than reverted. While one queued whole-document intent is pending every later record extends it, - * so a config change and the strokes after it still reach disk in one save. + * Record one local observation. The intent diffs the new document against the displayed local + * document: the confirmed snapshot replayed with the journal. It never diffs against the disk, so + * a second edit before the first save answers is queued rather than reverted. A remote change + * adopted since the last edit belongs to the baseline, so it never becomes a local intent. */ record(document: ISaveDocument): void { - const last = this.#journal[this.#journal.length - 1]; - if (last && last.state === "queued" && last.wholeDocument) { - if (same(last.target, document)) return; - last.target = structuredClone(document); - last.document = structuredClone(document); - this.#onChange?.(); - void this.flush(); - return; - } - const baseline = last ? last.target : this.#confirmed.document; + const baseline = this.localDocument; if (same(baseline, document)) return; const whole = !same(withoutLayers(baseline), withoutLayers(document)) || From a750a9a8b8662f437d42b1f36a2504da668e933e Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 15:11:35 -0700 Subject: [PATCH 022/210] perf(terrain): avoid numeric validation path allocations --- .../PRD-585-strata-proof-runs-in-ci.md | 2 ++ ...-the-terrain-editor-never-drops-an-edit.md | 2 ++ ...ls-closed-and-erodes-without-allocating.md | 8 ++++++- docs/PRDs/strata-terrain-hardening/README.md | 2 +- .../__tests__/terrain-consumer.spec.ts | 23 +++++++++++++++++++ packages/terrain/src/core/validation.ts | 15 +++++++++--- 6 files changed, 47 insertions(+), 5 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md index c8a01d9da..d3165600e 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-585-strata-proof-runs-in-ci.md @@ -70,3 +70,5 @@ Bootstrap repair (2026-10-10): generated projects now pin the shipped `pnpm@10.2 Parent bootstrap integration: all 68 scaffold tests pass on dde88e30c; scaffolder and assets builds pass, including declarations and package lint. The full consumer retry progressed beyond the earlier pnpm failure but stopped before gameplay: assets now depends on terrain, which the existing game-handoff tarball set does not supply, causing a registry 404. PRD-583 is wiring that local dependency closure. The game stage was not skipped; consumer and CI acceptance remain open. 2026-10-10 parent consumer repair verified: after integrating the PRD-583 terrain tarball/override closure and baking the required default forest fixture, the full unskipped consumer passes (exit 0). Packed-game scaffold, install, build and WebGPU playtest report NVIDIA Turing; the stock forest loader checks 4 820 placements with zero per-tree copies. This closes the consumer bootstrap failure; the separate showcase runtime budgets and real-CI acceptance remain open. + +2026-10-10 CI status explained: workflow `ci.yml` intentionally gates the scope job on `!github.event.pull_request.draft`. Exact candidate 5ad00f9bc therefore has skipped run 38088798294 and 33 skipped checks; the wait helper's exit 0 is not runtime evidence. The EPIC remains a draft while the full batch is implemented; real CI acceptance stays open until qualification. diff --git a/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md b/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md index 753fe3e7e..c3bda3c37 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-587-the-terrain-editor-never-drops-an-edit.md @@ -77,3 +77,5 @@ Parent integration: the initial focused run loaded stale terrain output (7/9), t 2026-10-10 follow-up: reproduced a queued whole-document save dropping an unrelated remote layer after an earlier 409 rebase, then fixed transaction construction to reuse the existing three-way merge. A real remote layer removal also reproduces an incompatible queued toggle; transaction-construction errors now retain that intent as refused and let the queue settle. Queue units are 22/22; focused strict source/spec typechecks pass. These checks cover the isolated queue only. Reload/restore response races, editable-terrain synchronisation, browser evidence and worker transfer/restart proof remain open. 2026-10-10 queue review correction: both adopted-snapshot regressions fail against the prior queue (remote amplitude 3 replaces newer amplitude 11), including a queued config intent. Recording against the displayed confirmed snapshot plus replayed journal preserves remote ownership; later edits remain separate intents. Removed the unused command coalescer. Independently reran queue units (22/22), focused strict source/spec typecheck and Biome (pass with existing complexity warnings). Browser wiring and runtime acceptance remain open. + +2026-10-10 parent queue integration: the adopted-remote-layer regression and queued-config variant now preserve the latest remote amplitude (11 rather than stale 3). Both queue commits are integrated locally; terrain build/declarations/publint and 45 queue/document/consumer tests pass (815 ms). The unused coalescer was removed. This proves the standalone journal, not the rendered editor; rapid strokes, retry/discard UI, reload races and real worker transfer/restart remain open. diff --git a/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md b/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md index 496379b2b..83a43cdd5 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md @@ -53,7 +53,7 @@ after on the same host. #### Phase 2: allocation-free loops **Status:** PARTIAL -**Files:** `packages/terrain/src/core/erosion.ts`, `world-package.ts` +**Files:** `packages/terrain/src/core/erosion.ts`, `world-package.ts`, `validation.ts` - [ ] Same bytes, faster bake. proof: golden spec + timed bake — Golden hashes unchanged (`starter-kit.spec.ts` 9/9). Allocation removal landed: hydraulic reuses two probes instead of allocating an object twice per droplet step, thermal scans its four neighbours without a per-cell array, `splatBytes` precomputes the component index, and the hydraulic `spread` brush is flat typed arrays with an interior fast path. Timed bake fell 18.5 %, not the 30 % AC-5 requires, so this box stays open. Remaining: reach 30 % without changing output bytes. Verification (2026-10-10): the initial allocation-removal candidate passed 254 terrain tests and package/workspace typecheck. The final interior-offset candidate passes terrain build, the 27-test erosion/golden suite, terrain typecheck and focused Biome (three complexity warnings, no errors); the parent independently passes 18 bounds/golden tests. Phase 1 is done with actual uncapped red/capped green work-count proof. Phase 2 and AC-5 remain open: the earlier exploratory 18.5% result is below 30%, and final same-host 3+3 timing via `offload.sh run --quiet --no-sync` is queued behind the laptop CI runner’s shared quiet lock. No final speed claim is made. @@ -63,3 +63,9 @@ Bound repair plan: existing layer validation caps explicitly supplied droplets a Bound repair implemented (2026-10-10): `hydraulic` now distinguishes a supplied `droplets` (held to the 200000 recipe limit) from the automatic default (one droplet per cell, capped at the largest supported grid 1025²). `erosion-bounds.spec.ts` proves the 513/1025 defaults, the accepted 200000 limit, rejected 200001 and unsafe-integer counts, and equal exact capped work at 1025/2048 grids. 2026-10-10 parent candidate review: the bounded profiling arm timed out after saving erosion-loop and placement-record optimizations; no fallback was started. Rebuilt terrain passes publint and 33 focused erosion/defaults/spikes/world-package/starter tests, including all 20 pinned kit hashes. Scoped Biome passes with four complexity warnings. A Python wall-clock sample suggested 45.45%, but the prescribed GNU Time `-v` three-run rerun contradicts that: baseline 0.84 / 0.63 / 0.63 s versus candidate 0.52 / 0.53 / 0.56 s, 23.33% faster. All 21 forest files remain byte-identical. AC-5 and phase 2 stay open; no best-sample speed claim. Raw timing: `/tmp/strata592-time-v.rfj5oeor`. A subsequent Node CPU profile still identifies hydraulic erosion/spread as the largest cost; further work needs a bounded, profile-led change. + +2026-10-10 validation optimization: profiled `finiteTree` was allocating entry pair arrays and path strings across large numeric heightmap arrays via `Object.entries`. Optimized `finiteTree` in `packages/terrain/src/core/validation.ts` to iterate `Object.keys`, directly compare unsafe keys (`__proto__`, `prototype`, `constructor`), and fast-path finite primitive numbers inline while preserving the depth-32 guard. Added focused test cases in `terrain-consumer.spec.ts`. Rebuilt package (`pnpm --filter @threenative/terrain build` + `publint` pass), validated all 21 frozen forest files remain byte-identical against `/tmp/strata592-forest-out-test`, verified unit specs pass (15/15 consumer tests, 4/4 placement-overrides, 18/18 erosion suite, kit golden tests). AC-5 and phase 2 remain open pending full host 3+3 GNU Time benchmark. + +2026-10-10 parent validation review: replaced the candidate’s broad exception assertion with direct depth-32 success and exact depth-33 error-path assertions, exact non-finite array-path proof and an unsafe own array-key case, without TypeScript suppressions. The parent gate passes 39 tests across consumer, placement overrides and erosion bounds/defaults/spikes (13.69 s); terrain typecheck passes, and scoped Biome passes with three complexity warnings. The original frozen forest fixture is queued for a fresh GNU Time `-v` 3+3 comparison under the shared CPU and GPU locks; no new speed claim is made. + +2026-10-10 prescribed post-validation comparison: baseline 1.35 / 1.03 / 0.99 s versus candidate 0.70 / 0.86 / 1.05 s, 22.55% faster by arithmetic means. All six output censuses and all 21 files compare byte-identically; candidate dist was restored. Raw GNU Time `-v` output is in `artifacts/strata592-finite-bench-z1e8qf2c/`. Host load was 31.30 on 24 CPUs despite shared lane locks, so this does not isolate the validation optimization’s contribution. AC-5 and phase 2 remain open. Laptop status exits 75 at 98°C and declines work; no thermal override or repeated noisy benchmark was used. Re-plan: obtain a quiet same-host window before selecting another optimization from the profile. diff --git a/docs/PRDs/strata-terrain-hardening/README.md b/docs/PRDs/strata-terrain-hardening/README.md index ed6d3fa9a..84d11ed81 100644 --- a/docs/PRDs/strata-terrain-hardening/README.md +++ b/docs/PRDs/strata-terrain-hardening/README.md @@ -36,4 +36,4 @@ Current baseline: `pnpm build` and `pnpm check:docs` passed; six documentation c ## Active worker checkouts -PRD-592 validation and golden bytes are integrated at `6c40d8757`; its new candidate passes 33 focused tests and preserves all 21 forest files, but the prescribed same-host timing still reaches only 23.33%, so the 30% target remains open. PRD-585 local coverage and the scaffold package-manager pin are integrated; 68 scaffold tests pass, while real CI and runtime acceptance remain open. PRD-583 forest cook, alternate LODs and both isolated-install dependency closures are integrated through `38c83ef48`; parent assets build and 17 cook tests pass, and the unskipped packed-game consumer passes on NVIDIA Turing; layered-material visual proof remains pending. PRD-587 server recovery is integrated at `5b8fbfa60`; its queue remains under review for a baseline concurrency risk, with UI and worker proofs open. PRD-593 owns the reused proof checkout and is building one pinned historical source state; 16 predecessor blobs still need source coverage. The active owner checkouts remain `.worktrees/strata-terrain-bake`, `.worktrees/strata-editor-durability` and `.worktrees/strata-terrain-proof`; cleanup follows owner release and tracked, untracked and ignored-data checks. Codex coordinates this entire batch through PR #483. +PRD-592 validation and golden bytes are integrated at `6c40d8757`; its validation candidate passes 39 focused tests and preserves all 21 forest files, but the latest prescribed same-host timing reaches only 22.55% on a busy host, so the 30% target remains open. PRD-585 local coverage and the scaffold package-manager pin are integrated; 68 scaffold tests pass, while real CI and runtime acceptance remain open. PRD-583 forest cook, alternate LODs and both isolated-install dependency closures are integrated through `38c83ef48`; parent assets build and 17 cook tests pass, and the unskipped packed-game consumer passes on NVIDIA Turing; layered-material visual proof remains pending. PRD-587 server recovery is integrated at `5b8fbfa60`; its corrected queue is integrated locally and passes 45 focused tests, with UI and worker proofs open. PRD-593 owns the reused proof checkout; its corrected pinned-source generator passes 33 focused tests, while actual patch regeneration and all 31 predecessor compatibility checks remain in progress. The active owner checkouts remain `.worktrees/strata-terrain-bake`, `.worktrees/strata-editor-durability` and `.worktrees/strata-terrain-proof`; cleanup follows owner release and tracked, untracked and ignored-data checks. Codex coordinates this entire batch through PR #483. diff --git a/packages/terrain/__tests__/terrain-consumer.spec.ts b/packages/terrain/__tests__/terrain-consumer.spec.ts index 13ffdb194..83c75a4d0 100644 --- a/packages/terrain/__tests__/terrain-consumer.spec.ts +++ b/packages/terrain/__tests__/terrain-consumer.spec.ts @@ -12,6 +12,7 @@ import { import { toGeometry } from "@threenative/terrain/three"; import { BufferGeometry, Mesh, MeshBasicMaterial, Raycaster, Vector3 } from "three"; import { describe, expect, it } from "vitest"; +import { finiteTree } from "../src/core/validation.js"; describe("public terrain consumer", () => { it("returns measured erosion transport, respects masks and isolates cached arrays", () => { @@ -267,3 +268,25 @@ it("rejects a scatter layer without asset", () => { }), ).toThrow(/asset/); }); + +describe("finiteTree validation fast-path and edge guards", () => { + it("accepts numeric leaves at depth 32 and rejects them at depth 33", () => { + let d32: unknown = 42; + for (let i = 0; i < 32; i++) d32 = [d32]; + expect(() => finiteTree(d32)).not.toThrow(); + + let d33: unknown = 42; + for (let i = 0; i < 33; i++) d33 = [d33]; + expect(() => finiteTree(d33)).toThrow(`value${".0".repeat(33)}: nesting is too deep`); + }); + + it("names the non-finite array entry", () => { + expect(() => finiteTree([1, 2, Number.NaN], "samples")).toThrow("samples.2 must be finite"); + }); + + it("rejects an unsafe own array key", () => { + const unsafeArray = [1, 2]; + Object.defineProperty(unsafeArray, "constructor", { value: 123, enumerable: true }); + expect(() => finiteTree(unsafeArray, "samples")).toThrow("samples: unsafe object key"); + }); +}); diff --git a/packages/terrain/src/core/validation.ts b/packages/terrain/src/core/validation.ts index 466598099..0bb314cbb 100644 --- a/packages/terrain/src/core/validation.ts +++ b/packages/terrain/src/core/validation.ts @@ -133,7 +133,10 @@ function fail(message: string): never { /** Rejects anything a recipe cannot round-trip: non-finite numbers, views, cycles, unsafe keys. */ export function finiteTree(value: unknown, path = "value", depth = 0): void { if (depth > 32) fail(`${path}: nesting is too deep`); - if (typeof value === "number" && !Number.isFinite(value)) fail(`${path} must be finite`); + if (typeof value === "number") { + if (!Number.isFinite(value)) fail(`${path} must be finite`); + return; + } if ( value === undefined || typeof value === "function" || @@ -143,9 +146,15 @@ export function finiteTree(value: unknown, path = "value", depth = 0): void { fail(`${path} is not JSON-serializable`); if (value && typeof value === "object") { if (ArrayBuffer.isView(value)) fail(`${path}: use a plain array in recipes`); - for (const [key, entry] of Object.entries(value)) { - if (["__proto__", "prototype", "constructor"].includes(key)) + for (const key of Object.keys(value)) { + if (key === "__proto__" || key === "prototype" || key === "constructor") fail(`${path}: unsafe object key`); + const entry = (value as Record)[key]; + if (typeof entry === "number") { + if (depth + 1 > 32) fail(`${path}.${key}: nesting is too deep`); + if (!Number.isFinite(entry)) fail(`${path}.${key} must be finite`); + continue; + } finiteTree(entry, `${path}.${key}`, depth + 1); } } From 44d213b4bbbe6b9890fc8e5bbb57d8b7abbc11fb Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 15:04:47 -0700 Subject: [PATCH 023/210] fix(terrain): reject invalid cook flags and preserve IO failures --- ...-bakes-headlessly-into-an-instanced-glb.md | 4 ++ .../assets/__tests__/terrain-world.spec.ts | 44 ++++++++++++++++++- packages/assets/src/world/terrain-world.ts | 5 ++- packages/core/capabilities.json | 1 + .../references/capability-reference.md | 2 +- packages/create-threenative/capabilities.json | 1 + .../terrain/__tests__/starter-kit.spec.ts | 19 ++++++++ packages/terrain/starter/forest/bake.mjs | 7 ++- 8 files changed, 78 insertions(+), 5 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md index a76e705ca..bde4a4b75 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md @@ -119,3 +119,7 @@ Initial review: six weak unit tests passed; `node --check examples/strata-terrai 2026-10-10 direct checkout command: root now declares the existing @threenative/assets workspace package as a dev dependency (terrain does not depend on assets). node packages/terrain/starter/forest/bake.mjs also passes from the checkout and emits the instanced GLB plus package, so the documented direct command has an explicit dependency closure. 2026-10-10 parent integration: offline frozen installation, assets build/publint and 17 terrain-world tests pass (1.05 s test time). The direct forest command emits the 24 641 592 B GLB in 595 ms. The full consumer first stopped before the game stage because the default cooked fixture was absent; after the documented bake, the unskipped rerun passes (exit 0). Its scaffolded game playtest reports WebGPU, NVIDIA Turing and no direct authoring-package dependency; the forest stock-loader census reports 4 820 placements and zero per-tree copies. Layered-surface and headed forest visual acceptance remain open. + +2026-10-10 fail-closed hardening (bounded): `packages/terrain/starter/forest/bake.mjs` refuses a bare, empty or flag-valued `--out`/`--assets` before any directory is created; `authoredSiblingLods` in `packages/assets/src/world/terrain-world.ts` propagates any non-ENOENT stat error instead of reading it as an absent sibling. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` 18 tests pass; `pnpm exec vitest run packages/terrain/__tests__/starter-kit.spec.ts -t "refuses a bare"` 1 test passes; scoped Biome clean except the pre-existing cognitive-complexity warning. The four sibling kits (alpine, coastal, desert, tundra) still carry the identical bare `flag` parser and were left out of this bounded change. + +Independent review: the CLI regression executes the actual checkout script from a temporary cwd (no TMPDIR lookup workaround), covers all six invalid flag forms, and leaves no world directory. `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts packages/terrain/__tests__/starter-kit.spec.ts -t "cookTerrainWorld|refuses a bare"`: 19 passed, 9 deliberately unselected, 8.67 s. Assets typecheck passes; scoped Biome has only the existing complexity warning. The broader starter golden hash/size check remains failing and is not claimed. diff --git a/packages/assets/__tests__/terrain-world.spec.ts b/packages/assets/__tests__/terrain-world.spec.ts index de06fd146..0af7a0593 100644 --- a/packages/assets/__tests__/terrain-world.spec.ts +++ b/packages/assets/__tests__/terrain-world.spec.ts @@ -14,13 +14,32 @@ import { import { PNG } from "pngjs"; import { type Group, InstancedMesh, Mesh } from "three"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { type IWorldPackage, validateWorldPackage } from "../../core/src/world-package.js"; import { cookTerrainWorld } from "../src/world/terrain-world.js"; // GLTFLoader reads the global `self` while it parses. Node has none. Object.assign(globalThis, { self: globalThis }); +// A deterministic IO failure injected for one sibling path, so a real stat error can be told +// apart from an absent sibling without touching permissions or the platform. +const fsProbe = vi.hoisted(() => ({ + statFailure: undefined as { path: string; error: Error } | undefined, +})); + +vi.mock("node:fs/promises", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + stat: async (...args: Parameters) => { + if (fsProbe.statFailure && String(args[0]) === fsProbe.statFailure.path) { + throw fsProbe.statFailure.error; + } + return original.stat(...args); + }, + }; +}); + /** A real evaluated terrain. The fixed seed fixes every scatter position. */ function evaluatedTerrain(assets: readonly string[] = ["fir"]): ITerrainState { let terrain = new Terrain({ resolution: 33, seed: 73, size: 256 }); @@ -457,6 +476,29 @@ describe("cookTerrainWorld", () => { ); }); + it("propagates a real IO error from a sibling probe instead of treating it as absent", async () => { + const nearPath = path.join(dir, "oak.glb"); + await writeFile(nearPath, await simpleSpecies()); + const sibling = path.join(dir, "oak-mid.glb"); + const ioError = Object.assign(new Error("injected EIO reading sibling"), { code: "EIO" }); + fsProbe.statFailure = { path: sibling, error: ioError }; + try { + const caught = await cookTerrainWorld({ + state: evaluatedTerrain(), + species: { fir: { glb: nearPath } }, + out: dir, + }).then( + () => null, + (error: unknown) => error, + ); + // The exact error reaches the caller; it is not swallowed as a missing sibling. + expect(caught).toBe(ioError); + await expect(stat(path.join(dir, "world.glb"))).rejects.toThrow(); + } finally { + fsProbe.statFailure = undefined; + } + }); + it("names a placement that is mirrored in Z as a contact failure", async () => { const state = evaluatedTerrain(); const spacing = state.size / (state.resolution - 1); diff --git a/packages/assets/src/world/terrain-world.ts b/packages/assets/src/world/terrain-world.ts index 82e7e1482..8f109b189 100644 --- a/packages/assets/src/world/terrain-world.ts +++ b/packages/assets/src/world/terrain-world.ts @@ -191,8 +191,9 @@ async function authoredSiblingLods(glbPath: string): Promise<{ distance: number; try { await stat(file); return [{ distance: 40, glb: file }]; - } catch { - // Absent sibling: try the next name. + } catch (error) { + // Only a missing sibling means "try the next name"; any other IO error is real. + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } } return []; diff --git a/packages/core/capabilities.json b/packages/core/capabilities.json index f75f5f5dc..fd73947a3 100644 --- a/packages/core/capabilities.json +++ b/packages/core/capabilities.json @@ -4020,6 +4020,7 @@ "constraints": [ "surface is the game's; this class creates no material, colour or geometry", "budgets are hard caps that report pressure instead of over-committing", + "`whenSpawnReady(position, radius, timeoutMs)` snapshots a fixed region and settles from existing updates or disposal; it does not drive streaming, and `prewarmed` remains the separate draw gate", "model loads are bounded by `concurrency` (default 12) across every resident cell, not per cell", "refilters are bounded by `rebuildsPerUpdate` (default 16) per update, nearest cell first", "admission is bounded by `admissionBudgetMs` (default 2) per update across every path, plus at most one unit each for terrain and props; while props are queued terrain takes at most half, so neither starves the other, and a deferred cell keeps drawing what it has", diff --git a/packages/create-threenative/agent-docs/references/capability-reference.md b/packages/create-threenative/agent-docs/references/capability-reference.md index ea8855eda..1d611955a 100644 --- a/packages/create-threenative/agent-docs/references/capability-reference.md +++ b/packages/create-threenative/agent-docs/references/capability-reference.md @@ -3148,7 +3148,7 @@ export class WorldCells extends Group implements IComputeDriven { … } ``` - **Use when:** stream a large Blender-authored world by cell instead of one huge GLB · keep scattered props and hand-placed chunks resident around a moving player · honour per-asset draw distances and hard streaming budgets without a mid-frame throw -- **Constraints:** surface is the game's; this class creates no material, colour or geometry · budgets are hard caps that report pressure instead of over-committing · model loads are bounded by `concurrency` (default 12) across every resident cell, not per cell · refilters are bounded by `rebuildsPerUpdate` (default 16) per update, nearest cell first · admission is bounded by `admissionBudgetMs` (default 2) per update across every path, plus at most one unit each for terrain and props; while props are queued terrain takes at most half, so neither starves the other, and a deferred cell keeps drawing what it has · SkinnedMesh parts are skipped; an instanced copy would draw one rest pose · a baked chain's switch distances are measured against `autoLod` (default 4 px of error over 60° and 1080 raster rows), because an instanced draw cannot select a level per instance; an asset with nonempty authored `lods` never consults it; explicit empty lists enroll only unchanged static geometry/material parts · eligible static CPU cells can use their cooked HLOD after compilation and an observed main-context backend submission; projected-error hysteresis requires actual source triangle reduction and no extra material draws, unsupported or shared source stays resident, and collision/shadow paths retain source representations; stats().hlod counts lifecycle and charged resources, not GPU timing or physical allocation · `prewarmed` resolves once every prewarmed shared batch has been drawn; a game with a loading screen waits on it, and `stats().pendingPrewarm` is the same gate as a number · every `asset:level:part` is one InstancedMesh for the main pass, plus one caster InstancedMesh per world-grid square of `clusterSize` on the shadow caster layer, so the main pass draws one mesh per key and a shadow level submits only the squares it covers · two definitions the asset loader resolves to one model — the same cooked `glb`, the same `lods` at the same distances, the same `maxDistance` and bounds — are one asset under the lexicographically smallest id: one model load, one set of `asset:level:part` keys, one prewarm and one refcount, released when the last cell holding any member of the group leaves the ring; `TN_WORLD_ASSET_ALIAS` reports how many of the package's assets are really distinct · the main pass mesh draws only the squares the render camera's frustum covers — on by default, narrowed once per frame for every main batch by the engine's render-cadence dispatch, never for an orthographic camera — a batch with nothing to draw is hidden rather than submitted at `count 0`, and `TN_WORLD_MAIN_CULL` reports both every five seconds · a loaded chunk is merged by material before it is added, so it submits one draw per material rather than one per node; a skinned, multi-material or morph-target mesh, one carrying a baked AutoLOD chain, and an instanced mesh past `chunkMergeMaxTriangles` (default 43,690 triangles) or with a shape over 2,048 triangles, all keep their own geometry; a material group crossing 131,072 vertices (4 MiB of position + normal + uv) is split into several meshes in traversal order instead of one giant upload, indexed parts keep their index, and `TN_WORLD_CHUNK_MERGE` reports what the merge did and the bytes it left · `shadows.castDistance` is accepted and ignored (clusters replaced it); `shadows.invalidate` is called at most once a second after streamed records changed +- **Constraints:** surface is the game's; this class creates no material, colour or geometry · budgets are hard caps that report pressure instead of over-committing · `whenSpawnReady(position, radius, timeoutMs)` snapshots a fixed region and settles from existing updates or disposal; it does not drive streaming, and `prewarmed` remains the separate draw gate · model loads are bounded by `concurrency` (default 12) across every resident cell, not per cell · refilters are bounded by `rebuildsPerUpdate` (default 16) per update, nearest cell first · admission is bounded by `admissionBudgetMs` (default 2) per update across every path, plus at most one unit each for terrain and props; while props are queued terrain takes at most half, so neither starves the other, and a deferred cell keeps drawing what it has · SkinnedMesh parts are skipped; an instanced copy would draw one rest pose · a baked chain's switch distances are measured against `autoLod` (default 4 px of error over 60° and 1080 raster rows), because an instanced draw cannot select a level per instance; an asset with nonempty authored `lods` never consults it; explicit empty lists enroll only unchanged static geometry/material parts · eligible static CPU cells can use their cooked HLOD after compilation and an observed main-context backend submission; projected-error hysteresis requires actual source triangle reduction and no extra material draws, unsupported or shared source stays resident, and collision/shadow paths retain source representations; stats().hlod counts lifecycle and charged resources, not GPU timing or physical allocation · `prewarmed` resolves once every prewarmed shared batch has been drawn; a game with a loading screen waits on it, and `stats().pendingPrewarm` is the same gate as a number · every `asset:level:part` is one InstancedMesh for the main pass, plus one caster InstancedMesh per world-grid square of `clusterSize` on the shadow caster layer, so the main pass draws one mesh per key and a shadow level submits only the squares it covers · two definitions the asset loader resolves to one model — the same cooked `glb`, the same `lods` at the same distances, the same `maxDistance` and bounds — are one asset under the lexicographically smallest id: one model load, one set of `asset:level:part` keys, one prewarm and one refcount, released when the last cell holding any member of the group leaves the ring; `TN_WORLD_ASSET_ALIAS` reports how many of the package's assets are really distinct · the main pass mesh draws only the squares the render camera's frustum covers — on by default, narrowed once per frame for every main batch by the engine's render-cadence dispatch, never for an orthographic camera — a batch with nothing to draw is hidden rather than submitted at `count 0`, and `TN_WORLD_MAIN_CULL` reports both every five seconds · a loaded chunk is merged by material before it is added, so it submits one draw per material rather than one per node; a skinned, multi-material or morph-target mesh, one carrying a baked AutoLOD chain, and an instanced mesh past `chunkMergeMaxTriangles` (default 43,690 triangles) or with a shape over 2,048 triangles, all keep their own geometry; a material group crossing 131,072 vertices (4 MiB of position + normal + uv) is split into several meshes in traversal order instead of one giant upload, indexed parts keep their index, and `TN_WORLD_CHUNK_MERGE` reports what the merge did and the bytes it left · `shadows.castDistance` is accepted and ignored (clusters replaced it); `shadows.invalidate` is called at most once a second after streamed records changed - **Overrides:** `hlod: false` keeps source rendering; ring, budgets, terrain tile size/resolution, terrain stream and collider radius, `transparentScatter`, `clusterSize` and `shadows.invalidate`, load `concurrency`, `rebuildsPerUpdate`, `admissionBudgetMs` and the package's per-asset maxDistance ```ts diff --git a/packages/create-threenative/capabilities.json b/packages/create-threenative/capabilities.json index f75f5f5dc..fd73947a3 100644 --- a/packages/create-threenative/capabilities.json +++ b/packages/create-threenative/capabilities.json @@ -4020,6 +4020,7 @@ "constraints": [ "surface is the game's; this class creates no material, colour or geometry", "budgets are hard caps that report pressure instead of over-committing", + "`whenSpawnReady(position, radius, timeoutMs)` snapshots a fixed region and settles from existing updates or disposal; it does not drive streaming, and `prewarmed` remains the separate draw gate", "model loads are bounded by `concurrency` (default 12) across every resident cell, not per cell", "refilters are bounded by `rebuildsPerUpdate` (default 16) per update, nearest cell first", "admission is bounded by `admissionBudgetMs` (default 2) per update across every path, plus at most one unit each for terrain and props; while props are queued terrain takes at most half, so neither starves the other, and a deferred cell keeps drawing what it has", diff --git a/packages/terrain/__tests__/starter-kit.spec.ts b/packages/terrain/__tests__/starter-kit.spec.ts index 96085327a..8c4c042d6 100644 --- a/packages/terrain/__tests__/starter-kit.spec.ts +++ b/packages/terrain/__tests__/starter-kit.spec.ts @@ -247,4 +247,23 @@ describe("forest starter kit", () => { // The 25 MiB budget is per baked world (asserted above); this only catches package bloat. expect(kitBytes).toBeLessThanOrEqual(40 * 1024 * 1024); }); + + it("refuses a bare, empty or flag-valued --out/--assets before writing", () => { + const dir = kitInTempDir(); + const bakeWith = (...args: string[]) => + execFileSync(process.execPath, [join(packageRoot, "starter/forest/bake.mjs"), ...args], { + cwd: dir, + stdio: "pipe", + }); + + // A bare flag, an empty value and a following flag are all refused, by name. + expect(() => bakeWith("--out")).toThrow(/--out needs a nonempty/); + expect(() => bakeWith("--out", "")).toThrow(/--out needs a nonempty/); + expect(() => bakeWith("--out", "--assets", starterAssets)).toThrow(/--out needs a nonempty/); + expect(() => bakeWith("--assets")).toThrow(/--assets needs a nonempty/); + expect(() => bakeWith("--assets", "")).toThrow(/--assets needs a nonempty/); + expect(() => bakeWith("--assets", "--out", dir)).toThrow(/--assets needs a nonempty/); + // Nothing was written beside the kit. + expect(() => statSync(join(dir, "world"))).toThrow(); + }); }); diff --git a/packages/terrain/starter/forest/bake.mjs b/packages/terrain/starter/forest/bake.mjs index 313a5061e..177f6bc4b 100644 --- a/packages/terrain/starter/forest/bake.mjs +++ b/packages/terrain/starter/forest/bake.mjs @@ -15,7 +15,12 @@ const here = dirname(fileURLToPath(import.meta.url)); /** `--key value` after the script; `--assets` and `--out` default as noted below. */ function flag(name, fallback) { const at = process.argv.indexOf(`--${name}`); - return at === -1 ? fallback : resolve(process.argv[at + 1] ?? ""); + if (at === -1) return fallback; + const value = process.argv[at + 1]; + if (value === undefined || value === "" || value.startsWith("--")) { + throw new Error(`bake: --${name} needs a nonempty directory value`); + } + return resolve(value); } // The installed kit ships its models and textures beside the package manifest, so a copied kit From 83a9e79bcf35c76d44261e39dedcac30b8b28182 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 15:38:13 -0700 Subject: [PATCH 024/210] fix(terrain): reject invalid bake flags across starter kits --- ...-bakes-headlessly-into-an-instanced-glb.md | 2 + .../terrain/__tests__/starter-kit.spec.ts | 38 ++++++++++--------- packages/terrain/starter/alpine/bake.mjs | 7 +++- packages/terrain/starter/coastal/bake.mjs | 7 +++- packages/terrain/starter/desert/bake.mjs | 7 +++- packages/terrain/starter/tundra/bake.mjs | 7 +++- 6 files changed, 47 insertions(+), 21 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md index bde4a4b75..4314ad2fa 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md @@ -123,3 +123,5 @@ Initial review: six weak unit tests passed; `node --check examples/strata-terrai 2026-10-10 fail-closed hardening (bounded): `packages/terrain/starter/forest/bake.mjs` refuses a bare, empty or flag-valued `--out`/`--assets` before any directory is created; `authoredSiblingLods` in `packages/assets/src/world/terrain-world.ts` propagates any non-ENOENT stat error instead of reading it as an absent sibling. proof: `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts` 18 tests pass; `pnpm exec vitest run packages/terrain/__tests__/starter-kit.spec.ts -t "refuses a bare"` 1 test passes; scoped Biome clean except the pre-existing cognitive-complexity warning. The four sibling kits (alpine, coastal, desert, tundra) still carry the identical bare `flag` parser and were left out of this bounded change. Independent review: the CLI regression executes the actual checkout script from a temporary cwd (no TMPDIR lookup workaround), covers all six invalid flag forms, and leaves no world directory. `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts packages/terrain/__tests__/starter-kit.spec.ts -t "cookTerrainWorld|refuses a bare"`: 19 passed, 9 deliberately unselected, 8.67 s. Assets typecheck passes; scoped Biome has only the existing complexity warning. The broader starter golden hash/size check remains failing and is not claimed. + +2026-10-10 sibling closure (bounded): the earlier note that the four sibling kits (alpine, coastal, desert, tundra) still carry the identical bare `flag` parser no longer holds. Each of the four `bake.mjs` now carries the exact seven-line guard from `packages/terrain/starter/forest/bake.mjs` (bare, empty or `--`-valued `--out`/`--assets` throw before any directory is created), with the per-kit defaults unchanged. The `refuses a bare` regression is parameterized over all five biome scripts, executing each actual checkout `bake.mjs` from a temporary cwd and asserting all six invalid forms throw and the cwd stays empty. proof: `pnpm exec vitest run packages/terrain/__tests__/starter-kit.spec.ts -t "refuses a bare"` 5 passed, 9 skipped; scoped Biome clean on all six files. Multi-layer terrain PBR appearance and runtime/visual proof remain open; no full-PRD completion is claimed. diff --git a/packages/terrain/__tests__/starter-kit.spec.ts b/packages/terrain/__tests__/starter-kit.spec.ts index 8c4c042d6..e9a8de230 100644 --- a/packages/terrain/__tests__/starter-kit.spec.ts +++ b/packages/terrain/__tests__/starter-kit.spec.ts @@ -248,22 +248,26 @@ describe("forest starter kit", () => { expect(kitBytes).toBeLessThanOrEqual(40 * 1024 * 1024); }); - it("refuses a bare, empty or flag-valued --out/--assets before writing", () => { - const dir = kitInTempDir(); - const bakeWith = (...args: string[]) => - execFileSync(process.execPath, [join(packageRoot, "starter/forest/bake.mjs"), ...args], { - cwd: dir, - stdio: "pipe", - }); + it.each(["forest", "alpine", "coastal", "desert", "tundra"] as const)( + "refuses a bare, empty or flag-valued --out/--assets before writing (%s)", + (kitName) => { + const dir = makeTempDirSync(`terrain-starter-flags-${kitName}-`); + const bakeWith = (...args: string[]) => + execFileSync( + process.execPath, + [join(packageRoot, "starter", kitName, "bake.mjs"), ...args], + { cwd: dir, stdio: "pipe" }, + ); - // A bare flag, an empty value and a following flag are all refused, by name. - expect(() => bakeWith("--out")).toThrow(/--out needs a nonempty/); - expect(() => bakeWith("--out", "")).toThrow(/--out needs a nonempty/); - expect(() => bakeWith("--out", "--assets", starterAssets)).toThrow(/--out needs a nonempty/); - expect(() => bakeWith("--assets")).toThrow(/--assets needs a nonempty/); - expect(() => bakeWith("--assets", "")).toThrow(/--assets needs a nonempty/); - expect(() => bakeWith("--assets", "--out", dir)).toThrow(/--assets needs a nonempty/); - // Nothing was written beside the kit. - expect(() => statSync(join(dir, "world"))).toThrow(); - }); + // A bare flag, an empty value and a following flag are all refused, by name. + expect(() => bakeWith("--out")).toThrow(/--out needs a nonempty/); + expect(() => bakeWith("--out", "")).toThrow(/--out needs a nonempty/); + expect(() => bakeWith("--out", "--assets", starterAssets)).toThrow(/--out needs a nonempty/); + expect(() => bakeWith("--assets")).toThrow(/--assets needs a nonempty/); + expect(() => bakeWith("--assets", "")).toThrow(/--assets needs a nonempty/); + expect(() => bakeWith("--assets", "--out", dir)).toThrow(/--assets needs a nonempty/); + // Nothing was written into the working directory. + expect(readdirSync(dir)).toEqual([]); + }, + ); }); diff --git a/packages/terrain/starter/alpine/bake.mjs b/packages/terrain/starter/alpine/bake.mjs index 35a3a508e..88f876779 100644 --- a/packages/terrain/starter/alpine/bake.mjs +++ b/packages/terrain/starter/alpine/bake.mjs @@ -13,7 +13,12 @@ const here = dirname(fileURLToPath(import.meta.url)); /** `--key value` after the script; `--assets` and `--out` default as noted below. */ function flag(name, fallback) { const at = process.argv.indexOf(`--${name}`); - return at === -1 ? fallback : resolve(process.argv[at + 1] ?? ""); + if (at === -1) return fallback; + const value = process.argv[at + 1]; + if (value === undefined || value === "" || value.startsWith("--")) { + throw new Error(`bake: --${name} needs a nonempty directory value`); + } + return resolve(value); } // The installed kit ships its models and textures beside the package manifest, so a copied kit diff --git a/packages/terrain/starter/coastal/bake.mjs b/packages/terrain/starter/coastal/bake.mjs index 58750cf12..e4c0f2a9a 100644 --- a/packages/terrain/starter/coastal/bake.mjs +++ b/packages/terrain/starter/coastal/bake.mjs @@ -13,7 +13,12 @@ const here = dirname(fileURLToPath(import.meta.url)); /** `--key value` after the script; `--assets` and `--out` default as noted below. */ function flag(name, fallback) { const at = process.argv.indexOf(`--${name}`); - return at === -1 ? fallback : resolve(process.argv[at + 1] ?? ""); + if (at === -1) return fallback; + const value = process.argv[at + 1]; + if (value === undefined || value === "" || value.startsWith("--")) { + throw new Error(`bake: --${name} needs a nonempty directory value`); + } + return resolve(value); } // The installed kit ships its models and textures beside the package manifest, so a copied kit diff --git a/packages/terrain/starter/desert/bake.mjs b/packages/terrain/starter/desert/bake.mjs index 1c1b93b16..b9cf02d60 100644 --- a/packages/terrain/starter/desert/bake.mjs +++ b/packages/terrain/starter/desert/bake.mjs @@ -13,7 +13,12 @@ const here = dirname(fileURLToPath(import.meta.url)); /** `--key value` after the script; `--assets` and `--out` default as noted below. */ function flag(name, fallback) { const at = process.argv.indexOf(`--${name}`); - return at === -1 ? fallback : resolve(process.argv[at + 1] ?? ""); + if (at === -1) return fallback; + const value = process.argv[at + 1]; + if (value === undefined || value === "" || value.startsWith("--")) { + throw new Error(`bake: --${name} needs a nonempty directory value`); + } + return resolve(value); } // The installed kit ships its models and textures beside the package manifest, so a copied kit diff --git a/packages/terrain/starter/tundra/bake.mjs b/packages/terrain/starter/tundra/bake.mjs index cfa66bf69..806dc394c 100644 --- a/packages/terrain/starter/tundra/bake.mjs +++ b/packages/terrain/starter/tundra/bake.mjs @@ -13,7 +13,12 @@ const here = dirname(fileURLToPath(import.meta.url)); /** `--key value` after the script; `--assets` and `--out` default as noted below. */ function flag(name, fallback) { const at = process.argv.indexOf(`--${name}`); - return at === -1 ? fallback : resolve(process.argv[at + 1] ?? ""); + if (at === -1) return fallback; + const value = process.argv[at + 1]; + if (value === undefined || value === "" || value.startsWith("--")) { + throw new Error(`bake: --${name} needs a nonempty directory value`); + } + return resolve(value); } // The installed kit ships its models and textures beside the package manifest, so a copied kit From 9e362e86901b25365d4ac08da732a7be542e973c Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 15:40:52 -0700 Subject: [PATCH 025/210] feat(core): await spawn readiness through WorldCells updates --- ...-bakes-headlessly-into-an-instanced-glb.md | 2 + ...-stops-reimplementing-engine-mechanisms.md | 22 +- ...ls-closed-and-erodes-without-allocating.md | 2 + .../core/__tests__/world-cells-data.spec.ts | 211 ++++++++++++++++++ packages/core/src/world-cells.ts | 98 ++++++++ 5 files changed, 333 insertions(+), 2 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md index 4314ad2fa..dc7f90d27 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-583-a-strata-world-bakes-headlessly-into-an-instanced-glb.md @@ -125,3 +125,5 @@ Initial review: six weak unit tests passed; `node --check examples/strata-terrai Independent review: the CLI regression executes the actual checkout script from a temporary cwd (no TMPDIR lookup workaround), covers all six invalid flag forms, and leaves no world directory. `pnpm exec vitest run packages/assets/__tests__/terrain-world.spec.ts packages/terrain/__tests__/starter-kit.spec.ts -t "cookTerrainWorld|refuses a bare"`: 19 passed, 9 deliberately unselected, 8.67 s. Assets typecheck passes; scoped Biome has only the existing complexity warning. The broader starter golden hash/size check remains failing and is not claimed. 2026-10-10 sibling closure (bounded): the earlier note that the four sibling kits (alpine, coastal, desert, tundra) still carry the identical bare `flag` parser no longer holds. Each of the four `bake.mjs` now carries the exact seven-line guard from `packages/terrain/starter/forest/bake.mjs` (bare, empty or `--`-valued `--out`/`--assets` throw before any directory is created), with the per-kit defaults unchanged. The `refuses a bare` regression is parameterized over all five biome scripts, executing each actual checkout `bake.mjs` from a temporary cwd and asserting all six invalid forms throw and the cwd stays empty. proof: `pnpm exec vitest run packages/terrain/__tests__/starter-kit.spec.ts -t "refuses a bare"` 5 passed, 9 skipped; scoped Biome clean on all six files. Multi-layer terrain PBR appearance and runtime/visual proof remain open; no full-PRD completion is claimed. + +2026-10-10 EPIC integration: parent reran the cook and five-kit flag regressions after integrating both commits: 23 passed, 9 deliberately unselected in 3.94 s. No runtime or appearance acceptance is claimed. diff --git a/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md b/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md index f61617456..2fd078078 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md @@ -1,6 +1,6 @@ # PRD-591 — The preview stops re-implementing engine mechanisms -**Status:** NOT STARTED +**Status:** PARTIAL **Priority:** P2 — A second prop LOD pipeline, a spawn-readiness gate and the scatter loop stay in example code where every terrain game must copy them (AC-1 to AC-4). **Complexity:** 6 (MEDIUM); risk override: visual output changes, so the owner's visual-judge rule applies **Owner:** ThreeNative maintainers @@ -54,8 +54,26 @@ both material paths. - [ ] Ladder deleted; editor on streamed props. proof: editor script + count-loc #### Phase 2: readiness and scatter -**Status:** NOT STARTED +**Status:** PARTIAL - [ ] Spawn readiness in `WorldCells`. proof: spec + web playtest + - Engine helper implemented; caller and native closure remain open: `WorldCells.whenSpawnReady(position, radius = 0, timeoutMs = 120_000)` in + `packages/core/src/world-cells.ts`, built on `readinessAt` as the only authority. It snapshots + the position, observes held waiters at the end of the existing `update` after admissions, and + settles on readiness, asset failure, disposal and its finite deadline, clearing the timer and + the waiter on every settlement. proof: `pnpm exec vitest run + packages/core/__tests__/world-cells-data.spec.ts packages/core/__tests__/world-cells-admission.spec.ts + packages/core/__tests__/world-cells-terrain.spec.ts packages/core/__tests__/world-cells.spec.ts` + — parent run: 97/97 passed in 52.94 s. The oversized `2**31` deadline first reproduced + Node timer overflow, then passed with the supported `(0, 2147483647]` ms bound. + Disposal clears the timer; unsettled test polling fails closed. Earlier core typecheck/build + (publint) passed; the parent core typecheck after the bound correction also passed (exit 0). + - Open, not ticked: caller migration (`examples/strata-terrain-preview/src/render/loading.ts`, + `game.ts`, five kit fixtures), `pnpm --filter strata-terrain-preview test:terrain:web`, and + native proof. This migration must keep the current pre-props failure capture, cancellation and + all five fixtures plus the `propStreaming` warmed/prewarmed aggregation; remove none now. + - Native proof open: the linked checkout has no host build, but the primary checkout already + has `packages/runtime-native/build/tn-linux-wgpu/mystral`. The bounded native assertion task + stopped before editing on a provider quota diagnostic. No native proof or new C++ build is claimed. - [ ] Scatter off the `enter` path. proof: web playtest long-task assertion #### Phase 3: ground and look check diff --git a/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md b/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md index 83a43cdd5..0a7d6bb5e 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-592-terrain-core-fails-closed-and-erodes-without-allocating.md @@ -69,3 +69,5 @@ Bound repair implemented (2026-10-10): `hydraulic` now distinguishes a supplied 2026-10-10 parent validation review: replaced the candidate’s broad exception assertion with direct depth-32 success and exact depth-33 error-path assertions, exact non-finite array-path proof and an unsafe own array-key case, without TypeScript suppressions. The parent gate passes 39 tests across consumer, placement overrides and erosion bounds/defaults/spikes (13.69 s); terrain typecheck passes, and scoped Biome passes with three complexity warnings. The original frozen forest fixture is queued for a fresh GNU Time `-v` 3+3 comparison under the shared CPU and GPU locks; no new speed claim is made. 2026-10-10 prescribed post-validation comparison: baseline 1.35 / 1.03 / 0.99 s versus candidate 0.70 / 0.86 / 1.05 s, 22.55% faster by arithmetic means. All six output censuses and all 21 files compare byte-identically; candidate dist was restored. Raw GNU Time `-v` output is in `artifacts/strata592-finite-bench-z1e8qf2c/`. Host load was 31.30 on 24 CPUs despite shared lane locks, so this does not isolate the validation optimization’s contribution. AC-5 and phase 2 remain open. Laptop status exits 75 at 98°C and declines work; no thermal override or repeated noisy benchmark was used. Re-plan: obtain a quiet same-host window before selecting another optimization from the profile. + +2026-10-10 quiet-host re-plan: laptop status later reports load 0.30/16 CPUs and 64°C. Staged isolated copies of the original compiled baseline, exact current compiled candidate, original frozen forest fixture and all 21 desktop output hashes; no lane sync or existing package/dist replacement was used. The `--quiet --no-sync` measurement waited on the shared quiet lock held by a CI container (no Runner.Worker observed). Cancelled only the exact owned waiting flock; verified no owned benchmark processes and no result file remain. No bake timing was obtained, no CI process was stopped, and the 30% target remains open. Retained inputs: `artifacts/strata592-laptop-bundle-2cxepmbf/`. Retry only when the quiet lock becomes available. diff --git a/packages/core/__tests__/world-cells-data.spec.ts b/packages/core/__tests__/world-cells-data.spec.ts index fb0b44463..15bd5485d 100644 --- a/packages/core/__tests__/world-cells-data.spec.ts +++ b/packages/core/__tests__/world-cells-data.spec.ts @@ -694,3 +694,214 @@ it("runs an owed stationary-camera refilter after the last byte-identical public world.dispose(); } }); + +async function spawnWorld( + follow: Vector3, + loadModel: (url: string) => Promise = async () => + new Group().add(new Mesh(new BoxGeometry(1, 2, 1), new MeshBasicMaterial())), +): Promise { + const assets = createAssetLoader(); + vi.spyOn(assets, "resolve").mockResolvedValue([]); + return WorldCells.load({ + url: "world.json", + assets, + surface: new MeshBasicMaterial(), + follow: { position: follow }, + ring: 0, + terrain: false, + data: { manifest, placements }, + budgets: { bytes: 128, instances: 4, residentCells: 2 }, + loadModel, + }); +} +async function untilReady(world: WorldCells, position: { x: number; z: number }): Promise { + for (let frame = 0; frame < 100; frame++) { + world.update(); + await Promise.resolve(); + if (world.readinessAt(position).ready) return; + } + throw new Error("Spawn region did not become ready within 100 updates"); +} + +it("resolves whenSpawnReady immediately when the spawn region is already loaded", async () => { + const world = await spawnWorld(new Vector3(32, 0, 32)); + try { + await untilReady(world, { x: 32, z: 32 }); + await expect(world.whenSpawnReady({ x: 32, z: 32 })).resolves.toMatchObject({ + ready: true, + failures: 0, + loadedCells: 1, + requiredCells: 1, + }); + } finally { + world.dispose(); + } +}); + +it("settles a whenSpawnReady waiter on a later update that admits the spawn region", async () => { + const world = await spawnWorld(new Vector3(32, 0, 32)); + const clearSpy = vi.spyOn(globalThis, "clearTimeout"); + try { + expect(world.readinessAt({ x: 32, z: 32 }).ready).toBe(false); + const ready = world.whenSpawnReady({ x: 32, z: 32 }); + let settled = false; + void ready.then(() => { + settled = true; + }); + expect(settled).toBe(false); + for (let frame = 0; frame < 100 && !settled; frame++) { + world.update(); + await Promise.resolve(); + } + expect(settled).toBe(true); + await expect(ready).resolves.toMatchObject({ ready: true, failures: 0 }); + expect(clearSpy).toHaveBeenCalled(); + } finally { + clearSpy.mockRestore(); + world.dispose(); + } +}); + +it("rejects a whenSpawnReady waiter when its spawn asset fails to load", async () => { + const world = await spawnWorld(new Vector3(32, 0, 32), async () => { + throw new Error("boom"); + }); + try { + const ready = world.whenSpawnReady({ x: 32, z: 32 }); + let settled = false; + void ready.catch(() => { + settled = true; + }); + for (let frame = 0; frame < 100 && !settled; frame++) { + world.update(); + await Promise.resolve(); + } + await expect(ready).rejects.toThrow("WorldCells spawn assets failed to load"); + expect(world.readinessAt({ x: 32, z: 32 }).failures).toBeGreaterThan(0); + } finally { + world.dispose(); + } +}); + +it("rejects whenSpawnReady on a disposed world without arming a deadline", async () => { + const world = await spawnWorld(new Vector3(32, 0, 32)); + world.dispose(); + vi.useFakeTimers(); + try { + await expect(world.whenSpawnReady({ x: 32, z: 32 })).rejects.toThrow("WorldCells disposed"); + expect(vi.getTimerCount()).toBe(0); + } finally { + vi.useRealTimers(); + } +}); + +it("settles a waiting whenSpawnReady when the world is disposed and clears its deadline", async () => { + const world = await spawnWorld(new Vector3(32, 0, 32)); + vi.useFakeTimers(); + try { + const ready = world.whenSpawnReady({ x: 224, z: 32 }); + const expectation = expect(ready).rejects.toThrow("WorldCells spawn cancelled"); + expect(vi.getTimerCount()).toBe(1); + world.dispose(); + await expectation; + expect(vi.getTimerCount()).toBe(0); + } finally { + vi.useRealTimers(); + world.dispose(); + } +}); + +it("settles every concurrent whenSpawnReady waiter and clears each deadline", async () => { + const world = await spawnWorld(new Vector3(32, 0, 32)); + try { + const first = world.whenSpawnReady({ x: 32, z: 32 }); + const second = world.whenSpawnReady({ x: 32, z: 32 }); + const third = world.whenSpawnReady({ x: 32, z: 32 }); + for (let frame = 0; frame < 100; frame++) { + expect(() => world.update()).not.toThrow(); + await Promise.resolve(); + if (world.readinessAt({ x: 32, z: 32 }).ready) break; + } + const settlements = await Promise.all([first, second, third]); + for (const readiness of settlements) expect(readiness.ready).toBe(true); + } finally { + expect(() => world.dispose()).not.toThrow(); + } +}); + +it("rejects a whenSpawnReady that exceeds its finite deadline and clears the timer", async () => { + const world = await spawnWorld(new Vector3(32, 0, 32)); + vi.useFakeTimers(); + try { + const ready = world.whenSpawnReady({ x: 224, z: 32 }, 0, 500); + const expectation = expect(ready).rejects.toThrow("WorldCells spawn exceeded 500 ms"); + expect(vi.getTimerCount()).toBe(1); + await vi.advanceTimersByTimeAsync(500); + await expectation; + expect(vi.getTimerCount()).toBe(0); + } finally { + vi.useRealTimers(); + world.dispose(); + } +}); + +it.each([ + [{ x: Number.NaN, z: 32 }, 0, 1], + [{ x: 32, z: Number.POSITIVE_INFINITY }, 0, 1], + [{ x: 32, z: 32 }, -1, 1], + [{ x: 32, z: 32 }, Number.NaN, 1], + [{ x: 32, z: 32 }, 0, 0], + [{ x: 32, z: 32 }, 0, Number.POSITIVE_INFINITY], + [{ x: 32, z: 32 }, 0, 2 ** 31], +])("rejects malformed whenSpawnReady inputs %#", async (position, radius, timeoutMs) => { + const world = await spawnWorld(new Vector3(32, 0, 32)); + try { + await expect(world.whenSpawnReady(position, radius, timeoutMs)).rejects.toThrow( + /WorldCells spawn readiness requires/, + ); + } finally { + world.dispose(); + } +}); + +it("snapshots the spawn coordinates taken at whenSpawnReady call time", async () => { + const world = await spawnWorld(new Vector3(224, 0, 32)); + try { + const spawn = { x: 32, z: 32 }; + const ready = world.whenSpawnReady(spawn); + let settled = false; + void ready.then( + () => { + settled = true; + }, + () => { + settled = true; + }, + ); + spawn.x = 224; + world.update(); + await Promise.resolve(); + expect(settled).toBe(false); + const expectation = expect(ready).rejects.toThrow("WorldCells spawn cancelled"); + world.dispose(); + await expectation; + } finally { + world.dispose(); + } +}); + +it("resolves a whenSpawnReady once and keeps that success after the region is evicted", async () => { + const follow = new Vector3(32, 0, 32); + const world = await spawnWorld(follow); + try { + await untilReady(world, { x: 32, z: 32 }); + const ready = world.whenSpawnReady({ x: 32, z: 32 }); + const resolved = await ready; + follow.x = 224; + await settle(world); + expect(world.stats().evictions).toBeGreaterThan(0); + await expect(ready).resolves.toBe(resolved); + } finally { + world.dispose(); + } +}); diff --git a/packages/core/src/world-cells.ts b/packages/core/src/world-cells.ts index 1e7599fcc..6419d4b18 100644 --- a/packages/core/src/world-cells.ts +++ b/packages/core/src/world-cells.ts @@ -921,6 +921,15 @@ export interface IWorldRegionReadiness { readonly ready: boolean; } +/** One `whenSpawnReady` caller: a fixed region snapshot, its deadline and its promise sinks. */ +interface ISpawnWaiter { + readonly position: { readonly x: number; readonly z: number }; + readonly radius: number; + readonly resolve: (readiness: IWorldRegionReadiness) => void; + readonly reject: (error: Error) => void; + readonly timer: ReturnType; +} + interface ICellBatch { readonly asset: string; /** @@ -4080,6 +4089,7 @@ class AdmissionBudget implements IAdmissionBudget { * @situation honour per-asset draw distances and hard streaming budgets without a mid-frame throw * @constraint surface is the game's; this class creates no material, colour or geometry * @constraint budgets are hard caps that report pressure instead of over-committing + * @constraint `whenSpawnReady(position, radius, timeoutMs)` snapshots a fixed region and settles from existing updates or disposal; it does not drive streaming, and `prewarmed` remains the separate draw gate * @constraint model loads are bounded by `concurrency` (default 12) across every resident cell, not per cell * @constraint refilters are bounded by `rebuildsPerUpdate` (default 16) per update, nearest cell first * @constraint admission is bounded by `admissionBudgetMs` (default 2) per update across every path, plus at most one unit each for terrain and props; while props are queued terrain takes at most half, so neither starves the other, and a deferred cell keeps drawing what it has @@ -4250,6 +4260,12 @@ export class WorldCells extends Group implements IComputeDriven { readonly #prewarmed: Promise = new Promise((resolve) => { this.#prewarmResolve = resolve; }); + /** + * Games waiting on `whenSpawnReady`, settled by the update that follows admissions or by dispose. + * One set, no listener registry and no polling: the update that already walks residency observes + * these, so a world nobody is waiting on scans nothing. See {@link whenSpawnReady}. + */ + readonly #spawnWaiters = new Set(); readonly #extentBounds: Box3; /** The largest run of each asset in any cell: the segment size every cell of it fits. */ readonly #runMax = new Map(); @@ -5032,6 +5048,8 @@ export class WorldCells extends Group implements IComputeDriven { deferred: this.#jobs.length, spentMs: budget.spentMs + props.spentMs, }; + // After this frame's admissions, so a region admitted by this update answers its waiters in it. + this.#settleSpawnWaiters(); } /** @@ -5898,9 +5916,89 @@ export class WorldCells extends Group implements IComputeDriven { }; } + /** + * Resolves when the content a fixed spawn needs has finished loading. + * + * The one-shot gate a streamed game waits on before it releases its loading screen. Readiness is + * `readinessAt` and nothing else, so there is one authority for "the spawn's data is loaded": every + * required cell and terrain tile in `radius` loaded with no failure. The position is snapshotted + * here, so a caller may keep mutating the object it passed; `update` observes the held waiters + * right after the frame's admissions, so a region an update admits settles its waiter in it. + * + * Fail closed: malformed coordinates, a negative radius or an unsupported timer deadline reject + * before a waiter is armed. A failed asset and a torn-down world reject with their own named + * error, and the deadline rejects with the budget it named. Every settlement removes the waiter + * and clears its timer, so a settled waiter holds neither the deadline nor the world, and a + * region evicted after its waiter resolved changes nothing. + */ + whenSpawnReady( + position: { readonly x: number; readonly z: number }, + radius = 0, + timeoutMs = 120_000, + ): Promise { + if ( + !Number.isFinite(position.x) || + !Number.isFinite(position.z) || + !Number.isFinite(radius) || + radius < 0 + ) + return Promise.reject( + new Error( + "WorldCells spawn readiness requires finite coordinates and a nonnegative radius.", + ), + ); + if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2 ** 31 - 1) + return Promise.reject( + new Error("WorldCells spawn readiness requires a timeout in (0, 2147483647] ms."), + ); + if (this.#released) return Promise.reject(new Error("WorldCells disposed")); + const snapshot = { x: position.x, z: position.z }; + const immediate = this.readinessAt(snapshot, radius); + if (immediate.failures > 0) + return Promise.reject(new Error("WorldCells spawn assets failed to load")); + if (immediate.ready) return Promise.resolve(immediate); + return new Promise((resolve, reject) => { + const waiter: ISpawnWaiter = { + position: snapshot, + radius, + resolve, + reject, + timer: setTimeout(() => { + this.#spawnWaiters.delete(waiter); + reject(new Error(`WorldCells spawn exceeded ${String(timeoutMs)} ms`)); + }, timeoutMs), + }; + this.#spawnWaiters.add(waiter); + }); + } + + /** Settle the waiters this frame's admissions can answer; a non-Disposed, non-Failed region waits. */ + #settleSpawnWaiters(): void { + if (this.#spawnWaiters.size === 0) return; + for (const waiter of this.#spawnWaiters) { + const readiness = this.readinessAt(waiter.position, waiter.radius); + if (readiness.failures > 0) { + this.#spawnWaiters.delete(waiter); + clearTimeout(waiter.timer); + waiter.reject(new Error("WorldCells spawn assets failed to load")); + } else if (readiness.ready) { + this.#spawnWaiters.delete(waiter); + clearTimeout(waiter.timer); + waiter.resolve(readiness); + } + } + } + dispose(): void { if (this.#released) return; this.#released = true; + // A world torn down mid-wait must not leave a game awaiting its spawn gate, nor a deadline + // outliving the world that armed it. + for (const waiter of this.#spawnWaiters) { + clearTimeout(waiter.timer); + waiter.reject(new Error("WorldCells spawn cancelled")); + } + this.#spawnWaiters.clear(); for (const [mesh, entry] of [...this.#awaited]) { if (mesh.onBeforeRender === entry.borrow) this.#handBack(mesh, entry.hadOwn, entry.own); } From 94d179efefe3cf06d38f168d18ece39c5eb30f37 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 15:34:19 -0700 Subject: [PATCH 026/210] feat(core): generate pinned Three patches with legacy upgrade compatibility --- .github/workflows/ci.yml | 5 + ...generated-and-the-upload-lane-leaves-it.md | 30 +- .../fixtures/legacy-three-patch.json.br | Bin 0 -> 743206 bytes .../__tests__/generate-three-patch.spec.ts | 260 + .../core/__tests__/three-patch-compat.spec.ts | 310 + .../__tests__/three-patch-upgrade.spec.ts | 52 +- packages/core/package.json | 1 + .../three@0.185.1-prd269-upgrade.patch | 16068 +--------------- packages/core/patches/three@0.185.1.patch | 829 +- packages/core/scripts/apply-three-patch.mjs | 268 +- .../core/scripts/generate-three-patch.mjs | 328 + packages/core/three-patch/pin.json | 197 + packages/core/three-patch/source-delta.patch | 2157 +++ pnpm-lock.yaml | 112 +- 14 files changed, 4494 insertions(+), 16123 deletions(-) create mode 100644 packages/core/__tests__/fixtures/legacy-three-patch.json.br create mode 100644 packages/core/__tests__/generate-three-patch.spec.ts create mode 100644 packages/core/__tests__/three-patch-compat.spec.ts create mode 100644 packages/core/scripts/generate-three-patch.mjs create mode 100644 packages/core/three-patch/pin.json create mode 100644 packages/core/three-patch/source-delta.patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4deb283d..d05db4d27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1169,6 +1169,11 @@ jobs: - run: pnpm budgets - run: pnpm quality - run: pnpm sync:agents --check + # Fails when a Three.js patch file is edited by hand instead of regenerated. The generator + # downloads the pinned upstream archive itself, so this proves the one command a maintainer + # runs locally (PRD-593 AC-2). + - name: Verify the generated Three.js patch has not drifted + run: pnpm --filter @threenative/core patch:generate --check # The native lane blocks the merge in exactly three cases: a full selection that touches a native # path, a pull request into main, and any full selection that is not a clean pull-request diff diff --git a/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md b/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md index c4f3d127f..e31688b29 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md @@ -1,6 +1,6 @@ # PRD-593 — The three.js patch is generated, and the upload lane leaves it -**Status:** NOT STARTED +**Status:** PARTIAL **Priority:** P1 — #381 grew the hand-maintained three patches by about 11 000 lines on private three internals, so any three upgrade can silently break texture upload for every game (AC-1 to AC-4). **Complexity:** 6 (MEDIUM); risk override: compatibility — a wrong patch breaks every game's renderer **Owner:** ThreeNative maintainers @@ -39,7 +39,7 @@ Core audit of `2d2124792` (2026-10-10): ## Acceptance Criteria -- [ ] AC-1 [local]: One command regenerates both patch files, and the result equals the committed files. proof: `pnpm --filter @threenative/core patch:generate && git diff --exit-code packages/core/patches` — Evidence: pending. +- [x] AC-1 [local]: One command regenerates both patch files, and the result equals the committed files. proof: `pnpm --filter @threenative/core patch:generate && git diff --exit-code packages/core/patches` — Evidence: passed after commit on 2026-10-10, including default pinned upstream download and own build; both artifacts unchanged. A separate fresh `--check` build also printed `matched`. - [ ] AC-2 [shared]: CI fails when a patch file is edited by hand. proof: CI job step in `ci.yml` — Evidence: pending. - [ ] AC-3 [local]: `prepareTextures` uploads compressed textures with the patch's upload-lane hunks removed. proof: `pnpm exec vitest run packages/core/__tests__/three-compressed-upload.spec.ts` plus a WebGPU playtest of the strata preview — Evidence: pending. - [ ] AC-4 [local]: Each remaining hunk (depth cache, 3D guard, bind-group invalidation) has a spec that fails without it. proof: `pnpm exec vitest run packages/core/__tests__/three-patch-*.spec.ts` — Evidence: pending. @@ -48,10 +48,10 @@ Core audit of `2d2124792` (2026-10-10): ## Execution Phases #### Phase 1: generated patches -**Status:** NOT STARTED +**Status:** PARTIAL **Files:** `packages/core/scripts/generate-three-patch.mjs` (new), `packages/core/package.json`, `.github/workflows/ci.yml` -- [ ] Regeneration matches. proof: generate + `git diff --exit-code` -- [ ] CI drift check. proof: CI run link +- [x] Regeneration matches. proof: default `pnpm --filter @threenative/core patch:generate && git diff --exit-code packages/core/patches` passed after commit; a separate fresh build with `--check` printed `matched`. Both artifact sha256 unchanged (base `ac74287d…170`, upgrade `721e99cd…e36`). +- [ ] CI drift check. proof: CI run link — step wired in `ci.yml`; draft CI run OPEN, not ticked. #### Phase 2: upload lane out of the patch **Status:** NOT STARTED @@ -62,3 +62,23 @@ Core audit of `2d2124792` (2026-10-10): **Status:** NOT STARTED - [ ] One failing-without spec per hunk. proof: `three-patch-*.spec.ts` - [ ] Wrapper cost measured. proof: `TN_FRAME_SPANS` A/B + +## Decisions + +- **2026-10-10 — old compiled byte identity is disproven.** The 16 uncovered source preimages are + moot: all 31 exact legacy acceptances are preserved and verified, and every generated target is + pinned solely to the source delta plus the upstream own build. The version-2 upgrade JSON format + is intentional. +- **2026-10-10 — reader hardened at the trust boundary.** `apply-three-patch.mjs` now rejects a pin + that is not exactly three `0.185.1` / `2431a09f…d5b9`, rejects generated/final/payload/compat path + sets that do not agree with the base patch, and refuses a stock hunk whose applied text does not + hash to the patch index blob. `three-patch-compat.spec.ts` adds one red test per failure; the + focused suite is 42 tests (9 compat + 22 upgrade + 11 generator), all green. +- **2026-10-10 — exact lock hash closure.** `pnpm-lock.yaml` had 56 old base-patch sha256 literals + (`f3c6b1fc…`); they are replaced by the new base sha256 (`ac74287d…`) with no version, importer or + format change. The offline attempt lacked the `threejs-procedural-animals` codeload tarball. + The authorized online `pnpm install --frozen-lockfile` then passed in 5.1 seconds and restored + dependencies without changing the lockfile sha256 (`2a680c32…dc4`). All 42 focused tests passed + again after installation. Generated unified-diff payloads retain upstream whitespace. + +2026-10-10 EPIC integration: parent reviewed the generator, bounded upgrade reader, frozen predecessor fixture and CI wiring. The lockfile delta is exactly 56 substitutions of the patch hash, with no other changes. Parent frozen install passed with the merged lock unchanged; the three focused patch specs passed 42 tests in 7.50 s, including all 31 legacy predecessors and all 30 previously shipped finals. A fresh default `pnpm --filter @threenative/core patch:generate --check` downloaded and built the pinned upstream and reported `matched`. CI structure/needs contracts passed 213 tests in 8.68 s. Actual CI, hand-edit rejection, upload migration, runtime/visual proof and wrapper cost remain open. diff --git a/packages/core/__tests__/fixtures/legacy-three-patch.json.br b/packages/core/__tests__/fixtures/legacy-three-patch.json.br new file mode 100644 index 0000000000000000000000000000000000000000..e4fb429feb7f115085e411243a0dc7291c922f31 GIT binary patch literal 743206 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

_<-9&0~eTb#{|hngHvYjb!TXcwl(SmgUPg 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

8;*j|SHc_d-p86F@*&Ln#wQs}@RVoMI)+awD#G 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

  • j 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 z z0b}tqV$9k0>|&CR(Dnl`7A_icv)ztzbDCe?c4!JlIH10(-44zMt}d|k=*NYLhs05hJxD#TzigfSY#2fP&5i zdk|OBM|{5m!n+q*e=oO_8@WM95b{Tp(pyyoc@Jwn_E2f+P%jmvR1)e ztt}nKOoNrl3Uyp|mQM$)6IZ`5K{V9MbV<0`Y~*0YI~k)_e8H>Q85$<}?CYb5wJS-A}A3~6klo=bQMW_kNCrGDl6 z9F1&eO=7n@5wHl%{HR4QMoX$cSt-JmkqqrL15J+Tx2|FX&onwahCJC6~@n~ zM4k?q5~9e9=%C+-+0A}TEwKH8K20c6I$9b;a{=Jx$NiYTMzHtUmuiyr&Ha%+eKZ;5 zV;t9dDB!U^zk})c@rzE7{L6<+B%j8@DddvU1)S-HPd5?Y21Ha7h|X^}rI>V70OL?k z&&8}&zwf2F_tFCc1YLE);Q$&O(>&}$Xo@GsjgH9ilVhSSW?_YpqrT+ux*Eq-;w51< zVuRvB697X>e}wfEzWRgROS*Beu14iMdJ>(@-|J$1<#d$9%~)oeXxH24B&P%1d_)u5 zPZ7DsVzXP#!ex2bJk!-HTIefzQu$VSnx)%=(WCs(nZmv>Vt8DZ?dUoXBSy^Vs060z zXa2ly$r7%B#tlT#2<#-L zq!g*wLnBl)2sc|2pw<|&T{>auI4e0Sy9C`)J`#jEKf6SPh=deAH$hOw>W`W6SG_i}xC_Alo%0{bOs7OG@Mmj!3$vGm|JO#+)89X@X!g7j3}2)~hpH zq-_4}rfGd#FQ(K3R7RkqkUTkn(BR3f34o(-M(kIaL3hlkh+*2r-~@O&HFWrQgOZzTIdIgmqj^8m=(5j6tG^!I>a5RwtD6FOPF|K}wbx`S#T(46 zrDWbm;5^kd$h?hZdG(*97y%A>s+gLwh(l2E^$^0EsF_bd!+Y)JK>9`}_++|mWV_;+ z7IMKO1&iVw(=DQyW-sxj^#vwa^0WktzBryyp4sdeddpdf0)Sb{!*YC}w>p~&uT%#FBs{t8j60!=Qx#sSAas5^pcx{G51tlLOk$?_j(cqNi> zh0=vnzZ2x;K4Ol4 zc*nDTD|iL9h_LqkPs0FeQ8OdiE0U;egc)%{NBEvb39K-7^@;Y!1K{6O;ft4sNSk}26`W;c2 z@hI*owso~LQv%%o*rIEM@F3{j2EC>)0+^4}f&k=E!T&I5x`96D>zzb}jmx$TJUpaE z_xVUHL)i*GVXSNGF*Fdhjna&C`iR!;FR1g7Znr@67B4Iab78smdonQbO$&TJNPEby zxseL!rR?v@%{0zR3;MG2BDoJM$J{wTy>V%}GwN0s-mMEK8lIS_G>#Ew~18Ev9Q9?A6mY?%=RZQ(3*N#YxYa2WuMkaKGQcwnUX+0EE(4I}xTy4nG z4jcHRY7`y(@{QNSD6_*?aJAHvav||zu=`EP)OQO>X%yTvtVW0NZpP@}Fe>({8wbf| z*Qt9wo0{EfqN|Ojlp!SJNyhi`7wRB3S9+=)sq=C_z{-!xOQtVQEE2B$cMq7|zt zoq6JNkE@OQbK*y%Vs<1c9%`c(+UnF}>~P%JRQF-s&}qu{+0oXQtRmWQSV7NHaX)N< z(_;Mp{L)hP|3A4hAmQ4TE-RgxaDAFI`O32x@m5ce!Nj#M1%On}z0^Qt7XYnO`7vdm zO@gw(p3?5{kzys1$e_!Ex2HEz-^0ShzH~tK-?q0m#8yoYvN>*Rdcj7b7Qs~*8tq6* zj!jxs9}B`M$sqHAu4@m6n_1qxkX`Og2NqVZ_l(G>?kSUBSHdA{9^jLi4xgJ9`XVWw z{ZsI-gbfY*r3g#JE~&aQrA}prHAL+(Sa3w@NXuQ0nEEjB!jIm>f{Ae^Uc&7#=%N=} zs}p>>m>EN3u$E)~zv1kK4b-4Yn8hbV2KIVbv=sG-cAE2mPDq@#H zRl>P}HLxHnfjntrUa7=_Q?}9u-z(>}E|jB0m1dvguHNHo-pBrA*;D#{}HZqlL+xq`CeS`o59E3`|Gk8 zU$zG5iU0)4K}0mp!b^i#l4HYW7kI!yP?Kz*u_zb1hI-ClX14+K$jp85KP(YI#1sN( z5duNlj^Y?)Vjz7&KTuF+%;<(NrpzS#FkapyC=9aNe02e5xrnG8s1Qh{!L>GnkJL^;utPYXYL zcSn!e0kb&qQKvD9g*fd?SkK1@;7yN}*#n`jc+6Z=``|7zV_zSwxZ=RQCHtp8#}MiL zJ?J0ywF=*S?+JgOh#m5Vv;q1BdVk2z#;Lu_8; zt((_~l6Od9{SANhd&e}izat>iH_VS`hBq&A={x5CDr@o$Y*p~!tS^YAlG0@IowBxm zZpc?IvS@D5MmJyMt(LTqyN#yTmetq)qm=X9wNB(eL2d!aP8VUmC@B z6P59`$#ps|uMRlew`5WFb7sz(NivMPv+HX{L13)NOu?usxQDa{NXK9TnatVx5X1YC z+P;9X%Jl$_;m)rPRsm8sip7F3DCq8X(B(q+2O^=dEwm8Ms>8bJ)lr!eMALXr9DT@ymoeg=aiJp;{u}ga)Fo0)nu@Na4w6{`C6;qH}5DTrsW`Ddm&s0$SV>Phq+j@ zIYGsLj2ie1WXVR=vm|DolgM#jQS#3y3a1+mH>rgpP0h&ZLx6V<5#2)ca$eM^suQ(? z=S=LA{>qpatwe!X)U7p8883rur?Hbl#Zsd#NZs-Wi!=e zCVTiPgpFalx>KfM0KHO*oDxz|FWK62*}!@riI4RXa$lyeURXjWBKIqL z@NmmgkQ-~5bst$guRyvJdfEJ)j_oy}L=z(cX!8$lSf7Q$Jw2w?rE*y|}mO}}qvahUT__g5MjMjrp?$HvaM-wXe-~es-I3sZ} zH7eOoug%U~wtI}Gd`y**JSF!cB^3?Ngt|2aQCroLnwjWVaA^XO#WbbX7_23VrDZ(T zUE{*oG>*X^j-vQb1$IbBMn9&xmCl5=aZqS$LIb4WGM7@{8;*8*IE?BIl77VR*Iew2 z-kedrG%YsQXQFf??Ly6=t_e{Znbm%=87H{awaAMJM3dcxIIOajy!5kK16=G(%^YhP zH|G{`Wi2`=y6XhRBO1|#YP~?-pSGQBXe%6l$}{Z6@S%{=yNS4mHlC?jK_-Un8uAJW z-FX?*d)XUebF%1em`KQc>F@qCu61&N?immICHgF+KiV3J+5)vP>Yvm{tJWr~Jr>xs zpG_Tvn3~T_eG#6`3fWkw*lpFFjqBppdX7A_k=e%0xd!;J7Oa_V_Z4>rq>%#0h zLor-Ij z5=<5m;@Gmh4o+=fF6Y#(!wdTupKALf9rX^xCCv*+P6iCmtZa*IsKdV}qi01%P|`zn zn-WVL8o}&O9NE7r4*S`x+8GW0)y7SX=@J`op+?D8jqJBX(&cztZwizxN|bTha}aKo zjyjNnK(o_z;q`{S^Z&Z>2;0_h!v6hgJL%;8VlQc0{E&`Zs=>&9jkpDowtKLLh95t} zjk=f8R+fr!OM8`XWqxJmmaH6(cVW}v9W^K$xyt=Z+e7$sWT$lm$~HBDD}yl`q9F{Z zP!v`zBEPGiZ1UiOt{ApSyl<3Kbq&Jey+r5Ql1yDM=ci0AcIxyi^r3e!|D(H2wk4#- z4*ve3KMEK=4o0o+9#8*Sxna3Uag0m(VPUaZSRQ0?ecPOa72^GFDgxMOWrX#U{B z+IEMV?SuPyK!+vQ=j1`sKdlS@d~Rp1lg9RtMiRZLw7tZM?J?gZpTh1dzD zSr-TvG7tflp4*8|#S+?Ox6^#Xg$t08e6k8d318x+7mT3%nUHozScgrdssE0yYduPc zoAKwu+ga(C9Cx@KfB2AtL4u(id2R;Ejcg&`_%fkcuuxZd+QlE>xN9=;V|#~nS|{#@ zb=;K9`RG2hv&S0O^4S(=XH4JMkyGdXWZ9|c63X9fypY|Ut~Rr$@{WV6+CrXPx-vba z_6pm?U-39Q(5`p@Q9!Q07yMxEaTD!=8F)$zbitkZgmXpi)pm>cq~CalkSn)$XpFCD zQn{JMh5qxBPznnr_LUg9EoKOCBIpdvk6JJjkC|O1habDM&zygiMQ<{X8i5gyyN@-Lasdvuu2;v74qeC*%+jfQrOeLjE?O46-CdF2RR9R zQVAF>=GmVtS=Qan^x>L8l9q`3sS@60LoOa9R$X5-`B^$wcqT*MSse(bzw zio=Mc8!$A|&;4eJ(&qp?4toQFd?pOOu;cTE00v#}Dbd0nqL+fg{g==7D z@1<4aaURs#nd(wF182*?>Pl${HfHw20*xFx65UD7CZ z&8IAJel;kHYt&E@t|B~#5$^n#nE4{ z77~{lvse;VH4FtOj1Vdk*LHEHc}T>hKep3X1^P3|$9d;}Hcv-o?1A263C7~ve&DI? z{1?dkgV)ZkTURD!_yCsq%g=XSbg>8tkNGYf{~&?jQFG}M;%2A&Rp?Q;_DGqv9qi_? zgCaK#r2>Phv}g2%J`0FCsC2*f?$vn%>o9Hs)c z0+|Jtf~Dd}b+TSVS%K4NRl)9?i*W5xt0xM?0+U!_kV!?ZZy6;+cJ}(JwxQ1gQK-HD zIcCkDcVMZ1{$j_PKQvD>@B-7UV+bUH=p5IDcs(MBx+9ps80{t&C;Qh(bUOA8I(|LZ zl_!~6MPX{nG%~{q9nAr0iI{M?FdNHpCg#=S?gv!7;dGCaV(grRwgHrI=JYzy2%~Z; z&M!c5gA>e$$<=)%uH1Yb7FAB)9cKD=WyDlLUR+2Ik5(!T`S2mNOg^1a0(%NDiq$8z zu)lS;#It5;NLs;hEoWqH&$;dV7NORVGq)i`yRXkO(OMF5no$0!f|7^(T8wQ4MoIC^tVewM>D~+bWhCplV^hxlbMrH z8v-#`i5O9`#(u0zI7i+)mEf0t0$%~#(_w{FJOcLlRj@~eu)Gnxk5~nB8eXn z*EOPHJ_7{#3J7$8&s>Ufl%tW{yVo<9FniY0HzJvGs}08mWin%Dw2^S&>0q@|NNRyd zi-NB-aYL4DN4+zN=Mew8K{)|PiZuTH&1Tj9C=YA_;on#WtK%%|_C>a3%2hgOh!Sc2d)KA9%5S_bcDW^W`H>7lD8eCT)+Qv6RSIZMAgy z2DBWL7DuWT zad=^-5Ho3ta>eeLi6yqY4T5pMCNHx|O1xh|IHsPV3bb?fEJ5qW&!CM_3n+v4iJc$2 z)C>?D%HZFDQLxbRvV_dA$}{0ZmHO#RWjrUjgG#Fxzrjo>Ur3tb6Jh}Q&m8*QJjAmr znY8O=at{YMFh}pPPK$fCBxz@npwhNIfR)gS2mOb=1@KHKgCms8#^p&aU7XZ>h_?fq zC=kalFnFx-geXywWR6Pv4emD0A>7DpZ$+D6vS@XXCLM!L9gd2GIMLoE7_~GItCai^ z0EHoDjh=vj6T~$dm35}zs*rMv zMYOrYRXZK{2o2I$;vi%A3?>(>PiZJ(5U|#97V5Nv`_vOrBHwMcybU@yQg@At&Ds5< zl2shlx6)X5p7NLEO*U2(vhyM71k!0LuLHEjctzS?Gv8656B`+`<0PgPOHg?4w@ex^G+G1djxx?|t z@A(OsZw?S@>|CsciLv+nJuR)bQ00(AJrU4G?!0h7F2;}#fg*)|Okch!maw34O%dYs zghF7O59?sO|6K6yzubaykG8ARZLBnA8I0DiUAK%t*zq8t>%9x{an%CqeCkv;yK= zAt!R{WCU~?{2=!bB4#e+846!@`OD#Q=O77xn0c>=-Cq$QcfoW0{;K>H&gJKVkTV(G zI)AEO$Fw^`uJ;yT@KL&{t;MGh9W%0%>ajy`0C$iDb|b<@i42JY7qL$Ojdv@~x)^d# zq#nzwTJo}_sVxT-U2rNV1}-0(S=)k&bK)cZZXF=p&(yS9=CE<@ATsR`_JlZfr`juW zPBmMI^G|tAv^6!1O%g#8&_%)5+{f2laqN*N7YOL5 z@)5lgO&`adtyzo|T5B>PT$((4dpz4OlAhh)H#=utP~KeYw%Oky5_V+4Iw`Wm%8V@A z()+WCo|h3vw<9vxttbnJHmxeK^1L?8jqjK=REGcb=7g1OenUy9#S$%?AT1YBCFriH{L?Oguex+L4rY_@?;;`xFil)~tf%S` zlXaR-wtqyaus@!o8*rlm=)fHT^(P~cj#U~@?XR<}_a(%EmIPaGiJa}-<+xpedIC|S zIeGx!%4RVj-X`$9LGoZ4#>bzk@xY%JQ)kQ;OY0EaS>AQ2n|{gz+MH~!(K*2_@k)dP zPXK>)R01V7a()5JJ^Me;R_{eJ8;Lk?t8-^D^MVufKTrIB(aY5<-@ErQDBNE%CpJz4 zE$mqaMXq7OI!MS@`E1UJM(7k_WO&(|Y~$A4t^0d*Ec>A7&V%^P{5N-a@V0;kSIH;l zR%{YF$mnNgjXd&ZF$eA@+Y{l5dAag~U{f4HOJyG8`bc;wj)KMHG&j_)Y@H+HsH=jy zbJq22*HtQ*7hmGSt75`SHvQL~wc)AeE^Bf&R`~Ax#w?lV5WMv6>sU64$JeBL&(9mr z1BBgwMM@)P_E2z`ZoRFZkc#1H19oEk6{?Nsv z_2PqCfJbmOE3VmiyvO!xS*}+-fz@qcT2)wEh8Di_?U?20ClrXEXvhPK zAD&+1tf*ESdrg#wj8Ujeai35Swjm zc-v}q5$;_;J@ZL5!(g-O`^vFftt0ZEn1xzW7AKGtV1`B}B*}R*@2AmS=#Xe4GWtR3 zqz{E~ko!)6EZObjOr&Uu}qs(5qdLrBAY z{1>-m)Q`nas6r%F^QqF5m;sTusl3^emzEH=f7dAmU1$jbBwPk7KfC=ogvGln810+t z>lOIJKb5b}+ejBQbU|T6%W@Rpq}~2injSFly<*0^;(;`#?dC8dE!T*b3(W#PF8WF9 zZfwn2z&9W;#)C8D7((5y`Um^i;rgC5`9h?&mc^jLLSOoNA9x*jS+Hj8Wnvw0uzsFP zK{=HBxACm$m6&0)Gls4~TW8d$)E#tu_zElFLzRn>q@-aqug|gl?-mozL9H3H6a8oD za&A_LCD}e(ZkJ1PyUehnN ziARQy^G5~|1I$b&*-6b;278rR)K_5hbr5dcH{Emqv4A8BE9xwHktjV!E80r}$$%o! z)v~9YRQaQNnl+xopoQ%_hT)(-9gdH>JUaQYMnKYY4H~XSn6PToSuyq9OV;N$IzJpeSDDV8R zQRx~+Ur~f3oI0^$q(v_I5kgipTfs6=Vj91Uz@6LGQFwNSZm#w?$miy4?sXQqZx>B? zW*E?guP|khL1L_6dhp-1Hb@{FH1$~3Vs2}$n6Edf_DF3jB3K7bwB||{2_Ui9)D55k z?r8LXUJgT5Stj*WujmDA&mv3atV%8}+Pc?L?lw~iN?OK06(GDqML8;vQL3m6!bfnU ziKT=o(q#WGX)LBiJ#AuMQD6q(XH@Mni>pQH%l?t!J<)2!IvS38KPz&PuL8EJiiL=) zTa0?xm~ods;h(Sr7SgzJ)eH-f-V%*Tn$+QxyBo7Vf9P%a33NgnOF!;9%HyfnB0>Lb zCbjzkf7_dnt>csZwTC|9>Ll9f`HwySc812|+PCL;ce&?-a3g3eMjjdGw#SG#YOg}k zTZGacvG#J15a`Ka8KKXg>j|l2MgmoEW@U}7NW4%FX{AIJ1Z@y{UiyawI$!#~Quv6k z)?h2b<;j6mG({D4HU0otIV2_^l}X$3OhiGePvuQu3+iluUr?qV(Mymb1Kv7kEtUL` z=A8AE1^@_&u{(KTkgWGUKL+_x`Vl9IIo?_Bn|usGMV2sAYdk@R@PHRm4F&)k@h2*7ADx0NmM&g=q~KEg zS#a-#M^(-YlcXAJ@o1bP)!@r?t`>Y;BO7ZE6$BD8Y}4uYdcdIYtUVfJ&a4h=Z-`%5 zuK24IKzMgmpe($qQ$n*gbOVLf{ zxjI&<+FA{@G%8WCxm?dsX^3ZliX~jLY7GPl zC&9v=M4Z&v0NPhJuZN2sxvZ`wA?J}ft+gP2ioAOdPwly+sa8vT_aK{mI0u(i!CXA;Upp5RfSp-hn-&{S{V8sI2- zg+n)cg3hKKw`_cCJajSM-+Ck)R)Lt4eJxrz$`fsLhhr@{AA(O>yGJZ4VvUJr_Frzv z<|#RO8TM|GLOzJ85XYzV=h2_~s@AG)zm%@Af6achjdN-)+}OOrC^qWd?IgV~dUiNc!e4Byufs%e<+e%~xlLmoQ zh`w7#&7L3)_7v0nhcf?`Dor9lKDg=TpWNeuJbgDZ2Nv^$x+loqa zX!y%f1w`T!9DWYYz``V$f_>UMFf%(kxLS|YC#z79{-vXqZ6k8pcAd>`?L8@KNI4|I z1r~6(*{0%Vdl&CgJKpcij@ZHA`9vvC&NqzYG>SyIbiUI}6``7jT>MXEGNrOoN^w5q ze5^O{A!Q@eMwHiDN0QO1c$Mqq#%ic4q&1jhMLlpeQ*D8p8`sb8=^+=EZdME_{|Ycr z>UW=%hwTIVJ>DE9=;v=E#S%Ze@X%9pZYZy zHPJl%MKfG>78MJw@k|$1?+pz6aRK9>>Vs{sCk=#sDSkY0Rh6&~H*`!z)^An6`fMWV z&V&`=B$N>g@erxp5@FiRZu&Z}YbH#*i~-Pk^73C>Ih|I zaS6REt2h5dk%_EmwMr`oCTk-lHs6laCcDm@+$X^&0ebR~>%T4owwW=`@w8ldN6t)p z@28K(h_izm^;#-4@2fG~PO$41rh>9?oZ22BzNR`=_;Eqc+ph`Rh!u(XlunYmR0bE? zxN2fT#1Ild-6ut{?RB7JHJyY|Q)8QTw=xE><0NY<3>zJsvZ<%Lz&yUl60nBzU{YjT zyh=gLT5XN%2mm!kZek1K=-ql#cc~(q6=ay8j0Xv8TRj|{`XLF>IT5-n zS;?D)^nPE__7b^64IVEpr>(?2iTQ(L2pvzy%hTy4B7Ml|ck<+PIQor7e|JY?zDt+V z1BKT60tvg7uY`dDl_x1>$@W_;m%5+|v{2oT;A?l{{-|i8Lb6fp=yY{z>YA3C=uG+- z1}GK?7>J}9B{6oTq#guI2K<~LOOel`%z?hiH~E&N;}ildF*-UsR+lXGpzYeqMaR_+ zJ5LE5IbP10a(T08z|7=rJsv7kVpcsd;%UXaC}4~-H!iUTB#N;GSd<_Ep+czw)u@SX zOp6ymb%rp_c&O@rnjOu|Xc>SE>$E87fOiMPR!t+=tfN1>%oOnk zh2^UVt6%wnuE@uwk2`r;|ELR#^0~#AHyhq*Qr=aT-<8Sno z?10p|B{dt0Akb+}tWHCQ_3AoXQANK$1E{VW`@DH!bbICr_q*}pr?e^n}1KZQ+#~!H?s~&7mhfAg{T)lr@!5j+|$9T%P0e0djDt1(fOJ zkQeAzd@JvQJP&1|ugp6E-um7&70LZU@UQ&m z*!@X;#mO>+E!+Q7bQI3LO>Bd~)?F=qqI&-{c20YKyG^Dtr_*ZBvL!{pr4u$ff}$Bt z7HzlUT``^ly2W>PRn(ToUDul@m}u7ZlP=_zn@E*9tHw|0&_K2O(hij8F;f*H`sJ0v&FIk@~Uw%k#43L zUIaW)6i&tqmE+IJT)crS#{+$$@v~Of7DtW%ULuAGE3%oB>}At@Ah~(Zd0RxmW?szjO<=6yc=K0Z?MGL6TI1ZGg0`El`7n{-Y+u7*|~zz#b!tF6aj-u*2D=cssJ)=3G;SXnYN?`gNY%xD(G39w)r$Iy6ZB@Q6Zt1r4 zm`k}L^Q`nJcK-x>IFovm^)Yh7h0mj|cPz_3EPzUf4A|r+WhZDk%$EI&f3OXiQ0TG= z7-$HQObZZ1SoOT+@&^(eUX>PG{3&2l}SG8DWvb6Ds6Lv1p%ryy!>x%=?HwqPEidn2sh#KKq_zT2|MxrkMqTRaC ziW)4RuGFChuARu6B?4bHmDSjMqr!_~JHwJF`vI8rt%0WmAz`y06uTI;y3G5rp!oDS0v;Xqn2^jxqYo|TA3fF>3|Iy@54+%L#iE}KT( zWDprZ6k7#7fSx21=yEY%urhf`V5%Dg3hgHuY^3m%=XbtCe(ekhghGa;R-b6pvDz6?c2YLK^Q?r$SUtFg~L1mg>wi zPG}3=MxXZbdt(v`9xQRre4vI5FQ$uD0)BL|<9e9Auca-?>d+#=3($WCtu%gM8tCUo z44s9d>^O8A@sj;7(C*EsrmXkDD1~|MqGUpU;BRqgWzDR+k*u`g$O{)nFl1tV*Ba{- zS#lTV=-}D!G$>Y3t=tuX7)epW2`G*LA=(63&aS!fX7TM4W8aco|D#pXtDq$-l5!R- zTFRq{TB~NJiC?o|U4j;YKU%3ewGkKX-wi=4cdg3Du<>jOiwI_Uhk$V8aqEx0E|XSwdrk zJ^_WHvmxvvj-ZkhLM#=&lb{JntG1>CFr&`f1dotn-1V(&5@ylsri!?EHm(s^rN z4bpY&@|Hm>xDls`50ToVrAU-fyw1opAH%inR!<*Aa^Hck40n>-gC zHetPil&^7i?rw;rzwwp!QnWEpVX?uGT6I9f@)Se}TqnG}C58J^UY9zbgtSby?%i)- zgu_ffdiouagyL;SjG40z#2aCvZKq2=rnIjMj8#0*h#~gvjdLj=wrYixTKA%t58XT= zq%A1IN}B)4XiUGf?-#vnY9d&tqOJV|TrMz9LxX|!(B=|a+PBket310O=#)H7@du4M zNz$NnjM3jYR$Av)xU%$&bk8A`z$uf&9cq}0BVBwS8xO{MvK?6lhHv_n!3IKyRZWMCw8V|4{nd4)nu1V8sMoAjP@&>1z)g}P?nqf&0}MjzMHYu8cv;5L|pC#;?m zXb|Ta*SZvNLx!=qh962vn2oN6cKMemxvLpCys>6oIHf}X(|QD!`SN5Yok6r>RrAE% zj`Y&rHfKtc>Lc$IoIglsVp-pi;<90QSll z%iJnbNN|lMoPw!6&JO@K%$?Z9v(x> zIpusi^~$vUgV<<$nyWjSvB2i5S+ z7om;5H=w3~zZtA>V7m`JNkP_ExaUwjBd;Kn>+U0nC;01GSy1&{0MAvV>|pK$B@pqV zMckTrlf!!bi0-!6*e7~k*)ct2^TWcVu`Ub$Yp<4Xy6~sx9Q{zY$qA`MJ7BLpwxRcM zC3vP1S(`p<`c{yGuv&#(7oN^}Or_}{u!|*l&YE^h#_Khys=vS0XYt04(ft(8tBRJ! z-=d-b8(yi6<{e1yp%(w-BNdMCuDTc0xGmhZ)I49#rNFb{C7wS!mN^PQkX4`EKPJR` z+Li%Kxe5s;Frpr8b`W3*-M2_0NEkiC3jkFs3`d5o6O(XeX6LJHbugp(kp8*jaR7!# z-QqZUg7;r>;U^q^H@%sU^9^CT;XmX5eKg~urIdBg+Nqu;8U8OwlGaEqkKL)#@DQ2$%?oFx0NQ}CrAc^$$C^4|tC0ZNDa942xp{FIf zO!1FVMIu)^E5%*v5>H^jOmIH5os&p%l3>hUKYMK>-FL4RSRQy#hrOHhjh{%QCQ&QI z%I@A=IS-!_qIR&5{}X_=1G#$G&^GQ>%1Bv7=CT8k+^23%Ly-|e0R!UJxz^53B$ZT$t!VqVumoezm37MM>Sj^B?;^)=e4}-X{F5Yagk|B(9j7O{bVVddt z#BU2*H;7^>A@)_si6EEH(R|&WZR&R8L|-i7jcO2`r4cagrcD`J8K$awC<8LbdXP6$ z)RImff_rp4SuI%z&NRAKV|!iP(GpmJ8M0J&w;P(>RWGAnDe`G+or4^IXRvh(7Dd>1 zoo$v*^V!Qb1(C%kkg;EUaBIQ)-hV~Ea%8j#C^C+TM!>xQipAJ=;m}+Qzxh$RiO(0S zmEBfhJ#^)?HYJz+*Ct?D@hrcLE_Q72ao=vc%3VOSb=)xA8;up=MFHH}rLfrmZtSf8 zpvM2t$)kJ#W2Bbn{wKdhg?{{5F#o~8x9%+ckNh|~VXpB2I&A;s^22)A!slYNjx*yp z?}Hhr(Ut^09y5)bn;&8;b6qMUC*JHHu4xtjT`ua{ARH?&FqAwBhQwc(wE-@wNS?A& zYspg>X&RsWPBk4&S@5?wCvQkB{kgZBQ?FR;)mE0I+6&>36 zEmm@9O~VkXBAFe{(tHd?T2ebZa;0rgsVW3V9pm5Ua|5};Og^L`rMoZCDKc7jU7*fV z4LRGG3w~d5b6@9Ou$P@_4}Ztc(Jgj1e3V06hk$bg+735z&DA6(@?G3j2c|jtCYA+c zK0-cOq0Fx=C=w*RG0>DdR1l*vToG%BUGJNAv=6egf9Dyn?rzw~)9m{MZj$BQi7Bj# zkJs$`H*EdzC087HXMj+2f=5gQJRvClP)Jblnc-27tdgY(j{}^R^xpq}y8JlvU);fU z@Pj)$*@br^?180egyL5YEY8tMwQ6DoXwPhGar|Reipvu)$;vpqAYmO0mxE_kmyH4>07mks==pN$3;kmmh=cU0jU$01qz&) zV#%b22R)7;EhvLTf*uG9(5X1QayLt$eCnz4cegG2j={QvuyPs?ev;C1A~nG*H>3M7(1`R^1$>R z?RluJ7uf|TTyh{DPGfX}Ed2w|7c0bDynM=hr$Q<{^pNyQ9n=~C%os~{Gq)UNd9){u z_bK{A9X7({`N9do%+9D)L-`)+#Zcay2v4iVMEa9en3eooVJJWMW$T@lNA?r=J@_8q z@~0NHgqVRaJQJiKeAt6%3H zJmba!3-G6%Z__Qx2JrJ`ca$$rf2#U}M4U5B7(v8zSp^iMx~ zei?_)US8;ZXl`RBmS7tv^D9R4=B-_EKPN@*oJN;YAvI_51JNNp4XV9of`a!Fh%!AZ zCuHDs?b^+8FWc~94SjtKujTx_130%o7a~C2&3Qa5WkjOgYYSVqbC&Fp_az+Znr_r? z$QaOy5iZf#r2N0OsW6$=Md9v0dd7s3Txl*p+DH}+(v9yFGaU0*38D0{l7&I9bjAX! zQNy%Nv2L)-{`)jO2>ga`_MUysksoLxd;ch-9_1(o}w_yLA z?Y)oF2&T_m3T#(<(0Kr#1_XhF7za~`kJnJe{XlyndeCzHC|Na566fUl_@?xuL+K^Z z`EU3p*UUJ-`Sg6I(=!id;@hG*HoB|9raK^%TXzjUw6~@v|-<7%rP(xCE~h*gUU% zr2#-i;ryL@P+)KCxu8^M&j6_fCIHPt&U1^x(Pt|QAJ-_-&n9y%QCz0ypVGSEK#;gq z(Z784oB+gSY7yi~!hBqex+8Y-1ApNPBKN;=1?79jfT&i|X$)Z_YMuv7>vQ`Ipu#Yb zI&`qfpcJmpQTJz`fzQ$%`_WP2tS4h77(j9FI$&)8tEqqxY3TqWM!clxd7<>u=m}wm z5igaVHewj@lIRjb2_yc(Oc!J@;<=>sqeSV)4N6}u`o*C??otvy*Aat=TNnK~K>sg~ zk1(#j2=oOg8TgD2Z$EmJZbdV3?!uYt0v1GoMb|?^j1V9Hlx`LMafi~|iB_qf=G7Us zP(_H3e1SxWHc1bG6hb_|l%97=H`3$jbNAG)Zm!4YjnbbTqtswIAtX`nq1KMqprCJkhlTnLtuY;F|L=HbUtN{tHy*jm0l>5+(s z5vs2Ndbqa?z@lA+|L=qYhmUM9s~VJi21QyPScfye@aT~rW82(1^p~i$#x%DRasNY^ z>+NwE9%k%gsN~^ReMDma5mGO;C2*&N4K>_idJW>~&-zy%vy^UOGnQ4p;DS8^y|;qH zLC<91K*JZ2ehg4@D*=Br0IxGVca%QCJz^A z20dw~I$dbR-jO-c{~ejrzdn=Q?b(zDctqu>0e>bR%B{E!5ri^vNM{i~ac}|Qe!rIM zk;a!RtuJSkkH6FmJ$_9xD)PL~fUAqtfRdN8 z9yXY}n8WVHnVM;x*~V>oF1t>VXLauNtgB0Nh5$>PIZ~U;OHJj zB}HDWt2yAo(&YJqN5-?A@lXd)E~^o=r+dvf8;l;V=y#1=#cA@}(|Lzz-fuhq63D?c zzswlWIMk(;lW9DpU=+DYQ!aspn*>;d+myg{fXI3DRC4cUG zi`ELw8{HO_6z64dPOU1EG;@6L>`-SR`MNRe*;e$D_YHf;Aj{Y?JXq^t@igygRHHW4 z0N$#40v^k`krSV~wR-suNg||2x+$8=n1~AMnZ>-uf?{=vCx@VgV{}#+1%-hB*ekY}3%~45^~d3#>}9}i zcO$X$KY(?;*bBe)oA}4!pY_Y2exl;tBvau&=B~5rQ`79dni^|ZZT0UY3Sx{n1a?wK zT`@F}a=!Gqnyu8Y)vh*pe|7oeY{FPnb0p6=fNoizb! zLexFo-8u1#_j6HB*QyG>TVE*pu(W3P%fRfXw?9E&CVg4;7n%rj9v%?DIO1uMv46uj%J1p4n7AL%?#?V+DXe!}2x9jD9 zsMX}Ix8B{w%44QAF*g4tj44Fkk`0bAd#)~kv8>@<{e<@7?f4bQVnL(IGNpA$mRG!H zT6pjyA3!Sbl&HGs#{@dpAUl1a^<@r`y-&T9gvhwFBtBLDJ?rQA3HCWgulN}r&;Bv9 z6_HK46c7^1$W8GGVohIT>+Xn7a34nGr({JHmd4hqsFy=HzrfVmrgnlc$xL3@N zZglhwl19?H+0pkMW{;yUk#4a}r}0vk_0W>fdnw6&3jWN4hc0=^pObcr$oEa+)4Qk=+>2E{DcJ`LD7Kq7!WeB01>p3|&&9{?(q-r1Z-2 zZPH{3m2P3iBi{sqy>6Srd7G0%ei(~<;Ln8t87z(`4`&oe5Zk|%JQoTtPuR-&k#C*B^$l^^9dO^g{+sG& zt`M9E(vi*7L6X{JWhzjRyYzcp+u@uLGx^<|n)SUq^}`+ARy57t&OpTo!Mw}6G>akM z_d~r?;bnzIoCv}GhW$=xjf}<^Xeg3^N|jOK2zsPDe>iKWOT+FT%-|LbPcu)8y`FA> zXiTzk`7`dSY0>kOnt0n(x2&$Igi{SprOwQ5!8~{&#scPHV1qq3Nw@H=9-_5f=&7}j z&-ZC)C_(>u6~N+`LTuOvl5a`NYG3GNKKyXc!ib-&XmkYbrHf2X1;364?+TQ>#htkS zV`ON4?=q%#@z9wdUtK<#^6|BI>mquRUL8h49LQu7#HTa8&M81#|Ai>M?BQ#cs-e0) zL~k~4a)94hB@p<@N&Zkj-njhI4QFa|2ZB^gCYTA2{N=Rg)42-oGw(eqYgYh zSX01eRI&RodUR8EUPdIfsM@^EWl(%lre0NQN!-o@Lh->QBAfVnDPJkQ5l3tSry9A7 zn{+myFQhr`Jy`JdUHc&P%P#Bw<(0^ z0)$gHv1HAd`1LC;xA1!$I3EZ%dDYr1*Zn`cDlP1NF2Zn8M&WYUSVOQ$9ZNqpha4w^Ptc@H^M8)W~?7X+0>LVj#I>A=pyU&R0i!_p(A z@<1g)xZs`6GS+41D0r{Zg~H{~(lp>K1u+BZV`q9eZQ&yQiUSFT=Odpi{nm#&-Ns5x zeWa0b@5g+S;Hy)$tj71W{h_#<8qM(`4QV1#Zvp6H>T7A2<`JFt#W!Yv8l|BarIfvhwv<&BiPoF1T;R0pqdNzdnVVp=FZKp)>YxG++>j)oOBn zHx88I`iLTi{kGkZk>gSFM(m7P9vB!47B>&bNYMhyGz^DiTcK0&{R{;yOwH{!8V>1| z3P^4~mRAy0XRiisPilQRwuIQ~o;xXHr}qCQ?@4~~kl3%<9Ee{7CBS6ImLKenfskoF z1_DYUS|vr^C*$->L$lp|<9`wJj`v}i3@#$$a-;$!to!4~_FRqPxBU9w))aD7UMzID zEZ}`|EG*tjZK+4D{8;&~E^#H)a4e2+tehbd^P4@IF1@42m|JQoL)*-r0Y90~l$Nts zb{yUzrZ>`O5osp3_fJ)l*NBwA2qkIC0HI1c9z)2^DYJky52ND#HWbS$2@Htsou?I} zcv6R&jtIy)IU6KffsU2-oU6iWM0$A^mugK2g1aSBAUOzB~o)ucL${jxD#|2->m?2ciZMyH&NMPwOS0y z<>q(PqXy34EQ)Zgh6_?8d8%b3^sz>CT7-p&Uo`E2u0FFRC1$$@DCp$qNZVJXkAc-q zfjS#qqMP-V;x!mwwMRz;Ae8REG&f0ch>>!H6M&TVp_2DAmPoZ9z&Eqo?&I>e&&41I z$&`R_SZ9whK%b*idfF(&@elxr)$rO%slF+*4I6R{8!`*|F?KDiNfJoSbZe2h+u@8# zD8QDiDbf;PsrJH6l_m2b516Q^ShZChGUdHKoVsd?i-mP5d|OBmQFtq|3)lB#W{{yE zBJ!|QJFl@UQvlVbBrPm=%xK8d&C})SVW&#I!=&!99ztbbYV7+Voc31^pBSz$e5xh zRoX2PXGsM*FxScbc0E*=0#I(bVhqXKNXn{HC3qjzMq%U`B_uC-^593f58ZB>Y6uNO zmWU;BQO*OpI(r-cu6baV_ft_noa4M ziTqsv8R+NZ zoPwgf@fRKmiOi$vTJbtG(X(Hr3bzV&Rr&2DZDNdL+83Wd=xv#1!U@mIK0@?&8vo6e zvJ}8DH@jXk+T|sA%88P|WV6_-^9xo_!oiGn`REQREP4*5RfZr#LGHN9^p%CGXX`wI zs-MljROVFaF^@@=2)HLvpa7^j7mJum#CrXy%SE-_6o8e&;07uC^`kPU%Iwwo!%4W- z90Hka%t4fR70&?}yx0iXg$NFvo;m_QO{j3wVC zKh7ilh0U<&7VUdc@3yBH@I~5O{SrS?xx#%v2(W4iU_%hBm`;bMEh^v_Ay4jt4V;|X zfb)a?T_Ga@$H#zRh7)!AqK!#yT2iGs@)xZs4zk_s zQ%(YLFAgN8pip44_Za3BtOmdj!N2KMji%J1sfdrTWip0Mz^OR?SwF$c z+ohgcqHqC{fCXlBQ3ZhDp$nCsRH@$VcM{R4Ui#LHX;|$dSiEP7FbilW0roM4Sv zq=oy+#U&%7Cty|qve6#1yJw45unjd=D&QPku)F~(Un{S76-cM!)If1-iY`VP0VebC z&>Lfs9E9gAuj5s9GNyZzF8t5?lbrEJ$~}*r_&$`WO^2y%L03puD33vyy%p%HbOfAN zB#K|QVSEkz40*>ufwBVlGM68v9Zbz|mX#W^ESKBwIKi^rfhBqfmg7^6%2nLx!Kv_W zFoQ?gra^D%Ymq3{E7E=^izW`P{FJPeUws!zIKw{= z9NNN07MJVA7dv)!)&O3E#c3SR+xFrBA}^q}wjs+tSYn**z2@f6i)SmSO0qRqbUNzm zG-AWVp1`6u4Hv9;$RHBS7{os#1s4h*NM!lt(*HJ)sfz<( zI_%ki)zaJshf|Cfd_`9)u{fcc>x-UG<#Q0eKhsErc=+GbUrGdL(z~=1LxQ}TELIkz^>CEG$Pq-LC#3kL5A_0>?xBbTf0 zgE*+L^g@`ab9g&kAj(;%Ab4=nnSX=2FXGHIaeG~QUV~(iLHal z?sTlmAX#E`=uzAveup4+NR}@t430|%7NzB5sJjPYTY{uL%6OJatW#;T1Q(d^!;w1N z3;@pxknwrNgKU}>=5HOYe?Dr%=&^%MGb13|E%0en6iRRT_zk7#BvgPZ>7GKUi;j{d z86yW$f{9cDBOQF8p?9eCoX94bte?U1RVc89{+uIQwQfcDP?2q3X+(>Tu{CZ5!Jo3| zg*nAq?U?=$4&QYi1A{J3$yASI^fg2dyFaT3imjg81Fls+ND7+Vo!D3*=zp5jEg9;j zlL7Ro(#jH6G*vTbJO|BPwYWr-l} zHw)4u40b3dx1N5PuAg2`*JqbmkFdBphVD7Hg$M5wAUG3pS*Inj0Zah0-FwFfG&*v-{RA|4zv)`dqmpiAF5fChDtY=<&Q zL#-epNQ)}>h0KXE>RGrls_qISV;?4w!I5=Thj@9iwmxt{)?poI#si9`ruX5^gYp!I zqRACs&mdp3!x_~&-kv8YpI|78GcmCZA&H(+GW4kDBYNMaPdIyGDu^~GKg|1r6aZ5| ztiRx;>#u6)GeJ4`PpK*)97df5o^pLx@3VXkIq_-v_AwIL1l~I;L@S?v^;Qb%5I7h& zT_z3t!`1aJUMZUL4ont3eB@~X3H3mut*YMR^_a9)b?8GyFP94rzY!(VoseBp9Ydf?4T2f3-P%FshlKKe{>eofS+6-8)J)hh8eWDKW16ln4{EWHGSc zwvk`&z({E=?F?UOGqfR?6?@a;db31p9D0%K?E-!9ZZjgkwT#V7H?L`Pg4YXjyK3?B zugftni6xZDPMf=QLJ9=jMBAWWq$P@ch(R`(7nw1L9nYi`r?Y}lU;3m+*J55i2W;kl zI@$~YSWs(9;4$bKcyKkQo0qEVsh*EIaFdtFhN>1(5J#a6oVcv~RcX9^x$=-HLD`XZGcJm2F|7ZL z=dhgiqW9vK+Y?_r z6Vl6p>0S=XhHAoIgPM;`uygsf$b4`#pRhV7f7!=4VI0Ux4A#))IV${i za3+^LIcR!yt_#ekLUVdpQhk<@5<49EE5}QnUpZ3aSD%G2BSYUYiKNoEGjg#_U*KNnpTL=Xc|3XQl(DUgu{ zYP){*;^g&FA4O+DG;`rWnZNq+K*d<#7U-Ne#1}teppMRUgG2(PBL~vo3ifzQoUF;_ z_*)FZQGRVVbl&7oYD2R|m(m6nlhFbC&SVqXR*rUlO5#w8)6(r_8h{EWiz>=iQ{+?7 zk~-n@dJ0SfK7j0kr$12S~72n{>9RpX1l!xIW8T$&N4@)FFFN7B(abEB{wc@-Fo})H55C@7xK|64ge-Z?b)Xhntns*VamHYcw_Pom5 zlw>A6X@zx^tD4hjJ4H5k21~D z)^QF7Yr0nB#=H^u zk(maNh}|$J*yxt@KXK>0?`TOZQ+Z!B<@Z6Vk==6S$YItmK)~hNV3_;%G6?}}gYGvG zPv>I>dw~84NxhJ0m=_)#eN>$F2g~k_*{8{5vL^bg$YnB_G%ocsKmHrte^fo`{5ZW# zcJm7-+sU=V^{|0|UeVOJ>RtO}51sKVsOpJ5B+_C0`eZm2^KVpU?U^FOmqd}_PehRi z`-4F}2M(4pl^XQtjvUAsvxlQQTm|~WKH&n-aDJpfMw5&gXFv)jOP;?*k6Xsc6$VWl zCMN3z8|tjro!@;bvZ3C>`^i{Dmj?nK?h7(3(n;9^cW7+oww)Lt zNdoR3EA1MsmP8fJNw~u1-md)zBl3v?ZYeJDr(v5BFbAZg6!uREIDl2w_Jebar73yJ zzPX>lq_DSx5k>>A8p8oOD@1aye=%E`6@#GC>IQ=@@Pn8qlh zw=?Ue4?OK^9)S|?J-Sh+)*kS3T*y;HS!M)wMk~4#f}y&myPMorzG&GMtEw-5Ab^l& zRM!&wy*8KBm4j@K(<(a;_JgFZO};FdsMLJMeO#hkfnQml1%0)4bb#9M?nxlvJXUQpugm6Xx>=`~QV~W)x*FQ2J$}rxj)E2*%DEFoB@`Yip z7P@PGb?XTg<_vHNfnvNp+8w0Wa}}6I09m3E<8A(A0V^(|5;D_V6<6do2x9g|SWAKX zoV4y7q$s5``69!Y|7c)ZmHW9oySt8SuR@LiA5-Ux5QcACfYhm90XQ6V$PoxW$rFec zt6+iZ(qa^NI~@UJD<`Oo5TbF?jOJ90X{taYjcV6If_f3~5V>0OFpYD>qnrkFJO=F} z?nJQ5C3tY(xl(h^*Px*`4?<5jlZ0T)SlQq()PCjQbq@stXsNF z-K>FhBT=q-J%!!3XAyc$eXyPyxZg{cyx#f*x&1ng5gu$a*3ToFJ;0pEYAaD^A<7V- zcq>uFgTa^~!be=X5Vq_xP_~A_!9rhu7sz7{eEMcqQir%UY+(dv#Hu;~aVA>&`6aL& z+gX{W+BwQ+7UcN0E(0~8WCxU4bJ?K;Q&XMk{aEwj+&Cv)IKWNQph!J$yjUK@&YMdi ze0s4ol6tVGA;rR9p1&sYV4}H=4h=xUmLs)iW;j6$kc?9yAoOQNvY3*^_V z!WTRj1P~|l=DQbwc$*r~%B3s?E%3000V<|4EZO+M4D0Fz-#2oq&@IWsFBmE6M{KZ> zu8sGnlAGKA2eYDUa_L6K-ccs1L>QcyTFZHSN0p6bgtdh9oi3fL+?Q)5=#c|h^=e_n z4`~DL-?6?urx<}p_w4`{JO*p|?1d?JtnXH;^5>4e8>#?g3~uYQXztgszUyy~5q9(^ zh1w9uFkPI*9HnD@d&@GHj=rt<;;Z4@R#`l_lt5Lsfifs9e>rGkBjR`FjEI&RNClH^ z1uNENt1DqoqSvNtGi7Y$kgAOHn$tILdugV>%0}3O^e>P#4b!@gL=n{FyJuRry%iLv z?XleJ-z%oRfHHjF%BpPSBR6yy73}$Zn}!s5yK(vrV-rkD>xd>qIjr!Y-#=znK?Y!X`sm}%CpkDgPz8xNbd#9DJeQw;{u*J&{KbV^KT9H2juYxCrYAZc%)swdl%_6ZMf z=a(K08PbFD3ed?2G`2l@z9uAO3Fd)nJJXI|7UDAMfPu+=SZmL_EFnC))0VBFe%>#L zb=aOHlUcK-Ep*Gb*>q!KPz*cn)T#l_Bk-h+*J~aW5kpeovF>y=YXjVC?F`G-6B_R8 z0tDFz$e=UuZ<5m$$>P{nmhGk^~=YA@Ew(q*1K`#in#b?{t^QW$Msn@ z_|6@5&v4Y|nwz(ElvdbcY~O!QgyJ1h(SuNP*q4`J8rq4evt^k{olob*le;|n9*iB# zbRc{lmF6p8@V&ynL-&6x`wZe8jKV9hb+v=%EkG_r+t<~U6WgBY8Vd{_iTu|8)#=vS z-8M8mR41_nX54iP@0-k#1Z8*)FzobVYMN_8$%Vm~}L`ZPl~K9lDY4XF1c z6A0Z{hjk(;A84);Z4Tw54Tg`Lqy+{oksfG9T3KD8Q({&xjiuvBHu|lh++%+f-xRg@ zk&dHvzF|(+d)+9lr(vgQJ{C`y?M5YU2lHFsOZGnPj|m<9|rFslp~Rmu%S_&iGSEp+livqt;H&;?_lL z12N1Gq)q3DOO^&Jy}6HW1bHJSiq;EK=T(oqEtlDv!NE~I^x(S_)9aRg2VFOqU9XYF zec3O|IEv$uo4I(xpX?-A9cPngXc8qcdyn^v%LP z{8jz)qn$mgdL$U8Wm`Z7S*|vze+bCOoJ)f3z?o18&W@?TZygwRCtv}dl@>_brOdAD zFbxhwo!9loVFmLCJ2AFHn0Miqt!FrS;jr02@13c3*^hunFz{OIx&Iy zHC8^)wh6D+8>Z;l#05jq=TElZ#TiRlRtCdZ6F8_0YX8Q47`aP=f`X`M}jsBKiPCzW*JWoTu@g!5AG4Rq387n=ZF+f3nwjDSS3OZ4zUG$WLp0B zQclj(V`t@k_kDj1LFaJOcCi7w3Y5s2=igGiXfatxng7MQitTVrEVSj03~|{TT9{Jp zDj~|p>m@o3@lNB+#YE}^n8XM<2~jxWfOVJm5Qg<{yP!6QS&6U-r{US4E7+@1$tRmU zijv_<^on><=G`j1D*&C6u8vj+NDF#|)3&*ipfUZkOUIK~S@#x$f?B7AN7i)aXg zQQ++nqZKzn0mMt`O;rudh^O+(fYJe@d!;+mQ!H8|JzfiD_7FE9z(>UdQC&ZLZab!0 zkt@sFMM1TQJ1bj$GIpg6iR8Gt$8F;~-o7ee3+iRT#iWih@s1k;-QC1YJZgwNQ1Zul ztqqv+Zex9zuF!$R>{H$1pyoX!=@-)6Lrjo7ibD~I^-E;p5SzDVQ*1x@8padYt*W<< zRf4;4en-<53Icu1AxBRVI+v(=<4NM}q{*5FVi~ms<3s7Rg%R;w11WoJGam*bpOZ*COPAtnBQ*V-l(crCAW)U!4lx+(tr?> zoh@GAh?9j_4~(DU+AeB}cRZ($qa5)(dy9ZQU}SdO%?{uN{V7=Iazq9@lCp1ByVs(2 zmEO|FtYMHOe3+P{{XvlHR;z|&pI>MU@kc9fx{d%`BzEmj5q3`8R+%Estu<1NXL<{% zt;FJC&A2DP^fsww&ckVTu@cI1uj0B?8Po#ohmApi$0W8XWIMJu*IeDHVXyBM5>M`q z&!-&Mpo7GdD!QSlT4rngtk$tUDxo6$X7Q{96zyGSF{cK^8nto8%1()*C>gwTXoXW@ z%FB%_3Rz3@_t0`tX=5w1#N4TZ5@aY16>u{5lGThlxTl-$tH1w44w3)PcsjJog~gws z{*7>M1EfSRa1$~BTk_`ykB#J4WK8o47<*2AkPbSNN*L{KG5n2Kla?9_iKrs+9&2i2XDg_$wx%OFZ(0h!9hyOit=H|Jw9d% zYgvK2u@gR11VU8HKJ=QHHfPbJexEem9kx|q`;0Ia(znWv-y-yUR-;xNB-%i%|N2FD z6rMX}vCIp#FcaXjXdOP`3>S?np?*4(h@P8oJG&+v0XKdg8Bo0#52}YfD~=J{uI-K| zr`@D6fr99&G}J}h+KID1!7P!wQh^RcbgG3dV@rx$81nRAzXS(6c|{RV+=oPhm$ErN zOhRF2!PT+x!5QqHm?d73V=y4vs5>oDX%|m=o7pSawe)=%oFmx!&=8&7fT7ABOwP^d zLps8#0u4(M$YH2%u{D9vE+2<_t#Sj%T|z{H9hIfIgu|C(C*dL6`Ox@fed)yOahfOY zw2y^qQebcAmpki9d6rYZYbqOkOs46SV`3PJNsO#&L&f8IQh%8u#KvlSvr`-g4rH?iJQvTZ#!{sUj7%ROBMJctUmIUaLCF1B_<>!5>zXm z@l9H>qK{9E4oNca*d;2N3OdgG@T%;@CxcLi90n6~@oi|!;p>g4<@M-2kV4eH!F0#` zE7}7As|&($L*XZi>LW#`fai)tHITBy6RjzNxMpdcH{vB-$+lGWs;KH;_3b*=UaVMXu}eAA$y%D6F=TB; z5sEd%>q0Jwh9IFoWR>v4?NLs3{iN6nL1z~&Rw(cBkh4VQq#9kGFB-q^VPUTXx2`3e zlFMy@e|T`@FIc0&e7Lj<^$nXc!2mB6$xF33RE=AdO`u2j(v#wDGsS*?*V`+9h2(5Z zAq+Ut*7c*Y2UbcI3@umm;?sW^f0-9UU$vr^K3;f+?((v~^XELY{6)IHtVdeV1D@0= z^ND6ZKfv!WkwJTq%@1+yr+@?n=A8FnWK6lycY+;&7AQiK(>_q|CiQ`i zYCE^r&5s&{d3?*)-i5*}77VBW^S-u|aN0tuh3C<5=?H|5p&t*ZroF6yd;j33p6hLH zX7eDum-9wZzF_gDi6HX}c<*%wOOKW}Uonkd{xmogkNh$3=%-B8=Ul#y&kdbC^`<%q zbbI-+>L`$UGh3H~=ecvPW{m+$vCV8>?mW-kb9LDmmSTrl==k#8^V-|YqLfu&=jy64 zU@7))3Ss*4JrNfB+G^iTq1R%`i(5_nmmkSf4Rrr#5o#@5uH97G1{V9?OMA|waR6Hz z;=O!((19Ee_kK~zn!~Ie;AyhMygLpP5DDgEC`kgRELyNGu*DNp^^g*K5+Yh3+@}!m zo7&YLrCvT9vq^xpraNcc>wW@>=kgiFHMVOq^h$rmZweXiCYi_ zYx}pqrz`Q)wFyoWsry`L5)Kqiu;K|*Hak4ng-Jq@i{zEPyatEfzYDsK9MQex313?X z-=(${XaXb0Y)iAZw}uq3I{U*_AO3}4niUUn&)K&X>vTh)t3RQuTFZ$;i z8omBNlS1v6;2?@DXA!kYN4>1_6G)E2PuL<|5?N7TLB`}iJ3uqX>`iKAmw2#cF_ zjf!=^Q($7umqwHdf)iujMP1A>-8trz>|7(N-ii5D6TlcQ{NY}xv-_m_nSH{=SBq_D zSN-5l&CEI7!ym>)yW@I%eKk`)B+020;1O_!)!dsPkz$?xv=`1b*EBxbFnZUbKSI~> z$0Yz`c4u|2U9`Oz8NR&pzRr9@kWp;d&^Sp!T(xL2o-dYiw>hVcG0cKH^?baL?6K+X)Dl2$eg}U~ZArmrCHWjpD`$?IG^EU^%99?YH z1C8n&WqJD_8lhB2=fD&};|-v2%DMpUm`vVFvy^WK6yv_TTR}9%t5XmzYRw4($B9F! znEVK%6}f-_VK}qV&38nQYOG{s>~wVu!&*`5_lxkO8@0y&YBHoaLem-!+)|84X(Axi zD+nMQxGTR#2#>g8|5}mR8JeH>vlfAthtG+mrk>0O8RC$oJENnN_`?^tUQ7&SW=6A9a#<$t0N` z&ZOc!M530HhbYgE?-0n81M7hi8a^@0V6T;nGv!{QU(be$Bq_u6YD%NQ_W5YK{CZX1 zN-GHgaM{f9I?oS;1pT%%+(ijDBddDlbMtEqq-?f#!2HNhC(1>682Mi_FG_-j_t@tA z>wO4VzVFtcUT4Efse{{s`Bph7u?EPdKTaf_UB+svY8IaB#;^@D(A47PZiyEvr$A69 zXSOPnl6F?Zul~$1;UEH~KhNpOo$#nbEDH;fiET}vVJoo=k9&s)**3-Cl44!o9ZnH5BY?@ydh z=Q=a>2pi+mNn&xUu^!^P0`W3@PdZUaqTA<{FJol8A<|IcT%I_%SJty0#w|wYw4^8? zdb~g!5ow^%k%&x`F$WkMZVC?$z3b&{1tmSPk_IX{9>7k7G?%lVOP4CZY<++rx7%N* zW6ClGK*fnbV#?m_1g((udyH>5kr-u2o2k#DLmDKCvDf{ZNIXnRyov(IuW`GX|+ zkBtEY(R)=sI4|wuM%n`PYx2TAGC{j4k|ReQ5x|~|wQ6@daJ)i*N{JN?)2{zSj{YfX zyJbU}hpPT#aw^Q97Ddgo0RjlasMfSnQ)X1#i66oj8nA`#`i9naq%S& zv0TCR76iJ+?heLgisTl}tmHn}`Ta@JtY%6Ugsu^WU{0@ed62|2o$xUms52daK8L3$ ztdas_W~mk(>G%nc07rWouz*9a-s>d!9n z9QoACNT{>Kh#Eeix@^XHTxjxvmh{71@VNvzW0V#lR-aN3q;c6mpOuRg_d(;u40pO` zl~!k6b19ewA_dV~G*XL4N|;?xN-Qjic%7n(R$6j#$$0M>|G`^fdQV5^o6hVW8BA7K zL$}ypx`yeYj%_MwvrOJ3yk$G*e{2SvwJ}u|v)$*prpZ-Q+uet53I-l6sjI4Pm`1iF zLkucoGxw;Yy8b!@;IKcpxtn!q)r-!;u=YxFOT-$M#r8VLcr)(i0;peI=4oFP0s6^P z?l;p2)a#V{OayYSD~We{(bVGqkfZQN-kRq>q<^lfQda0|M|NfU0t?8(V)LN&Bds!n zOJm-~0~(VEDQ-`73)f0`G(v4@>6`G;i+1W#Z}1v`l9?6)A_aH8lMhu%I?Hk$2=5rV|} zu@A17@G?O^jIWvSPb26DznS|;;!2&VVz`4tqv21|-{7dtD{OWYwP|7GSFm`dEg9uQSL8*1YK8 z;aZ)ubP-&Z!ngSj)2D_3md@XhOZPioP$M*wc$v`7v7g9nO&2AH$rLC9_iPNmh2{W# za1{~d>w)c_eu{pPM=eidG$(sKB@qmTmV=7}6-@?#@m!g7{Qn2g4FN^Eznx%u(IffO zpnzSJ5CN@9!XkieQ95PW6dm5KW|)2wK-1`8I&AnviZF*c8k^|4oOr8H*v;Ln#oJFf zX6}jMi#DktS#{G^U>fo%+a9_yl$LJv(fmgb`!yPmo`i^i7LFahG$1KF>O{+Y5{a-~ zRvxha@G)(3Nj~IxBs}e}p%JZjePe|V#|t&)j1aKgpz8r1-175|yMFO||8zl=d8T?{ zXdznQB)P^+@AR^xtGY>LejY*kpu`;|(T_A1#{5OMF<2K)#E=L9ee~#}!`o(8KS#@p zw-JZj`Na=zq4~<}3?-Wi180f*4ITXcowyJ2!$Ny0vpd#IARW+^$6KM@DCF_dL!T57 zu-OThS>E#899*0L;w?gH_x{*;OY51j0Ow8Z+l=1kY;f>Sy1~|{`V9_1M+drLvrl9L zIU$XY$7D&#$0O#AX9Di7s~eP56XE@eNRC<3DyruA-t_?{;XQv5G`lgX0 zs-{2>fMOITyydnztWwgx1R+pC&$DaX02L?Dlhz zr#qL(CiKs^0b2@p1wz3BvhiSf1S226ao)CSK_7Hio#cST_y@Fs(bDiXwQqrUusj(1 ziK*IFgd+}gI%3|}_qpa8mt2zrb_Oo>RgcE5npEf>;16~sbI8%Shtwk-%A7s7IqIPN zur*-L-b%SovQKvepQfg0j#a- zdSAk5@%N>D&Yjm*JZ(vPhEXEYXfp%XzHi3l*!_Ze_3x2&p)?M0*u?(g4?q$?1Gt>_ zNwQo$ZG9M{%6wCcK8dItlGni`=a-frL)qd076PNbdZSgz<%LcGx2lkqBD(;a56D`X zH<$!6XRVSZg!B~rfbxl#e`4-abjP}5G;o=xd}Od@0LI_~Q!PcI#pTP#ye6v3dklfM zo__o}vBPlcHG$P_UsfIwWYMkre>;2?__X~vZy5wT5#JK0U_W8$Ey2bRO!LaA&-l{0 zOc;c*-#WEnqm1}LPa@{dEkd!gvEK2Z(0VAD^OPoZQ=0))*Q=ZnhCrc@abt1*1|_hx zhj70W*%>$Kl27gjI*j|#b6x+6<$wyxzMvDf--x)-bh0R1g3~VIXox>El=G8;j2r`A zKw8&(7OMuIfaiY|X9Z0E9UYHa6>pJc@ru$2&>a)s-+cj=vWhQG9Tw6&Lrl`wr)I_% zZ#)(ZW7jqt>Bw%$nY-*+;OU3CP{CkPTu{tUa|5w*)YRx!~6{1pzXb?jTD+iw~6z2DTS5baUJ7oEXH+Aj=bRWS`51F0s zr>S$+(;a~mKF|ry-&zCvk5{)r(Bs^<yN+cVJwqAOEQbRi-pV;gmd-9sZ-0Ul7R zHdbk&x;yG)K3+LRtW3F_4x$y5rAFf=MleSB*na3!H4rp9_V0P(1Tf&9@t;mq%vO!= zwI-6Q@xt?*Y4g|>6E*wwQGuS5Y(^%R=QRB>exmn&OAQr_g^Mc;lzRRL?uJAH$9b_Y zl?xyTO9OgM%T;*PS1|W^V&o*y!=9%+6dw0fD=F7v!&KjXeN?Z=8fy@s7-4`R&VrA8 z4Ny>pLpTS*#^HKk<8*B*@twqFp8iG5n|Bzkf)_PW2b=)i3Cj;XDyr_#Fp`p^|I#Za zgJD%G07vs!i4O1Sywad|12De(&QHM%F#m*LN2#tD#E;!p-)F^SaI8+_zsq$2qe}gy z8P1L8!%Ud2Af&jh%q30Kz`Z{g*dS|8TD#28ZHV(n7xSU4GEKuxl}zUdsHS;^}*g%?Z;e{EgUW<`J8>;2kaWxD{p zAbjhs0Whe5!tAd~|L)JjVR^c0n`)Q2OGmohHTIb(4Y2Lz;1@g<%DRH#8s` z`wx>Ap(12dBe-#kt_S@40LA1t;rH*ubbg(3+cdN&4Da!2(oi@3w(g+fy$keqGQWZe zVT$>!wRE9#d4|i)>y%HEe5vbPb$GQ&5E#^04^~LI8K+wwHlnz~sQAg0yWKX_O*bH0 zPuru5vPmpcuuyfc{o^`^W^)qygt#AE@wOFZ78&zVC2kY7XtjHr(lS0zP0Za`XUs)F zg)bZ?tMJMa$i;N)ue7n@!7JAaK*Hn_kQn4OUVNPLL6@|so-)-00*N63CPdgl!o0li z=Z)+0XBxkPGdN#w})E)51 ztBXms8Kx5;8Zq*;HDqyt#w0n4P@?Y`BzsM)Gzden0VXdnjLE$$3>AZ{5p#9zvtd_B z2nyKfDbsH;QfeMxUAfDB-IxyzPRJ&(YU-C<@Jji+|DI!&7_$Tq)p{S#d&}<_nLDJN zvq`iuvDSe`=Q1o4a5^I9qO8LTU8o$36fG1wTab%6JAx*(MhO>P{q1!62!vkkA?bVE@2(Oe>0$6H9T^1@V+YN#cMyq~nAk#LnX4U zIutr$0snpI5;t5*T@_rKO{x69y~DUf@6B#)O3l@-{>tlhdjIN}8u?}G%9OGyCfBM} z$@wjgDBrKgyTnP>Wk@G9BAN5W(h4B}E*o&b&V7LjE;F!V1hCLqybxGl-2d@MhKz%! z2Zkp;#Ek1>zP62|bo>S#FpI(bLG<|#`k{q5GfY=?kaY+D!xxtwRKSK=F5{`MvSoyl0JK(=CkLxt1r?;WjVV1u zb)7Lh_{s;J78_WKa~VV!acVQxlYS3Bjn;#VLTNTZ7bWH>%m^>bZ}v)XZX+g2bkwZqXv2s)18(N2 z<9^5zz@Th*C4AN$gGD+!!oxd6BHVW7w{sD!cW~#q7Qw9CNcC_ew#9avcj^l4ht^^U z_}jlZIM&QiIFFwtdc_}2@AMC_w7q1`84q-5-#FaAzD4JVZc?=lZOv^uPJRD_MR>Z{ zMFFF1DX5{lC(Od%({kURh?|I-y3+Yt|D~?$cN+h_bbt#8Yz~ob0L?zxLV;;@{{zAm z4Wx$L!HrSu#6ppbqS0VY(M7b*&&ehc6TFZ4Je*|f9Lkcs7Hj}UnpUjnt1&v*xux!i zar$MRl@sXVq4YH091%T^O}dGc0>i~V)YZQHOgGUMTv!}PX$6`x3; zCcAsk{umjd7%M1xdPL^_T1*77tkyE5Q8RUl+y-!FG0qD^LxY|HsU0|IIYp)Vyn2=q?)AQB&M~*SOLo2T| z5Xv{-pd*u^%VDUw#gtsSobzFHv^@hLb_)|O+U@%4*M-OaLx5l?fuX?khHC)kRCtL+t+)X6EHUvjFFx*Z56T94p!Edd zLviKW(3i&|{UkHsfDuJP?uL1#MJ&p+IsvUFP(AfX^=pjwfwq(N`EGsPMW@92LuBkO z^IHBwjm1vIK55j&5Lex&JcyhYeWnD02(x^r^rt9q4oV{8c30W_z`(mzpF037WCuJ1 zKamojTf~){zp$gb(26|Em@xO_{;CyCRr-!jJk>mZh7h`* zbM%FYotyDMSLHTXnZW2Z26bAh6ERAR;&r7PB`edl$V#$0-7IolvyG-JXKSS^)77)h zO0Vm3qqCKlYt2^9R$m@q7K0-a7z0e%+-kM}@95zH(YO+P8Xb2Fh0p|a5N)adxaGr%SdXDqREs1hG6kBNhAS)=X4X7hRcooMYAtnDUB5av z>YvS2AyVi$h*TN$=RRuW1HNQ?$zDw=)F5)PPRW zd$#-#z%7TO1i!bPLkGK_jJVCvclh7DkcKtgV;>=Rq`gjHMc_pIhy2gC=sz9`Nmyy+H<3Jq&In2OrgR` zMfk3Ev`V5*6g$_R2lv-#b}GuCrMkE<6jBV78gRyK1fHXgWI~wE&f?Phwr^x^2MPQ3 zALb9^PFMk9;M}1}%w1f_&BfEE0NBrsqN1uF?*o4MP|Ss*iiT(msK9Ibyfun}0sY9a z`hexKlnoUF#p$iwVFv1CE4se~iAB!k#FMu< z;_opGaxU;+*|njHxhCuHi{@UQNVadg(9F5;`iJH^>}tdS41%rZ6k_^CT!(jl zV}ibbjCL?1!RX_7g5Ha~iho9l(C_ip7LRjp`a44GImJ{dac~OuQ1G!Niq}D)la}5dT9u_Q{sZa7G|C$N^&?FN(j*g1tKT#WrC@CZ6wr-dlUMD@!x8x zT1~@0{fV!ESeZs7J^i>S5imp%mKCeAq5E(~U9q&Y$?p#XE($<{69nNu}Ad1y1*Oh>cH=5vQ2^oRF3i zPsDa%stGM!>)XO}5=_yd)_01@AIR8wGv2s^^!RGOqPkU;x0QAhSvn7H?EG{UX!5%x z@5ct^;HIJ;`Ju<3F@t2chdvMk4SCtvdglBnA4YRhx+QJhDT5hc1C-^>$71rZfkCEz z=pAY6O_TBQi?;O&9e`9$W~S9)s*X}!?l^!6L%Lx60mx~}XsPR_&6yzw%N#E23Qjnz z=$ucdh=#q57n%;0%vVoIPUjBjL|HoKPvG7H<_t#LnVoP&tKkM9hiX(aTRIR)A@|%& zCt1kE5r_OjYQx6V6kxrsHg=X9vY=-rJclXRF^<6s8yh zNLl6?K6X#1w1(3$}4{x{>k#x@l8! zrKtBaM#IP}Et%mLl?&RG*9V@1JSEhqvxIAt`O()7vqsb6{{z&h;{(h<|HhNBU=X$3 zcDj6Et0&5!I?bW+&&An5e| z;~H59*Gl{4PZccHC+wJK#znGwgS}g-giFw|fd9M9B~=d{UvMXG@5g-^aR-Rp?|AJHT5+$JUJ@Gvg` zB*dI6Z+yP`8j2v=iGrNX@hY@kQYcU^Im~WMEBWFx70>b()tCdN@sVykd=0+qN1#W3 zH>zHKRlobar*P-)oKph(%B;PcYj;^Exj;I%SNPd8%!2^Z&FgyNDKg)Ks6LOR=V9b4 z2Pf*mVhW>NgJ(>%rAoAjl-!{d3-|=*v}Qztx{30hSw8tc_I9y>^mny2a1Go!r()*q>PJombdqT=T^gYrGkI)gR;L4ch=3ZyK2+t^}FPj7H#)M zojiApp%x~>i`^Eq@8-|fS>^(L+bZaXz;Z!#OyJmNa18=INECFK4{yGAtjY0v8pYVx zb%OwwI1ca_uZFq!xPo_wTq$o#P|5KAK;E-upuPuwf1|ejQ2Udm3YTK;Q_ow=3gV^i zi)O2tfS{}~bMH|wFqU^Oa`G+_#PK}`H!0KDE*uAOxuXZE3z%ek#tpc57JH!ik*@+r zu4zorE~#QhV;ZD9+{?mA^ZB}A+dms@At1Yk=k!+!@`kw4ayP)Ya?aGwId+Z(!$+pl z6rTimmaY8wP{faQWeIqTv+v7Y{>U)WJm;02(A6#%*dhH*Q9)gBCeSOyJciKgSGHX%5-_( z;3jFSGGrP%wkR5@9^jp%kZx%W7DCxPjs)V<#LI( zp&g9MaM8X&6<(uGof3gSq!gF~fzwA%>4NdnKG+(PmW*km^GL>wVCZA(3r@>-9n95Z zy%#S=0B@!arh~@h;aoj(F!d9aqA(h6I??@BWa<=J7H2A)z=oM^lMl>eSA!XjA!C&><%?6EQ2IdrddptO6oJC654ay zXIJ;yx{IFn*FO$pvgQxy4qjm}TFVsF*s~*RbHf^VS*)E zqbD>6Xf{eBJd6$3z+XWs{H^3Xh!KpY9YVTgAfx`=<24^n;lA?KZ-ku>5Xl!?$< zU5-SV*V%j=2GP(=jyqS}ROi*-K58j$1$6nbqZAaVZU*jJwflcr8C>EnDHwIBt{pAv>*X7)fIM=x12Q;eg;I|~Eft<8-+m@(b)S$Zvc zss4H)Mz;mNTqE#3v$4wY;UzrM$N5ME-a6hBm)~DN1>R%74_plToPg!o%&Y|~zHdj| zoH(W}vR3woOVH3C!JMI7X5Bp=Pz0$3QEKob6j4oqg}}Uoj;y2UNHGCvn)tj@+cq?(mvAer1iML}vAp0(y169xNry%lDPdHff2ZKz_2SZ}(cF?H9omKS!l3MoE~N4pvgypVWBu zotrAb+ zo5a)}`dS&yvm^U_unNG|GJKqu!Dpm(iE=(9#VVkTPA!Nha?=OON7#5U30w-F6txaw z6M}cg7ODuT)Iv^Bf8QUt>?GIIzM{`l6u1qpjE^REEG^tahMfXSL~UK%HYPqAz+zpK zMN4w+;2fR#SWxOuKM%{DgEqDa9WtG~#EYGJ-r+1&yO5nH8q({#K16%Cs|G6@gf=@x z!+OC%fEax5>{F3g4h>3&{6WN-)&s+ca#vBkt}_D$N*ekXZ+b1Pj~cEt!A-M^^Hkni+5}8H&+w~pD%Pk!<21hK&O}4=s3Nh(xW@&srX}aHKFh8 ziIaJw5W`dlWsKy&$(kOqeLBJ;jB$_jQJ}iazIg=bMBhrwq38WboUh%>-3mkF4(n~r z(Jfa4>vz6tOm&W53>hSSAH;lCJ-qtq)K2C~IO7D0u)VnD=Arkqh*Nwu(S*bmIu(ZM z%rrDjgZ-@Tdb5`^rxWzlzIRI(K_zrgJA{38KOV1I%p81C7 z*D-$wb9Ks{avaY|E_ub0-IBdccIiJ#20pSgJ^?p(>9`!LtYg?;w^5VbU%PW}95tdC zH77T|I>pju$n@`Wv5WS6Vd8_2Y&uA)NBy@@75>>?>>mhd!OfK<`5#{u&F8!0=}~Pp zcyUJmE7uyNo20Kk&kjFiQLk6Hu{$i&<96~g}PuE{|}-N@f!Hx`?-CnlzE5d zYt8-m_d~ej#M-@X9IS83!(Bf$P3(P!dY)}fr%LKE7|xM+3R^@V+=Mx8Zs0ca%O}G| zrBDhBS1v)Ae^cC^p^m~kjV!`PFc8W(bJAEadtIXp?=HK|(gD)f8gg+)&4HLMhAI3ZogY2-yYzHhL)zJm;CqZ?u?8Tx0BCC0)ie1#9rJx>*DFD7fX z0CE*PzjPISe%UH!9GnspEKM>WGy2ARu^+k)!RwKVYXRg$icc-sdCXc!kZ>DUIr$8en-MP(=d9RQ z=+?IWfDFT+|*Oq(xeZc+eH znsDOj z{0@=yhxOvVJ(UwWXriqhK$1=P)v=Ta?I(?8@U{P6w_R(FdC+be%c?YQtiAw;4}g71 ztr_(cTJlkonU5w#51WO!<%>_K=|`2R zXFz-U=&#G%T)=wuvMYzq-pYVM& z^al3zFV&pkubZZyNzhpa_f0*3A~YvVyK4WjHYsF1QVOfV<#kkx=Tc^se6eu&&G!Mo&Jxs9D`x)CK9H$oE;R^CcxpvP z??$1b06eC^wIn|f7I5loDinPhzgVfL7BlXmT=5u z2x+da?loY8A~JeO`gVPMNrrdgq$V$h2YmxJ~`)AUjFgNn>RZY|F2##OxzUDHXW zg+IA{a=TX+WXV^WrW$)cpRs^c?I3i`<8C8#fU_RLxoXRmjXUbHX@DKfiv)Kpy~@?% zQ*qpJf~8YjDw98>1BseS*bu8j@*R}J^%QD{lm{w9K#N7O7qMR`2c#NOP+>xqmI>VQ zJxkc_9)3I)jL+lYLz$L`=xBRA>yYs9;#eS>Z7XL`RAIw_4Fic%6N1<%-LR8TWM2H> zq(g(Km7t}Tx?#+m1ZQ+c)~Ww zo(i8*uZl{rbST5%0K6FpoTjrD8hrj2w9^C0tt^#wBg!Z{iRXHz@x&#Rwf2T1FvPhk z7-B*-p{nPn>tg-;ala~lH+b?0a+x)k?ke~UTMuXYTY9T)(ZBJQFgeTWL>W)&H>nWF|eJ)S|0?ouoHJO*2_3~~jH0f=-pLG+ZtA_<02iIx# z`905UA)g4ayLgTWGgG1u%?s{2>&*@7LF2f-GX0YBvq3$MuTVJ^(S<(Qbri|`%i@KP zukQIIVuNwtm-3FB$0xe9( zs^hB^s-mC>JE6Q~PND|ZNM5s4qj5Aft_o+Ya@v&~q2(-PSWo^r8OBAFBIdmNN9W@_^< zWx6`%QnQ&@caUVB%793d7l}Q6k+vXiJBAktU+nyx90%60wSVDOQ zfwo18seJ^Ht(2w=kwj~H`NT2I#~x|8V8PuzvOMkCf+PV}Lg=a0fVi%zac<-M-o0@@ z_$&)(wYn(@)mNinV0*N!sul%lonk&dyi2CQ?m}M$Z09XtMP`Z;-?+&cAiH?Sxq4w* zxY4ljB>ASOK+9*_N8(J6W5L61Bgux+cN&(Ul*~Iu@E1+wyUIg{Yk7%njI5&3z#2#% zZ1HJ>%{=!nr0ADlbIEbrilK8v((qzPAY3!{A&7a3{r!WeJ+O0s+|UC%NG32ZmRDZD zH|3!{z&P-T$Zpzkd$&_~tf{fl2Wl^y`CZFXMZxaz)_fj2B&lk-0W-d-wRp}@b!D5? zM(fQzY!8z6`XPB3$(nk+woE{+pi=qJJk$h$*sQn(FCpd^zJul?&~(X0=f6sJZR80Hm$BjZ@>X9M?a$gT$ zLa65iM5_YjIL+C*&e5N4TKkNA0N)En{CnX$zZa(4dxqN2D05-JdQ8)M8(%NI=A`C& z@(!I6)LBj^oyu(0MM(AzeUR}Nlg(objM6~Mr?*6qMDjWckmdZs#8V{z-cd*g3jTrQ zpl;Grh5C0U4s~NjBW9Pre&(SS>2^pZieZ)1igeZOj~-!`YoXV&yc?Z_+r2>KPQo!8 zZ7+3GC09reRO7D$uYkIk>$PT;<_vHz?Dc(#mu>dl=fqI=l6Y@8PnBjS zt&Ky^{LweHVnKV8&NhO;*9Wa`?_F^lWX_1@S!^cGPouWS*Q9{emyBnV$5DIg82UwM zHhsOqhfpKsDk$fQ6qOBh=W9_j33u2!t9-iOD&`$|;`_P_=H;IgJRcrE1p$^bW(VQS z``|bZqG8rCAOyBj;M?Qd)HOAAH*@!m7R~JT?tr&Cgvx`YFZui1`&(Ipi_#0(cThWhtI?APWkA7?WTD3|XbY=01=9DI#bmz)-9Y{N?=ryHW3_NuNsRnaiIy`!suz z^z$x1lfLlmQ=eXV_NlASzvMi>-;+kBhstJY7%jY(hM~Z4>0^t8zf!u3EYZqNDcysX zXyu;t9T1FJ(sxLtypg^`V#W&TJ1s6bAAN_!Cq_r#IdR3;C^K+D5HcaYbBSUeTS zp8_&)>tBCtK4|N-A)2r!S41KgCbtEFm9lwR)CwSQ376z9l*^`#u{vgx*TnP@vxE)p z_h;L4A@D?ykJN0syc`=}Udg=OT6EMGPpZ6HuPH<+5x8_v?A4Ov3Ow_xdR+2lW%z3aG!fVpdj4iiwHTN*8gIdq$V`bntIOB=y{0atEJI{i3@GTdVP#SYItw zF=I`PF;!t=3Rw)9!M?~t0KQu&6qXKD<|}F2Ed9X+)X_;?8$7-n2BVAz{5J6z zyapA&54QJi-t*ys*&{w`Ds$XktJSOvG7zaUSpruNWI5 zL#bIWO`>Y^MN-DLINaHIXQ)8eKBmSz1XIR6ZOd0p2)`^*S;1a~5L#3el zbw@kSNOu^31Hk=Z#=u}!g<(xKGUM&?(s{FesyrdXuRl452;Q?xkrl>CSaB)K`^nrQ zIw^48-jmZbOhEQ7cpfD1y8g5Qi3C=B|6&rMqS-d8oIQbl8@8zOu#;A2CZejX*^sDy zWoDpRp^BqM7&0-+j7YXMbw<4^BZ`Cd+Z^^U#=DApU6yA6Wee6;;rN%q z779t$w%;E|PQEu{Ud0W0;UCO!P0$(@wCz`w4v{2bH@1^Xve|%D^4a=&D6JeVvb^SV zBaU5&2$k>cBGIJLoa0T*=PBl>M_q7tI1NN=h-lkph32{~EyfinoJe}n0)zvQm21)% zjKI*EQw$Y}<`NA|Ov}w`yMP>x9)#Q+FRd2_G4#&C2Ry@#5M0pvV8(MyU(SraER#fM zjiUQF`f_&kMJNg8XU=VwUnH}k0hXqSqY3;UQ614cd%)}()N-d+TF4v~6be<@k`wX@niZzl5E;~b2R0;&Bd`gCZp226hE+0qAqb#kc|(3{ z_xs`ot*|VS$5MElsE`G4YQ`@Q_?b#tv=a}GVV>j;@elY8ExST1Qa$L@0*=j==9DmU z+bSN>oH+Sm1ZpNTMGL7Y>!&mQ>MSHyR0moM;W>3yT|hyiq8JT@C@I(hvk z`HTF#a~15r`-2=tccO3slmDg{B|nmf&jnz9jlqPZYksl*%NR@`i}Gaa1#ezZn1>=h z0inQbTK=Ur44de_>J~5mbZl22)`2r24EyL(|Z_#%C&SIa(9lu|hNS4WEN8fbO ztkEY?>1g=ZBNlZ9)&g_(P)Kvj!D+c#Xk~1U?+GU?7M@||ep*1R73qOT=I_cq$d8E6 zz~+)rbU=zC2y!Tsq>&N|CwqEyJZ64AhT^DBTN6eQlb^xKGtHKnsvuWy$aX238a$A2 zN>}|ESfI<}(f0+^@I7TZFmc?2tj1;o4}o>a;CLY{3ltx)q2xjNtM{da$Bo_vTjgA5 zgc7m3m8`9{ruI2^TLv~iOkzZGF(L<|*rkX1s8UAc9)6=1#T)XxvZKfI5qhD&Yh6M@ zeca(iDiyQKF|%*Gr{r5)(#Q(1+??ZKwtn&*nip*i?^qR zyuV33@LyUPCbvv(zWU+=OF9|TpYaz6I;4cA+ZM^qRhp_f6G$uXNtSv#5Rct)XqLS}q73)^&a)*w-XXT6#Yjx(&3x(*rt{Y)A26&1E9S3{rj#GmuYhasY41=SQ-aG9oduH$hpn_-`S+kD=?LQ zqfW`w%kH;MXkl`B!?WcSzHx;_@A_o}Dfo6900C$EO6jj#VF!3^w(`?&en-zio?ZrO zINW>i1g}E-rpDmsWw-8^rYT9CY=i&`5Xi(rh{HZay5+Qq|70r2-4vyDH>OYuBYRf z@(VtFVUjOAy}Gm{hw!^Hp|)eN^qKEwp7G1Wa+4E6yE z`Y~p1)Rs=6O!5IH`cWIA#YFsd@X>hq{^+f^XtZ-P7rZ=~QJ?B?*-(1pgR1fhkrpT7ctf;304 z6HRj_2^i>)G6k?|P1p9L&faVu*3@$z?y@B7?b%GHUQW8&^S!baJOG*=gGMz;lneuX zVp`;Y)GYPL)&{vEV4Xxn?97R?E9zBlRc1=n2#_1#YPq^G?rbg^;Wba8?>qN68vm-X z-meeEqkMeDRi;TkI89tcU4vGf?udoG;YR#}va;Ry7}L^i^>IF`QY5MO@IU0o!$r-(nm7H z><$>q{vBC0<*H~rcQgjYpIvPzkS(}SX`8VZ;jl(_KC8s_t$CG` zCK}E!2*`^XE7rviC`vUW($bb_{3USUzP3#^UDnZSJ5oqaz5h86H{=PHS^arMN`)8V ziJ`UG0ia>ToHy~XztmdhBE-FfEeS^YzJW%g(8C}WsTg1QB zGG|3-Pk!`i!3y(|aUz=qpXys)r7B#(SUfZ*j?^1TRdy`RZEnkcwFa(4g`Ta2@SQ<)!NHgkWdf#OS^m2Swo(E5St zFCve!j(q^C+dyiki6RuLiG3njxo53pj|Fuj3UQB=N!8@Fp%B1i*tcXd?aCFd$SqKl zL*IS>WcMFZRv`+5?A%u6=OUMHvkalY^}(;m+zb23pWD1e8&W`dpycUB2Y31+J{UuK zh%5YUC;7yl^6U##rcCJ}&5KAnj7dbzD1ElM2q{6Cx@TS#0>D}#!p_XG37b{35$`os zf2to{u!q4fP@a4z%a*Fw0>H$>t|eRP|x@V8I{6LK2-K*eQ6Ufvfw4TSs)WYTSq z$acKWyv||UXM6a91Sp3l*Ru0N_m%k%IZ@uA^0z52+3FEwblMAR>Ln=!HWxI`2Cq9} zqDW+u1jedX0F~^%D0!LiqV#3b7iBM#$-{NijRZo8-3ql#jmtDeye>b>oit;S3ynKf zGOKBrE`s|X4J2(%Pi==cLe4FD*a$th^kF0H+_Hy_q#H`jt6zr}srso(QLDV_%&to< zD>^-;6`GzZia=*Wu^|6exxAWMRzt1fXcPv>?lM}0VM^LUeMZ)tDm%aMW)%ZLH!?zzx!0Q9xA#3xHSR$v1Fl!*xtWpI)#A+u zF5O2cq(j!%|KCxR_Vi6bIT6p!p`*dRL`^=_rC(poKDKFlD!!fUT+;r$rM>C;z!Uq_ zjY8w<+6NUyb-#3pu;tnQ%?Go*=_y+L{qIBE`S+6jSw0;@&m@w9@?JhBQ#CHpj!fnf}eh}Cw8A{(&Knp2s8*D@*JOisC@6UbO z$81I{naw5LX4E0K4h#BA4n?V7Wk>75tm}UMZG$AK@n9T0;@`ACoU24=Z}Fo>k@y~F z=vYjuOF3mQ2|lEF3_3=(EF-k?TEr?sA(IGs_7GZP4519;YA=-JO7G_*BS6VykJ*iB zE1~l&f#jG0$+7{GU;v~t>mRM8UaB}c9_Ea)W%V`@p+Zq0!zxp4T&TwFQIgN2G>1nS z-j3>Wb(H7lsDzWFx(KEJPIY+bh|8JU_sYE~lMBE9qHi_&lxx+-JYJtZJKwL)r@XwP z&inwVr4wUq!#~#7hF{jV4S%~*g4r~kXCp}O6vc@TTN|Nn6RMZbOwdA}XV6#Tzqzw= zufpgB%Ul^W46(6&y}|aCycMG zzV{b+IhL%#MMjd-S|6BN*6xQYqNWeIH+6d_j0oaJIvF|6ef-jW*#4i_9ft4bSLnKa ztg!2PSy9&cwj5$oP|_!xfx7!oNEd53+L-mV30Tk>d88Bnv^y!41s2p(62}LbUOd={ z(F}pX6cTPe>bXO+pJ{oR!YTbb>E)enJ6v~}oUsjD1*z*1BTG&Xs)-=yd-m`{7jAMd zA&A|3#akn9hU~L@|9m)tdgEk|Pf-Use0620_T6|QxeEi{*F!s+0_NDEmS@VdZ`@p4 z>q`bu(MwwRGro=JN`ch z!nE@Zjq%13s$zUf@EWn*gqlB-=YeBP{&A7L+{Gs9y8Nc|H)-xB;BFh`V=gojINXHz zFUpfu__e&>>ik2{qc4n`%X_?D69@K9VxASfzTt)+pW5V_H*43_FGKK9n$m+ocuT9& z&WZ5C@Le=oR2&B)xtn-2{zp@oRMN;rC~*6FLvmwzx#*5Neom6RT_<-gndbSP@;gq- zSlc&d{}~%MuYbmYO6{AxYAFl+lFGNOM4|1o#}@Ek*lT7I3Qlp)x2Q716|d6P-is&Z=51%4^*s0GKnmlmN z`|lupxL(?UXx>R`i)G5C6lE)R681I+xJZ6mb!tM;_sgdU2)#{}=YRe`!59yr zZG5qd`zQ&H^#J@K43q;P#?_B48 zD>+Ne(@);C9~aO6J^Pu3ye~!Q5Pw<@L*2TzLZoy7<_ zV&^U~(l4@jyfgLXGn;>xyhl+4Guvxv0c=h1yF&xK#5`FaimTDCJez4u>YhXH*GL2a znRGurP0-Z855&Ynq#N+f93#XJ)>6DhY9K}}i6_{&4Yn7IeNQ?vIjr@TGD%yP7{(T@ z$vdSyJx~Vb@sQ|Qi1ocV5+K8fqhm--Xw@Q_W#FNuivce{XAId~fWh8yyOB?+ZQ*!kLL6_s>##eavDW?bW$#z=o)^lZ4D|f-|le`#= zCLCX+Oi$%Q%iJVPs`v5((4?u=I8;j5iojW>E4%WNSmb|3a{9&SVS*;(KM%C@_{R^s4?%Y$>l{B zizHc33CK)c63PfKFNmStyH5Ufs9459!*HHyOAafFK;O`xq@|ZYT>ufmjmg?-m|zY} zDbx|B**DdLFnJ$%0ONuVMX1N0rb?T9vgNi_Htf1O6$kM#py&xm=40&0T zb4}(%x)ri}#j>8Dme$K?YG)`BT(8A1?`m6AGc-)LQ1rgd1vHbLpshAkdACYUp%FJt zk-0+URAO+=`+*|q%T2XZW^#W$p=~Lcy8KT!F}T!JHwk!-)!w>nfhvZP(k{Y=*zbFb|N0%XXhs*f%XtO~JVia*b|`9biWb3mH$3!0-9=WuO<# z^th6_G`kZNkXuT<#d>1SaRZvDq()caFTM02Q>5dB+@79B_2gNMzZbTXzO^n;cp#+< z`+Xa`L5DGeCq7+N^*r7^QBL$n~-EiDG&u^K{4=9!isk zib=L_=@qRkzWWD*v9xJLh|t(o;vJ>^F4P3gb<%4rN_=Ex!8{Gp-*h@mq&qP3L32yx zc|`4ALIMtU1!6b5@$oP<9Hs3UKm8Ik({)!~or)y!%!`7t~=Gqs0AA|f&t#=3hU`u=$^TIE#$?d2)mgS;; zQG~%8@(HHTy;PmqsEym$MD(1VbtYonAi>~Uat0nF?zWpZGiQRiM?kd7^tk#uv%

    J@9XNCgnLo>Vv)`V&X;+C>bIAzNzEL3b% z0Dga2KH_k5RpAM{3txC@8H9Y;0LY+xBOPT~yj?JF(GSR#tUGu@bd^)hQt9N%Wy;Y> z6j8$UOlvBq5Ia(uPAMEny+-IVjxR(~t!~2GfBaCz;@1 zd>!f!2x5cZdeToGgD#!WbuZq|azLu9;^m7srKU1pFuqNk zb+H5~Yq>Q}pk3C|l<Yg@=jCU5AwjRs!zF3!zF{?;FA@6-cEKY?G%gE(9eh0#Ah? z0YITLrCOn78$)Yw!Tu8^1gSot09d5xrv_k69+@fHsRk4u!vvs(W0acUG&nL8QyoAi z`XVcU1*#t_fzCIa`|ZZFY=b!z3=|N6ci5@nuF2EFS09O5uBGUflzL^+Do^q;y6mXJ zLA}bv2wX)aZNSjA;EZTYeEs^=uXF{@IMQBvmA?_bjJH$6+_j93SWM=rhPkz1s4Ih> z@F6Y9H(kF&V9Y6La3}*~wtt$f|nAlfYwc)o`fYc0{O#$FZJHbFldj zwc9?0iXh=X*&QqF8SKErAXU#t?Mgk`nathdiO$C@yngt)}J z8hQkynd)(~dZ+-T>t_eb1R$n*yBn zDXoJ;OerUcoTqh3BYkfb=9Ez9oUPplI34eC0?K8~i&U)KB&iH^Pm;3mudu0r zu=S5IqJX2Fyu5}H@^VB^g&8zL0JN?Iq?M92Ru0mXbB8H$5mP&vdDSLj@1?bvbnU@u#vg^#90q1GN)@%+e+d1wEL&81j&ut$fB*^^ zmCabxsHO=5344cR*BGw6OB@I2(?0%0DnGR<`M8RPIx?`(zfDm4nEg|?6l4v`q8WY@ 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�w0ai=>+i|deUn>QWpx)ciAi{|i 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

    L}Cf+JqR!qc3#4dbW0meqPa`g9(X{s$i*; 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�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 literal 0 HcmV?d00001 diff --git a/packages/core/__tests__/generate-three-patch.spec.ts b/packages/core/__tests__/generate-three-patch.spec.ts new file mode 100644 index 000000000..627418f5a --- /dev/null +++ b/packages/core/__tests__/generate-three-patch.spec.ts @@ -0,0 +1,260 @@ +import { existsSync } from "node:fs"; +import { cp, mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { brotliDecompressSync } from "node:zlib"; +import { describe, expect, it } from "vitest"; +import { makeTempDir } from "../../../test-support/temp-dir.js"; + +// @ts-expect-error -- the generator is plain ESM with no type declarations. +import { + assemblePatches, + blobHash, + generateThreePatch, + parseSourceDeltaPaths, + validatePin, +} from "../scripts/generate-three-patch.mjs"; + +const realPatchDir = join(dirname(fileURLToPath(import.meta.url)), "..", "three-patch"); + +async function tempRoot(): Promise { + return makeTempDir("tn-test-gen-patch-"); +} + +async function readRealPin(): Promise> { + return JSON.parse(await readFile(join(realPatchDir, "pin.json"), "utf8")); +} + +async function copyRealInputs(packageRoot: string): Promise { + await mkdir(join(packageRoot, "three-patch"), { recursive: true }); + await cp(join(realPatchDir, "pin.json"), join(packageRoot, "three-patch", "pin.json")); + await cp( + join(realPatchDir, "source-delta.patch"), + join(packageRoot, "three-patch", "source-delta.patch"), + ); +} + +/** Builds stock and patched trees holding exactly the pinned target paths. */ +async function makeScopedTrees( + root: string, + pin: { baseFinalHashes: Record }, +): Promise<{ stockDir: string; patchedDir: string }> { + const stockDir = join(root, "stock"); + const patchedDir = join(root, "patched"); + for (const rel of Object.keys(pin.baseFinalHashes)) { + await mkdir(dirname(join(stockDir, rel)), { recursive: true }); + await mkdir(dirname(join(patchedDir, rel)), { recursive: true }); + await writeFile(join(stockDir, rel), `// stock ${rel}\n`, "utf8"); + await writeFile(join(patchedDir, rel), `// patched ${rel}\n`, "utf8"); + } + return { patchedDir, stockDir }; +} + +describe("generateThreePatch validation", () => { + it("rejects when pin.json or source-delta.patch is missing", async () => { + const root = await tempRoot(); + const packageRoot = join(root, "core"); + await mkdir(join(packageRoot, "three-patch"), { recursive: true }); + await expect(generateThreePatch({ packageRoot })).rejects.toThrow( + /missing pin\.json or source-delta\.patch/u, + ); + }); + + it("rejects an invalid pin commit or version", async () => { + const root = await tempRoot(); + const packageRoot = join(root, "core"); + await mkdir(join(packageRoot, "three-patch"), { recursive: true }); + await writeFile( + join(packageRoot, "three-patch", "pin.json"), + JSON.stringify({ version: "0.185.0", commit: "badcommit" }), + "utf8", + ); + await writeFile(join(packageRoot, "three-patch", "source-delta.patch"), "", "utf8"); + await expect(generateThreePatch({ packageRoot })).rejects.toThrow(/unsupported pin/u); + }); + + it("rejects a pin with an unsafe path, bad hash, or duplicate pair", async () => { + const pin = (await readRealPin()) as Record; + const pair = (path: string, oldHash: string) => ({ + path, + oldHash, + newHash: "a".repeat(40), + }); + expect(() => validatePin(pin)).not.toThrow(); + expect(() => + validatePin({ ...pin, frozenCompatPairs: [pair("../evil.js", "b".repeat(40))] }), + ).toThrow(/unsafe compat path/u); + expect(() => validatePin({ ...pin, frozenCompatPairs: [pair("src/a.js", "nothex")] })).toThrow( + /invalid compat hash/u, + ); + const duplicate = pair("src/a.js", "c".repeat(40)); + expect(() => validatePin({ ...pin, frozenCompatPairs: [duplicate, { ...duplicate }] })).toThrow( + /duplicate compat pair/u, + ); + expect(() => validatePin({ ...pin, baseFinalHashes: { "src/ok.js": "nothex" } })).toThrow( + /invalid final hash/u, + ); + expect(() => validatePin({ ...pin, baseFinalHashes: {} })).toThrow(/non-empty/u); + }); + + it("accepts a source delta only when every path is a safe src/ path", () => { + expect( + parseSourceDeltaPaths("diff --git a/src/a.js b/src/a.js\n--- a/src/a.js\n+++ b/src/a.js\n"), + ).toEqual(["src/a.js"]); + expect(() => parseSourceDeltaPaths("")).toThrow(/empty/u); + expect(() => + parseSourceDeltaPaths( + "diff --git a/build/a.js b/build/a.js\n--- a/build/a.js\n+++ b/build/a.js\n", + ), + ).toThrow(/safe src\//u); + expect(() => + parseSourceDeltaPaths( + "diff --git a/src/../escape.js b/src/../escape.js\n--- a/src/../escape.js\n+++ b/src/../escape.js\n", + ), + ).toThrow(/safe src\//u); + expect(() => + parseSourceDeltaPaths("diff --git a/src/a.js b/src/b.js\n--- a/src/a.js\n+++ b/src/b.js\n"), + ).toThrow(/rename/u); + }); +}); + +describe("assemblePatches", () => { + it("scopes the diff to the pinned targets and never emits installed dependencies", async () => { + const root = await tempRoot(); + const pin = (await readRealPin()) as { baseFinalHashes: Record }; + const { stockDir, patchedDir } = await makeScopedTrees(root, pin); + await mkdir(join(patchedDir, "node_modules/pkg"), { recursive: true }); + await writeFile( + join(patchedDir, "node_modules/pkg/index.js"), + "module.exports = {};\n", + "utf8", + ); + await writeFile(join(patchedDir, "package.json"), "{}\n", "utf8"); + await writeFile(join(patchedDir, "package-lock.json"), "{}\n", "utf8"); + await writeFile(join(patchedDir, "README.md"), "# three\n", "utf8"); + + const { basePatch } = await assemblePatches({ patchedDir, pin, stockDir }); + const headers = basePatch.match(/^diff --git /gmu) ?? []; + expect(headers.length).toBe(30); + expect(basePatch).not.toContain("node_modules"); + expect(basePatch).not.toContain("diff --git a/package.json"); + expect(basePatch).not.toContain("b/package-lock.json"); + expect(basePatch).not.toContain("b/README.md"); + }); + + it("reflects the actual build result and is deterministic", async () => { + const root = await tempRoot(); + const pin = (await readRealPin()) as { baseFinalHashes: Record }; + const { stockDir, patchedDir } = await makeScopedTrees(root, pin); + const target = "build/three.cjs"; + + await writeFile(join(patchedDir, target), "console.log(1);\n", "utf8"); + const first = await assemblePatches({ patchedDir, pin, stockDir }); + const repeat = await assemblePatches({ patchedDir, pin, stockDir }); + expect(first.basePatch).toBe(repeat.basePatch); + expect(first.upgradePatch).toBe(repeat.upgradePatch); + + await writeFile(join(patchedDir, target), "console.log(2);\n", "utf8"); + const second = await assemblePatches({ patchedDir, pin, stockDir }); + expect(second.basePatch).not.toBe(first.basePatch); + const firstJson = JSON.parse(first.upgradePatch); + const secondJson = JSON.parse(second.upgradePatch); + expect(secondJson.generatedFinalHashes[target]).toBe(blobHash("console.log(2);\n")); + expect(secondJson.generatedFinalHashes[target]).not.toBe( + firstJson.generatedFinalHashes[target], + ); + }); + + it("covers all pinned targets and preserves every legacy compat pair", async () => { + const root = await tempRoot(); + const pin = (await readRealPin()) as { + baseFinalHashes: Record; + frozenCompatPairs: unknown[]; + }; + const { stockDir, patchedDir } = await makeScopedTrees(root, pin); + const rel = "src/renderers/common/Renderer.js"; + await writeFile(join(patchedDir, rel), "// current renderer\n", "utf8"); + + const { basePatch, upgradePatch } = await assemblePatches({ patchedDir, pin, stockDir }); + expect(Object.keys(pin.baseFinalHashes)).toHaveLength(30); + expect(pin.frozenCompatPairs).toHaveLength(31); + + const json = JSON.parse(upgradePatch); + expect(Object.keys(json.generatedFinalHashes)).toHaveLength(30); + expect(json.acceptedLegacyCompatPairs).toHaveLength(31); + expect(Object.keys(json.acceptedLegacyFinalHashes)).toHaveLength(30); + expect(json.generatedFinalHashes[rel]).toBe(blobHash("// current renderer\n")); + expect(json.acceptedLegacyFinalHashes[rel]).toBe(pin.baseFinalHashes[rel]); + + const bundled = JSON.parse( + brotliDecompressSync(Buffer.from(json.bundleBrotliBase64, "base64")).toString("utf8"), + ); + expect(Object.keys(bundled)).toHaveLength(30); + expect(basePatch).toContain( + "diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js", + ); + }); +}); + +describe("generateThreePatch artifacts", () => { + it("writes both artifacts on success", async () => { + const root = await tempRoot(); + const packageRoot = join(root, "core"); + await copyRealInputs(packageRoot); + const planner = async () => ({ basePatch: "BASE\n", upgradePatch: "UP\n" }); + + const result = await generateThreePatch({ packageRoot, planner }); + expect(result.status).toBe("written"); + expect(await readFile(join(packageRoot, "patches", "three@0.185.1.patch"), "utf8")).toBe( + "BASE\n", + ); + expect( + await readFile(join(packageRoot, "patches", "three@0.185.1-prd269-upgrade.patch"), "utf8"), + ).toBe("UP\n"); + }); + + it("rejects drift in check mode and leaves both destinations unchanged", async () => { + const root = await tempRoot(); + const packageRoot = join(root, "core"); + await copyRealInputs(packageRoot); + const baseDest = join(packageRoot, "patches", "three@0.185.1.patch"); + const upgradeDest = join(packageRoot, "patches", "three@0.185.1-prd269-upgrade.patch"); + await mkdir(join(packageRoot, "patches"), { recursive: true }); + await writeFile(baseDest, "OLD\n", "utf8"); + await writeFile(upgradeDest, "OLDUP\n", "utf8"); + const planner = async () => ({ basePatch: "NEW\n", upgradePatch: "NEWUP\n" }); + + await expect(generateThreePatch({ packageRoot, planner, check: true })).rejects.toThrow( + /drift/u, + ); + expect(await readFile(baseDest, "utf8")).toBe("OLD\n"); + expect(await readFile(upgradeDest, "utf8")).toBe("OLDUP\n"); + }); + + it("matches in check mode when the generated text equals the committed text", async () => { + const root = await tempRoot(); + const packageRoot = join(root, "core"); + await copyRealInputs(packageRoot); + const baseDest = join(packageRoot, "patches", "three@0.185.1.patch"); + const upgradeDest = join(packageRoot, "patches", "three@0.185.1-prd269-upgrade.patch"); + await mkdir(join(packageRoot, "patches"), { recursive: true }); + await writeFile(baseDest, "SAME\n", "utf8"); + await writeFile(upgradeDest, "SAMEUP\n", "utf8"); + const planner = async () => ({ basePatch: "SAME\n", upgradePatch: "SAMEUP\n" }); + + const result = await generateThreePatch({ packageRoot, planner, check: true }); + expect(result.status).toBe("matched"); + }); + + it("does not write either destination when the archive is missing", async () => { + const root = await tempRoot(); + const packageRoot = join(root, "core"); + await copyRealInputs(packageRoot); + const baseDest = join(packageRoot, "patches", "three@0.185.1.patch"); + + await expect( + generateThreePatch({ archive: join(root, "absent.tar.gz"), packageRoot }), + ).rejects.toThrow(/archive not found/u); + expect(existsSync(baseDest)).toBe(false); + }); +}); diff --git a/packages/core/__tests__/three-patch-compat.spec.ts b/packages/core/__tests__/three-patch-compat.spec.ts new file mode 100644 index 000000000..7a2070042 --- /dev/null +++ b/packages/core/__tests__/three-patch-compat.spec.ts @@ -0,0 +1,310 @@ +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import { brotliCompressSync, brotliDecompressSync } from "node:zlib"; +import { afterEach, describe, expect, it } from "vitest"; +import { makeTempDir } from "../../../test-support/temp-dir.js"; + +// @ts-expect-error -- the postinstall helper is plain ESM with no type declarations. +import { applyThreePatch } from "../scripts/apply-three-patch.mjs"; + +const testDir = dirname(fileURLToPath(import.meta.url)); +const patchDir = join(resolve("packages/core"), "patches"); + +const UPGRADE_TEXT = readFileSync(join(patchDir, "three@0.185.1-prd269-upgrade.patch"), "utf8"); +const BASE_PATCH = readFileSync(join(patchDir, "three@0.185.1.patch"), "utf8"); +const UPGRADE = JSON.parse(UPGRADE_TEXT) as { + acceptedLegacyCompatPairs: { path: string; oldHash: string; newHash: string }[]; + acceptedLegacyFinalHashes: Record; + bundleBrotliBase64: string; + generatedFinalHashes: Record; + version: number; +}; + +// Frozen, test-only copy of the legacy unified migration and the exact bytes the previous release +// shipped. It is the only stable reference for the predecessor states a consumer can hold. +const FIXTURE = JSON.parse( + brotliDecompressSync( + readFileSync(join(testDir, "fixtures", "legacy-three-patch.json.br")), + ).toString("utf8"), +) as { originalUpgradePatch: string; targetTexts: Record }; + +const run = promisify(execFile); +const roots: string[] = []; + +afterEach(async () => { + for (const root of roots.splice(0)) await rm(root, { force: true, recursive: true }); +}); + +async function tempRoot(): Promise { + const root = await makeTempDir("threenative-three-compat-"); + roots.push(root); + return root; +} + +function blobHash(contents: string): string { + return createHash("sha1") + .update(`blob ${Buffer.byteLength(contents)}\0`) + .update(contents) + .digest("hex"); +} + +/** A full, registered Three.js tree at the exact bytes the previous release shipped. */ +async function installedTree(): Promise<{ + packageRoot: string; + threeRoot: string; +}> { + const root = await tempRoot(); + const packageRoot = join(root, "core"); + const threeRoot = join(root, "three"); + await mkdir(join(packageRoot, "patches"), { recursive: true }); + await writeFile(join(packageRoot, "patches", "three@0.185.1.patch"), BASE_PATCH, "utf8"); + await writeFile( + join(packageRoot, "patches", "three@0.185.1-prd269-upgrade.patch"), + UPGRADE_TEXT, + "utf8", + ); + await mkdir(threeRoot, { recursive: true }); + await writeFile( + join(threeRoot, "package.json"), + `${JSON.stringify({ name: "three", version: "0.185.1" })}\n`, + "utf8", + ); + for (const [rel, contents] of Object.entries(FIXTURE.targetTexts)) { + await mkdir(dirname(join(threeRoot, rel)), { recursive: true }); + await writeFile(join(threeRoot, rel), contents, "utf8"); + } + return { packageRoot, threeRoot }; +} + +/** Invert the frozen legacy hunks to materialise the exact predecessor bytes for one entry. */ +async function legacyPredecessor(path: string, oldHash: string): Promise { + const root = await tempRoot(); + const file = join(root, path); + await mkdir(dirname(file), { recursive: true }); + await writeFile(file, FIXTURE.targetTexts[path], "utf8"); + const block = FIXTURE.originalUpgradePatch.split(/(?=^diff --git )/mu).find((section) => { + const candidate = /^diff --git a\/(\S+) /mu.exec(section)?.[1]; + const hash = /^index ([a-f0-9]{40})\.\./mu.exec(section)?.[1]; + return candidate === path && hash === oldHash; + }); + expect(block, `no frozen hunk for ${path} ${oldHash}`).toBeDefined(); + const patchFile = join(root, "one.patch"); + await writeFile(patchFile, block ?? "", "utf8"); + await run("patch", ["--reverse", "--batch", "--fuzz=0", "--silent", "-p1", "-i", patchFile], { + cwd: root, + }); + const contents = await readFile(file, "utf8"); + expect(blobHash(contents), `predecessor ${path} ${oldHash}`).toBe(oldHash); + return contents; +} + +const STOCK_TEXT = "const alpha = 1;\n"; +const TARGET_TEXT = "const alpha = 1; // threenative\n"; +const TARGET_BLOB = blobHash(TARGET_TEXT); +const STOCK_BLOB = blobHash(STOCK_TEXT); +const COMMIT = "2431a09f46f34c560bc8e44b33be0e567723d5b9"; + +const TINY_BASE = `diff --git a/src/thing.js b/src/thing.js +index ${STOCK_BLOB}..${TARGET_BLOB} 100644 +--- a/src/thing.js ++++ b/src/thing.js +@@ -1,1 +1,1 @@ +-const alpha = 1; ++const alpha = 1; // threenative +`; + +function tinyUpgrade(overrides: Record = {}): string { + const json = { + version: 2, + generator: "scripts/generate-three-patch.mjs", + pin: { commit: COMMIT, version: "0.185.1" }, + acceptedLegacyCompatPairs: [ + { path: "src/thing.js", oldHash: blobHash("old\n"), newHash: TARGET_BLOB }, + ], + acceptedLegacyFinalHashes: { "src/thing.js": TARGET_BLOB }, + generatedFinalHashes: { "src/thing.js": TARGET_BLOB }, + bundleBrotliBase64: brotliCompressSync( + Buffer.from(JSON.stringify({ "src/thing.js": TARGET_TEXT })), + ).toString("base64"), + ...overrides, + }; + return `${JSON.stringify(json)}\n`; +} + +async function tinyTree( + upgradeText: string, + baseText = TINY_BASE, +): Promise<{ + packageRoot: string; + threeRoot: string; +}> { + const root = await tempRoot(); + const packageRoot = join(root, "core"); + const threeRoot = join(root, "three"); + await mkdir(join(packageRoot, "patches"), { recursive: true }); + await writeFile(join(packageRoot, "patches", "three@0.185.1.patch"), baseText, "utf8"); + await writeFile( + join(packageRoot, "patches", "three@0.185.1-prd269-upgrade.patch"), + upgradeText, + "utf8", + ); + await mkdir(join(threeRoot, "src"), { recursive: true }); + await writeFile( + join(threeRoot, "package.json"), + `${JSON.stringify({ name: "three", version: "0.185.1" })}\n`, + "utf8", + ); + await writeFile(join(threeRoot, "src", "thing.js"), STOCK_TEXT, "utf8"); + return { packageRoot, threeRoot }; +} + +describe("the versioned JSON upgrade patch", () => { + it("frozen fixture matches the shipped final hashes for all 30 registered targets", () => { + expect(Object.keys(FIXTURE.targetTexts)).toHaveLength(30); + for (const [rel, hash] of Object.entries(UPGRADE.acceptedLegacyFinalHashes)) { + expect(blobHash(FIXTURE.targetTexts[rel] ?? ""), rel).toBe(hash); + } + }); + + it("recognises all 31 legacy compat predecessors and lands on the generated target", async () => { + const { packageRoot, threeRoot } = await installedTree(); + for (const pair of UPGRADE.acceptedLegacyCompatPairs) { + const predecessor = await legacyPredecessor(pair.path, pair.oldHash); + await writeFile(join(threeRoot, pair.path), predecessor, "utf8"); + await expect(applyThreePatch({ packageRoot, threeRoot }), pair.oldHash).resolves.toBe( + "patched", + ); + expect( + blobHash(await readFile(join(threeRoot, pair.path), "utf8")), + `${pair.path} ${pair.oldHash}`, + ).toBe(UPGRADE.generatedFinalHashes[pair.path]); + await expect(applyThreePatch({ packageRoot, threeRoot })).resolves.toBe("unchanged"); + } + }); + + it("migrates all 30 previously shipped finals to the generated target", async () => { + const { packageRoot, threeRoot } = await installedTree(); + await expect(applyThreePatch({ packageRoot, threeRoot })).resolves.toBe("patched"); + for (const rel of Object.keys(UPGRADE.generatedFinalHashes)) { + expect(blobHash(await readFile(join(threeRoot, rel), "utf8")), rel).toBe( + UPGRADE.generatedFinalHashes[rel], + ); + } + await expect(applyThreePatch({ packageRoot, threeRoot })).resolves.toBe("unchanged"); + }); + + it("refuses one custom byte and writes no recognised file", async () => { + const { packageRoot, threeRoot } = await installedTree(); + const edited = join(threeRoot, "src/renderers/common/Renderer.js"); + const untouched = join(threeRoot, "build/three.webgpu.js"); + await writeFile(edited, `${await readFile(edited, "utf8")} `, "utf8"); + const before = await readFile(untouched, "utf8"); + await expect(applyThreePatch({ packageRoot, threeRoot })).rejects.toThrow( + /TN_THREE_PATCH_PARTIAL/, + ); + expect(await readFile(edited, "utf8")).toBe( + `${FIXTURE.targetTexts["src/renderers/common/Renderer.js"]} `, + ); + expect(await readFile(untouched, "utf8")).toBe(before); + }); + + it("rejects an unsupported version, schema drift, and a duplicate compat pair", async () => { + const pair = { + path: "src/thing.js", + oldHash: blobHash("old\n"), + newHash: TARGET_BLOB, + }; + const cases: [string, Record, RegExp][] = [ + ["version", { version: 1 }, /unsupported upgrade version/u], + ["schema", { acceptedLegacyCompatPairs: "nope" }, /non-empty array/u], + ["pin", { pin: {} }, /pin must name/u], + ["duplicate", { acceptedLegacyCompatPairs: [pair, { ...pair }] }, /duplicate compat pair/u], + ["unsafe", { acceptedLegacyFinalHashes: { "../evil.js": TARGET_BLOB } }, /unsafe/u], + [ + "compat-final", + { + acceptedLegacyCompatPairs: [ + { path: "src/thing.js", oldHash: pair.oldHash, newHash: "d".repeat(40) }, + ], + }, + /does not match its accepted final hash/u, + ], + ]; + for (const [name, override, expected] of cases) { + const { packageRoot, threeRoot } = await tinyTree(tinyUpgrade(override)); + await expect(applyThreePatch({ packageRoot, threeRoot }), name).rejects.toThrow(expected); + } + }); + + it("rejects mismatched payloads, bad base64, a decompression bomb, and a missing target", async () => { + const cases: [string, Record, RegExp][] = [ + [ + "mismatched payload hash", + { + bundleBrotliBase64: brotliCompressSync( + Buffer.from(JSON.stringify({ "src/thing.js": "const alpha = 1; // other\n" })), + ).toString("base64"), + }, + /does not match its generated hash/u, + ], + ["bad base64", { bundleBrotliBase64: "not base64!!" }, /not valid base64/u], + [ + "decompression bomb", + { + bundleBrotliBase64: brotliCompressSync( + Buffer.from(JSON.stringify({ "src/thing.js": "a".repeat(9 * 1024 * 1024) })), + ).toString("base64"), + }, + /bounded Brotli payload/u, + ], + ["missing target", { generatedFinalHashes: { "src/other.js": TARGET_BLOB } }, /base patch/u], + ]; + for (const [name, override, expected] of cases) { + const { packageRoot, threeRoot } = await tinyTree(tinyUpgrade(override)); + await expect(applyThreePatch({ packageRoot, threeRoot }), name).rejects.toThrow(expected); + } + }); + + it("rejects a wrong pin even when the consumer already holds the current target", async () => { + const cases: [string, { commit: string; version: string }][] = [ + ["wrong version", { commit: COMMIT, version: "999.0.0" }], + ["wrong commit", { commit: "0".repeat(40), version: "0.185.1" }], + ]; + for (const [name, pin] of cases) { + const { packageRoot, threeRoot } = await tinyTree(tinyUpgrade({ pin })); + await writeFile(join(threeRoot, "src", "thing.js"), TARGET_TEXT, "utf8"); + await expect(applyThreePatch({ packageRoot, threeRoot }), name).rejects.toThrow( + /pin must name/u, + ); + } + }); + + it("rejects a bundle missing the target for a registered predecessor when already current", async () => { + const { packageRoot, threeRoot } = await tinyTree( + tinyUpgrade({ + bundleBrotliBase64: brotliCompressSync(Buffer.from("{}")).toString("base64"), + }), + ); + await writeFile(join(threeRoot, "src", "thing.js"), TARGET_TEXT, "utf8"); + await expect(applyThreePatch({ packageRoot, threeRoot })).rejects.toThrow( + /missing the target/u, + ); + }); + + it("refuses a stock hunk whose new text does not match the patch index blob", async () => { + const corrupted = TINY_BASE.replace( + "+const alpha = 1; // threenative\n", + "+const alpha = 1; // tampered\n", + ); + const { packageRoot, threeRoot } = await tinyTree(tinyUpgrade(), corrupted); + await expect(applyThreePatch({ packageRoot, threeRoot })).rejects.toThrow( + /does not match the base patch index blob/u, + ); + await expect(readFile(join(threeRoot, "src", "thing.js"), "utf8")).resolves.toBe(STOCK_TEXT); + }); +}); diff --git a/packages/core/__tests__/three-patch-upgrade.spec.ts b/packages/core/__tests__/three-patch-upgrade.spec.ts index 29bbed42e..da821c4c0 100644 --- a/packages/core/__tests__/three-patch-upgrade.spec.ts +++ b/packages/core/__tests__/three-patch-upgrade.spec.ts @@ -1,14 +1,41 @@ import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; -import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; -import { join, resolve } from "node:path"; +import { readFileSync } from "node:fs"; +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; +import { brotliDecompressSync } from "node:zlib"; import { afterEach, describe, expect, it } from "vitest"; import { makeTempDir } from "../../../test-support/temp-dir.js"; // @ts-expect-error -- the postinstall helper is plain ESM with no type declarations. import { applyThreePatch } from "../scripts/apply-three-patch.mjs"; +const testDir = dirname(fileURLToPath(import.meta.url)); + +// A frozen, test-only copy of the legacy unified migration and the exact files the previous +// release shipped. Production no longer carries this format, so the fixture is the only stable +// reference for the migration the reader must still recognise. +const FIXTURE = JSON.parse( + brotliDecompressSync( + readFileSync(join(testDir, "fixtures", "legacy-three-patch.json.br")), + ).toString("utf8"), +) as { + originalBasePatch: string; + originalUpgradePatch: string; + targetTexts: Record; +}; + +const GENERATED_FINAL_HASHES = ( + JSON.parse( + readFileSync( + join(resolve("packages/core"), "patches", "three@0.185.1-prd269-upgrade.patch"), + "utf8", + ), + ) as { generatedFinalHashes: Record } +).generatedFinalHashes; + const roots: string[] = []; afterEach(async () => { @@ -189,14 +216,19 @@ async function previousInstalledPackage(priorBlobs: Record, crlf const root = await tempRoot(); const threeRoot = join(root, "three"); const packageRoot = resolve("packages/core"); - await cp(join(packageRoot, "node_modules/three"), threeRoot, { - dereference: true, - recursive: true, - }); - const migration = await readFile( - join(packageRoot, "patches/three@0.185.1-prd269-upgrade.patch"), + await mkdir(threeRoot, { recursive: true }); + await writeFile( + join(threeRoot, "package.json"), + `${JSON.stringify({ name: "three", version: THREE_VERSION })}\n`, "utf8", ); + // Start every registered target at the exact bytes the previous release shipped. + for (const [file, contents] of Object.entries(FIXTURE.targetTexts)) { + await mkdir(dirname(join(threeRoot, file)), { recursive: true }); + await writeFile(join(threeRoot, file), contents, "utf8"); + } + // Invert the frozen legacy hunks to reach the older state this group represents. + const migration = FIXTURE.originalUpgradePatch; const selected = migration .split(/(?=^diff --git )/mu) .filter((block) => { @@ -234,8 +266,8 @@ describe.each([ const { packageRoot, threeRoot } = await previousInstalledPackage(priorBlobs, crlf); await expect(applyThreePatch({ packageRoot, threeRoot })).resolves.toBe("patched"); for (const file of Object.keys(priorBlobs)) { - expect(await readFile(join(threeRoot, file), "utf8")).toBe( - await readFile(join(packageRoot, "node_modules/three", file), "utf8"), + expect(blobHash(await readFile(join(threeRoot, file), "utf8"))).toBe( + GENERATED_FINAL_HASHES[file], ); } await expect(applyThreePatch({ packageRoot, threeRoot })).resolves.toBe("unchanged"); diff --git a/packages/core/package.json b/packages/core/package.json index 9998deb29..3b58a8c35 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -79,6 +79,7 @@ "sideEffects": false, "scripts": { "postinstall": "node ./scripts/postinstall.mjs", + "patch:generate": "node scripts/generate-three-patch.mjs", "build": "node scripts/generate-version.mjs && tsup --config tsup.config.ts && node scripts/bundle-engine-mcp.mjs && node scripts/bundle-blender-mcp.mjs && publint", "typecheck": "tsc --noEmit", "test": "publint --strict" diff --git a/packages/core/patches/three@0.185.1-prd269-upgrade.patch b/packages/core/patches/three@0.185.1-prd269-upgrade.patch index 191a290fb..ab302ab03 100644 --- a/packages/core/patches/three@0.185.1-prd269-upgrade.patch +++ b/packages/core/patches/three@0.185.1-prd269-upgrade.patch @@ -1,15838 +1,230 @@ -diff --git a/build/three.webgpu.js b/build/three.webgpu.js -index 148869ee7f18c62570ac1eafb7df5775e5a89581..a308a892c59283b3b9a71477b3b02d64db40becd 100644 ---- a/build/three.webgpu.js -+++ b/build/three.webgpu.js -@@ -12307,7 +12307,8 @@ class TextureSizeNode extends Node { - const textureProperty = this.textureNode.build( builder, 'property' ); - const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); - -- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); -+ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; -+ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); - - } - -@@ -12958,7 +12959,7 @@ class TextureNode extends UniformNode { - - } else if ( this.sampler === false ) { - -- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); -+ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); - - } else if ( levelSnippet ) { - -@@ -18860,7 +18861,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve - - } - -- return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); -+ // The previous bones and world matrix must use the same frame's bind inverse. -+ const previousBindMatrixInverse = uniform( skinnedMesh.bindMatrixInverse, 'mat4' ).onObjectUpdate( ( { object } ) => { -+ -+ const mesh = object?.isSkinnedMesh === true ? object : skinnedMesh; -+ return mesh[ Symbol.for( 'threenative.velocity.previousBindMatrixInverse' ) ] ?? mesh.bindMatrixInverse; -+ -+ } ); -+ -+ return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, previousBindMatrixInverse, skinIndexNode, skinWeightNode ); - - } - -@@ -34249,6 +34258,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34398,7 +34411,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34562,7 +34584,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34572,7 +34603,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34589,6 +34620,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34621,9 +34663,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34801,6 +34968,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -40515,7 +40684,7 @@ class PassMultipleTextureNode extends PassTextureNode { - * @param {string} textureName - The output texture name. - * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. - */ -- constructor( passNode, textureName, previousTexture = false ) { -+ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { - - // null is passed to the super call since this class does not - // use an external texture for rendering pass data into. Instead -@@ -40536,6 +40705,8 @@ class PassMultipleTextureNode extends PassTextureNode { - * @type {boolean} - */ - this.previousTexture = previousTexture; -+ this.isRawTextureNode = rawTexture; -+ if ( rawTexture ) this.setSampler( false ); - - /** - * This flag can be used for type testing. -@@ -40557,17 +40728,33 @@ class PassMultipleTextureNode extends PassTextureNode { - - } - -+ getUniformHash( builder ) { -+ -+ const hash = super.getUniformHash( builder ); -+ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; -+ -+ } -+ - setup( builder ) { - - this.updateTexture(); - -+ if ( this.isRawTextureNode ) { -+ -+ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); -+ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); -+ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); -+ -+ } -+ - return super.setup( builder ); - - } - - clone() { - -- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); -+ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); - newNode.uvNode = this.uvNode; - newNode.levelNode = this.levelNode; - newNode.biasNode = this.biasNode; -@@ -40748,6 +40935,8 @@ class PassNode extends TempNode { - * @type {Object} - */ - this._textureNodes = {}; -+ this._rawTextureNodes = {}; -+ this._rawTexturesDisposed = false; - - /** - * A dictionary holding the internal depth nodes. -@@ -41054,6 +41243,7 @@ class PassNode extends TempNode { - this._previousTextures[ name ] = texture; - - this._textureNodes[ name ].updateTexture(); -+ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); - this._previousTextureNodes[ name ].updateTexture(); - - } -@@ -41066,6 +41256,17 @@ class PassNode extends TempNode { - * @param {string} [name='output'] - The output name to get the texture node for. - * @return {TextureNode} The texture node. - */ -+ getRawTextureNode( name = 'output' ) { -+ -+ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); -+ let node = this._rawTextureNodes[ name ]; -+ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); -+ node.updateTexture(); -+ return node; -+ -+ } -+ - getTextureNode( name = 'output' ) { - - let textureNode = this._textureNodes[ name ]; -@@ -41408,6 +41609,7 @@ class PassNode extends TempNode { - */ - dispose() { - -+ this._rawTexturesDisposed = true; - this.renderTarget.dispose(); - - } -@@ -44676,6 +44878,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44730,8 +44972,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44762,7 +45004,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45831,8 +46073,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45868,9 +46110,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -61391,6 +61633,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -62384,6 +62638,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -76680,6 +76942,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76896,7 +77161,12 @@ class WebGPUTextureUtils { - - } - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat ) { -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && -+ texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; -+ -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -76969,6 +77239,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -77009,7 +77295,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -77034,8 +77320,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77058,64 +77344,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -+ let cache = this._depthBuffers.get( depthTexture ); - -- return backend.get( depthTexture ).texture; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- } -- -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -- -- let format, type; -- -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -- -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -- -- return depthTextureGPU; -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77270,6 +77551,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77343,6 +77761,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -79269,7 +79689,7 @@ class WGSLNodeBuilder extends NodeBuilder { - * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. - * @return {string} The WGSL snippet. - */ -- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { -+ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { - - if ( levelSnippet === null ) levelSnippet = '0u'; - -@@ -79293,7 +79713,7 @@ class WGSLNodeBuilder extends NodeBuilder { - const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; - const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; - const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); -- const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; -+ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; - - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79835,7 +80255,7 @@ class WGSLNodeBuilder extends NodeBuilder { - - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - // Also textureGather always need sampler. -- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; -+ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); - - if ( needsSampler ) { - -@@ -80265,7 +80685,8 @@ ${ flowData.code } - - for ( const { name, property, type } of builtins.values() ) { - -- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); -+ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; -+ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); - - } - -@@ -80521,6 +80942,8 @@ ${ flowData.code } - getVaryings( shaderStage ) { - - const snippets = []; -+ const policy = this.context.sampleInterpolation; -+ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; - - if ( shaderStage === 'vertex' ) { - -@@ -80543,7 +80966,12 @@ ${ flowData.code } - - let attributesSnippet = `@location( ${ varyingIndex ++ } )`; - -- if ( varying.interpolationType ) { -+ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { -+ -+ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); -+ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; -+ -+ } else if ( varying.interpolationType ) { - - const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; - -@@ -80647,7 +81075,7 @@ ${ flowData.code } - - const { primarySamples } = backend.utils.getTextureSampleData( texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { - - multisampled = '_multisampled'; - -@@ -82145,7 +82573,20 @@ class WebGPUBindingUtils { - - let resourceGPU; - -- if ( textureData.externalTexture !== undefined ) { -+ if ( binding.textureNode?.isRawTextureNode === true ) { -+ -+ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; -+ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); -+ if ( textureData.rawTextureViewSource !== raw ) { -+ -+ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); -+ textureData.rawTextureViewSource = raw; -+ -+ } -+ resourceGPU = textureData.rawTextureView; -+ -+ } else if ( textureData.externalTexture !== undefined ) { - - resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); - -@@ -82316,7 +82757,7 @@ class WebGPUBindingUtils { - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { - - texture.multisampled = true; - -@@ -82354,7 +82795,7 @@ class WebGPUBindingUtils { - - } else if ( type === FloatType ) { - -- if ( this.backend.hasFeature( 'float32-filterable' ) ) { -+ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - -@@ -82871,6 +83312,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82885,7 +83335,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84140,6 +84590,19 @@ class WebGPUBackend extends Backend { - */ - this.gpu = null; - -+ /** -+ * A reference to the adapter this device was requested from. -+ * -+ * The adapter owns the wire instance every operation in flight completes on. Chromium -+ * collects an adapter the page stops referencing and the instance goes with it, so every -+ * operation still running completes with a shutdown the page reads as -+ * 'A valid external Instance reference no longer exists.' and presentation stops for. -+ * -+ * @type {?GPUAdapter} -+ * @default null -+ */ -+ this.adapter = null; -+ - /** - * A reference to the device. - * -@@ -84256,6 +84719,8 @@ class WebGPUBackend extends Backend { - - const adapter = this.gpu !== null ? await this.gpu.requestAdapter( adapterOptions ) : null; - -+ this.adapter = adapter; -+ - if ( adapter === null ) { - - throw new Error( 'THREE.WebGPUBackend: Unable to create WebGPU adapter.' ); -@@ -84481,7 +84946,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84508,6 +84975,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86248,6 +86718,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/build/three.webgpu.js b/build/three.webgpu.js -index 5c5ef3c06885c724c1c2288295e7e68483ee1f6e..a308a892c59283b3b9a71477b3b02d64db40becd 100644 ---- a/build/three.webgpu.js -+++ b/build/three.webgpu.js -@@ -12307,7 +12307,8 @@ class TextureSizeNode extends Node { - const textureProperty = this.textureNode.build( builder, 'property' ); - const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); - -- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); -+ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; -+ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); - - } - -@@ -12958,7 +12959,7 @@ class TextureNode extends UniformNode { - - } else if ( this.sampler === false ) { - -- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); -+ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); - - } else if ( levelSnippet ) { - -@@ -18539,7 +18540,7 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { - // Synchronization of dynamic buffer updates per frame - if ( interleavedMatrix !== null || interleavedColor !== null ) { - -- OnFrameUpdate( () => { -+ OnBeforeFrameUpdate( () => { - - if ( interleavedMatrix !== null ) { - -@@ -18580,12 +18581,28 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { - - const instancedMesh = builder.object; - -- OnObjectUpdate( ( { object } ) => { -+ const updatePrevious = ( object, array ) => { -+ -+ const { previousInstanceMatrix } = _previousInstanceMatrices.get( object ); -+ previousInstanceMatrix.array.set( array ); -+ previousInstanceMatrix.needsUpdate = true; -+ const interleaved = _matrixBuffers.get( previousInstanceMatrix ); -+ if ( interleaved !== undefined ) interleaved.needsUpdate = true; -+ -+ }; - -- const previousInstanceData = _previousInstanceMatrices.get( object ); -+ // Scheduled history must reach the GPU in this draw, before the geometry uploads. -+ OnBeforeObjectUpdate( ( { object } ) => { - - const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ]; -- previousInstanceData.previousInstanceMatrix.array.set( scheduledPrevious ?? matrices.array ); -+ if ( scheduledPrevious !== undefined ) updatePrevious( object, scheduledPrevious ); -+ -+ } ); -+ -+ // Without a scheduler, retain Three's draw-time snapshot for the next draw. -+ OnObjectUpdate( ( { object } ) => { -+ -+ if ( object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ] === undefined ) updatePrevious( object, matrices.array ); - - } ); - -@@ -18844,7 +18861,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve - - } - -- return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); -+ // The previous bones and world matrix must use the same frame's bind inverse. -+ const previousBindMatrixInverse = uniform( skinnedMesh.bindMatrixInverse, 'mat4' ).onObjectUpdate( ( { object } ) => { -+ -+ const mesh = object?.isSkinnedMesh === true ? object : skinnedMesh; -+ return mesh[ Symbol.for( 'threenative.velocity.previousBindMatrixInverse' ) ] ?? mesh.bindMatrixInverse; -+ -+ } ); -+ -+ return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, previousBindMatrixInverse, skinIndexNode, skinWeightNode ); - - } - -@@ -34233,6 +34258,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34382,7 +34411,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34546,7 +34584,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34556,7 +34603,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34573,6 +34620,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34605,9 +34663,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34785,6 +34968,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -40499,7 +40684,7 @@ class PassMultipleTextureNode extends PassTextureNode { - * @param {string} textureName - The output texture name. - * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. - */ -- constructor( passNode, textureName, previousTexture = false ) { -+ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { - - // null is passed to the super call since this class does not - // use an external texture for rendering pass data into. Instead -@@ -40520,6 +40705,8 @@ class PassMultipleTextureNode extends PassTextureNode { - * @type {boolean} - */ - this.previousTexture = previousTexture; -+ this.isRawTextureNode = rawTexture; -+ if ( rawTexture ) this.setSampler( false ); - - /** - * This flag can be used for type testing. -@@ -40541,17 +40728,33 @@ class PassMultipleTextureNode extends PassTextureNode { - - } - -+ getUniformHash( builder ) { -+ -+ const hash = super.getUniformHash( builder ); -+ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; -+ -+ } -+ - setup( builder ) { - - this.updateTexture(); - -+ if ( this.isRawTextureNode ) { -+ -+ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); -+ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); -+ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); -+ -+ } -+ - return super.setup( builder ); - - } - - clone() { - -- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); -+ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); - newNode.uvNode = this.uvNode; - newNode.levelNode = this.levelNode; - newNode.biasNode = this.biasNode; -@@ -40732,6 +40935,8 @@ class PassNode extends TempNode { - * @type {Object} - */ - this._textureNodes = {}; -+ this._rawTextureNodes = {}; -+ this._rawTexturesDisposed = false; - - /** - * A dictionary holding the internal depth nodes. -@@ -41038,6 +41243,7 @@ class PassNode extends TempNode { - this._previousTextures[ name ] = texture; - - this._textureNodes[ name ].updateTexture(); -+ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); - this._previousTextureNodes[ name ].updateTexture(); - - } -@@ -41050,6 +41256,17 @@ class PassNode extends TempNode { - * @param {string} [name='output'] - The output name to get the texture node for. - * @return {TextureNode} The texture node. - */ -+ getRawTextureNode( name = 'output' ) { -+ -+ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); -+ let node = this._rawTextureNodes[ name ]; -+ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); -+ node.updateTexture(); -+ return node; -+ -+ } -+ - getTextureNode( name = 'output' ) { - - let textureNode = this._textureNodes[ name ]; -@@ -41392,6 +41609,7 @@ class PassNode extends TempNode { - */ - dispose() { - -+ this._rawTexturesDisposed = true; - this.renderTarget.dispose(); - - } -@@ -44660,6 +44878,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44714,8 +44972,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44746,7 +45004,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45815,8 +46073,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45852,9 +46110,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -61375,6 +61633,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -62368,6 +62638,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -63161,8 +63439,8 @@ class Renderer { - // attribute scan - because each of those answers for itself, and a step left out here - // cannot: a group a node added after this object's first draw still has to be created, and - // the pipeline reads that group's layout whatever this draw decided to do with it. -- this._geometries.updateForRender( renderObject ); - this._nodes.updateBefore( renderObject ); -+ this._geometries.updateForRender( renderObject ); - this._nodes.updateForRender( renderObject ); - this._bindings.updateForRender( renderObject ); - -@@ -76664,6 +76942,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76880,12 +77161,12 @@ class WebGPUTextureUtils { - - } - -- // Compute-only 3D storage has no attachment or render-pass mipmap consumer. -- // Requesting the unused attachment usage rejects the texture on wgpu-native. -- const computeOnlyStorage3D = dimension === GPUTextureDimension.ThreeD && texture.isStorageTexture === true && -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && - texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! computeOnlyStorage3D ) { -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -76958,6 +77239,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -76998,7 +77295,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -77023,8 +77320,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77047,64 +77344,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -- -- return backend.get( depthTexture ).texture; -+ let cache = this._depthBuffers.get( depthTexture ); - -- } -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -- -- let format, type; -- -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -- -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- return depthTextureGPU; -- -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77259,6 +77551,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77332,6 +77761,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -79258,7 +79689,7 @@ class WGSLNodeBuilder extends NodeBuilder { - * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. - * @return {string} The WGSL snippet. - */ -- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { -+ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { - - if ( levelSnippet === null ) levelSnippet = '0u'; - -@@ -79282,7 +79713,7 @@ class WGSLNodeBuilder extends NodeBuilder { - const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; - const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; - const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); -- const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; -+ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; - - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79824,7 +80255,7 @@ class WGSLNodeBuilder extends NodeBuilder { - - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - // Also textureGather always need sampler. -- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; -+ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); - - if ( needsSampler ) { - -@@ -80254,7 +80685,8 @@ ${ flowData.code } - - for ( const { name, property, type } of builtins.values() ) { - -- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); -+ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; -+ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); - - } - -@@ -80510,6 +80942,8 @@ ${ flowData.code } - getVaryings( shaderStage ) { - - const snippets = []; -+ const policy = this.context.sampleInterpolation; -+ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; - - if ( shaderStage === 'vertex' ) { - -@@ -80532,7 +80966,12 @@ ${ flowData.code } - - let attributesSnippet = `@location( ${ varyingIndex ++ } )`; - -- if ( varying.interpolationType ) { -+ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { -+ -+ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); -+ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; -+ -+ } else if ( varying.interpolationType ) { - - const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; - -@@ -80636,7 +81075,7 @@ ${ flowData.code } - - const { primarySamples } = backend.utils.getTextureSampleData( texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { - - multisampled = '_multisampled'; - -@@ -81391,6 +81830,15 @@ class WebGPUAttributeUtils { - buffer.unmap(); - - bufferData.buffer = buffer; -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ -+ } else if ( bufferData.version < bufferAttribute.version || -+ ( bufferAttribute.usage === DynamicDrawUsage && bufferData.uploadCall !== backend.renderer.info.calls ) ) { -+ -+ // A rebuilt shader can create a fresh attribute wrapper for this existing buffer. -+ // Synchronize its dirty bytes before Attributes records the wrapper as current. -+ this.updateAttribute( attribute ); - - } - -@@ -81411,6 +81859,11 @@ class WebGPUAttributeUtils { - const bufferData = backend.get( bufferAttribute ); - const buffer = backend.get( bufferAttribute ).buffer; - -+ // A fresh wrapper may already have uploaded this shared buffer in the same render or compute call. -+ // Do not repeat that write after its partial ranges have been consumed. -+ if ( bufferData.version === bufferAttribute.version && -+ ( bufferAttribute.usage !== DynamicDrawUsage || bufferData.uploadCall === backend.renderer.info.calls ) ) return; -+ - let array = bufferAttribute.array; - - const itemSize = bufferData._itemSize; -@@ -81492,6 +81945,9 @@ class WebGPUAttributeUtils { - - } - -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ - } - - /** -@@ -82117,7 +82573,20 @@ class WebGPUBindingUtils { - - let resourceGPU; - -- if ( textureData.externalTexture !== undefined ) { -+ if ( binding.textureNode?.isRawTextureNode === true ) { -+ -+ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; -+ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); -+ if ( textureData.rawTextureViewSource !== raw ) { -+ -+ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); -+ textureData.rawTextureViewSource = raw; -+ -+ } -+ resourceGPU = textureData.rawTextureView; -+ -+ } else if ( textureData.externalTexture !== undefined ) { - - resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); - -@@ -82288,7 +82757,7 @@ class WebGPUBindingUtils { - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { - - texture.multisampled = true; - -@@ -82326,7 +82795,7 @@ class WebGPUBindingUtils { - - } else if ( type === FloatType ) { - -- if ( this.backend.hasFeature( 'float32-filterable' ) ) { -+ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - -@@ -82843,6 +83312,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82857,7 +83335,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84468,7 +84946,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84495,6 +84975,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86235,6 +86718,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/build/three.webgpu.js b/build/three.webgpu.js -index 7aa62adb9e78ba8f0a67709d3141bf93c67419cc..a308a892c59283b3b9a71477b3b02d64db40becd 100644 ---- a/build/three.webgpu.js -+++ b/build/three.webgpu.js -@@ -12307,7 +12307,8 @@ class TextureSizeNode extends Node { - const textureProperty = this.textureNode.build( builder, 'property' ); - const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); - -- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); -+ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; -+ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); - - } - -@@ -12958,7 +12959,7 @@ class TextureNode extends UniformNode { - - } else if ( this.sampler === false ) { - -- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); -+ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); - - } else if ( levelSnippet ) { - -@@ -18539,7 +18540,7 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { - // Synchronization of dynamic buffer updates per frame - if ( interleavedMatrix !== null || interleavedColor !== null ) { - -- OnFrameUpdate( () => { -+ OnBeforeFrameUpdate( () => { - - if ( interleavedMatrix !== null ) { - -@@ -18580,12 +18581,28 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { - - const instancedMesh = builder.object; - -- OnObjectUpdate( ( { object } ) => { -+ const updatePrevious = ( object, array ) => { -+ -+ const { previousInstanceMatrix } = _previousInstanceMatrices.get( object ); -+ previousInstanceMatrix.array.set( array ); -+ previousInstanceMatrix.needsUpdate = true; -+ const interleaved = _matrixBuffers.get( previousInstanceMatrix ); -+ if ( interleaved !== undefined ) interleaved.needsUpdate = true; -+ -+ }; - -- const previousInstanceData = _previousInstanceMatrices.get( object ); -+ // Scheduled history must reach the GPU in this draw, before the geometry uploads. -+ OnBeforeObjectUpdate( ( { object } ) => { - - const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ]; -- previousInstanceData.previousInstanceMatrix.array.set( scheduledPrevious ?? matrices.array ); -+ if ( scheduledPrevious !== undefined ) updatePrevious( object, scheduledPrevious ); -+ -+ } ); -+ -+ // Without a scheduler, retain Three's draw-time snapshot for the next draw. -+ OnObjectUpdate( ( { object } ) => { -+ -+ if ( object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ] === undefined ) updatePrevious( object, matrices.array ); - - } ); - -@@ -18844,7 +18861,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve - - } - -- return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); -+ // The previous bones and world matrix must use the same frame's bind inverse. -+ const previousBindMatrixInverse = uniform( skinnedMesh.bindMatrixInverse, 'mat4' ).onObjectUpdate( ( { object } ) => { -+ -+ const mesh = object?.isSkinnedMesh === true ? object : skinnedMesh; -+ return mesh[ Symbol.for( 'threenative.velocity.previousBindMatrixInverse' ) ] ?? mesh.bindMatrixInverse; -+ -+ } ); -+ -+ return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, previousBindMatrixInverse, skinIndexNode, skinWeightNode ); - - } - -@@ -34233,6 +34258,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34382,7 +34411,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34546,7 +34584,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34556,7 +34603,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34573,6 +34620,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34605,9 +34663,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34785,6 +34968,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -40499,7 +40684,7 @@ class PassMultipleTextureNode extends PassTextureNode { - * @param {string} textureName - The output texture name. - * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. - */ -- constructor( passNode, textureName, previousTexture = false ) { -+ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { - - // null is passed to the super call since this class does not - // use an external texture for rendering pass data into. Instead -@@ -40520,6 +40705,8 @@ class PassMultipleTextureNode extends PassTextureNode { - * @type {boolean} - */ - this.previousTexture = previousTexture; -+ this.isRawTextureNode = rawTexture; -+ if ( rawTexture ) this.setSampler( false ); - - /** - * This flag can be used for type testing. -@@ -40541,17 +40728,33 @@ class PassMultipleTextureNode extends PassTextureNode { - - } - -+ getUniformHash( builder ) { -+ -+ const hash = super.getUniformHash( builder ); -+ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; -+ -+ } -+ - setup( builder ) { - - this.updateTexture(); - -+ if ( this.isRawTextureNode ) { -+ -+ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); -+ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); -+ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); -+ -+ } -+ - return super.setup( builder ); - - } - - clone() { - -- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); -+ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); - newNode.uvNode = this.uvNode; - newNode.levelNode = this.levelNode; - newNode.biasNode = this.biasNode; -@@ -40732,6 +40935,8 @@ class PassNode extends TempNode { - * @type {Object} - */ - this._textureNodes = {}; -+ this._rawTextureNodes = {}; -+ this._rawTexturesDisposed = false; - - /** - * A dictionary holding the internal depth nodes. -@@ -41038,6 +41243,7 @@ class PassNode extends TempNode { - this._previousTextures[ name ] = texture; - - this._textureNodes[ name ].updateTexture(); -+ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); - this._previousTextureNodes[ name ].updateTexture(); - - } -@@ -41050,6 +41256,17 @@ class PassNode extends TempNode { - * @param {string} [name='output'] - The output name to get the texture node for. - * @return {TextureNode} The texture node. - */ -+ getRawTextureNode( name = 'output' ) { -+ -+ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); -+ let node = this._rawTextureNodes[ name ]; -+ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); -+ node.updateTexture(); -+ return node; -+ -+ } -+ - getTextureNode( name = 'output' ) { - - let textureNode = this._textureNodes[ name ]; -@@ -41392,6 +41609,7 @@ class PassNode extends TempNode { - */ - dispose() { - -+ this._rawTexturesDisposed = true; - this.renderTarget.dispose(); - - } -@@ -44660,6 +44878,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44714,8 +44972,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44746,7 +45004,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45815,8 +46073,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45852,9 +46110,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -61375,6 +61633,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -62368,6 +62638,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -63161,8 +63439,8 @@ class Renderer { - // attribute scan - because each of those answers for itself, and a step left out here - // cannot: a group a node added after this object's first draw still has to be created, and - // the pipeline reads that group's layout whatever this draw decided to do with it. -- this._geometries.updateForRender( renderObject ); - this._nodes.updateBefore( renderObject ); -+ this._geometries.updateForRender( renderObject ); - this._nodes.updateForRender( renderObject ); - this._bindings.updateForRender( renderObject ); - -@@ -76664,6 +76942,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76880,7 +77161,12 @@ class WebGPUTextureUtils { - - } - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat ) { -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && -+ texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; -+ -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -76953,6 +77239,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -76993,7 +77295,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -77018,8 +77320,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77042,64 +77344,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -- -- return backend.get( depthTexture ).texture; -- -- } -- -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -+ let cache = this._depthBuffers.get( depthTexture ); - -- let format, type; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -- -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -- -- return depthTextureGPU; -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77254,6 +77551,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77327,6 +77761,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -79253,7 +79689,7 @@ class WGSLNodeBuilder extends NodeBuilder { - * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. - * @return {string} The WGSL snippet. - */ -- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { -+ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { - - if ( levelSnippet === null ) levelSnippet = '0u'; - -@@ -79277,7 +79713,7 @@ class WGSLNodeBuilder extends NodeBuilder { - const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; - const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; - const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); -- const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; -+ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; - - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79819,7 +80255,7 @@ class WGSLNodeBuilder extends NodeBuilder { - - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - // Also textureGather always need sampler. -- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; -+ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); - - if ( needsSampler ) { - -@@ -80249,7 +80685,8 @@ ${ flowData.code } - - for ( const { name, property, type } of builtins.values() ) { - -- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); -+ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; -+ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); - - } - -@@ -80505,6 +80942,8 @@ ${ flowData.code } - getVaryings( shaderStage ) { - - const snippets = []; -+ const policy = this.context.sampleInterpolation; -+ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; - - if ( shaderStage === 'vertex' ) { - -@@ -80527,7 +80966,12 @@ ${ flowData.code } - - let attributesSnippet = `@location( ${ varyingIndex ++ } )`; - -- if ( varying.interpolationType ) { -+ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { -+ -+ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); -+ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; -+ -+ } else if ( varying.interpolationType ) { - - const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; - -@@ -80631,7 +81075,7 @@ ${ flowData.code } - - const { primarySamples } = backend.utils.getTextureSampleData( texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { - - multisampled = '_multisampled'; - -@@ -81386,6 +81830,15 @@ class WebGPUAttributeUtils { - buffer.unmap(); - - bufferData.buffer = buffer; -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ -+ } else if ( bufferData.version < bufferAttribute.version || -+ ( bufferAttribute.usage === DynamicDrawUsage && bufferData.uploadCall !== backend.renderer.info.calls ) ) { -+ -+ // A rebuilt shader can create a fresh attribute wrapper for this existing buffer. -+ // Synchronize its dirty bytes before Attributes records the wrapper as current. -+ this.updateAttribute( attribute ); - - } - -@@ -81406,6 +81859,11 @@ class WebGPUAttributeUtils { - const bufferData = backend.get( bufferAttribute ); - const buffer = backend.get( bufferAttribute ).buffer; - -+ // A fresh wrapper may already have uploaded this shared buffer in the same render or compute call. -+ // Do not repeat that write after its partial ranges have been consumed. -+ if ( bufferData.version === bufferAttribute.version && -+ ( bufferAttribute.usage !== DynamicDrawUsage || bufferData.uploadCall === backend.renderer.info.calls ) ) return; -+ - let array = bufferAttribute.array; - - const itemSize = bufferData._itemSize; -@@ -81487,6 +81945,9 @@ class WebGPUAttributeUtils { - - } - -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ - } - - /** -@@ -82112,7 +82573,20 @@ class WebGPUBindingUtils { - - let resourceGPU; - -- if ( textureData.externalTexture !== undefined ) { -+ if ( binding.textureNode?.isRawTextureNode === true ) { -+ -+ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; -+ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); -+ if ( textureData.rawTextureViewSource !== raw ) { -+ -+ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); -+ textureData.rawTextureViewSource = raw; -+ -+ } -+ resourceGPU = textureData.rawTextureView; -+ -+ } else if ( textureData.externalTexture !== undefined ) { - - resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); - -@@ -82283,7 +82757,7 @@ class WebGPUBindingUtils { - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { - - texture.multisampled = true; - -@@ -82321,7 +82795,7 @@ class WebGPUBindingUtils { - - } else if ( type === FloatType ) { - -- if ( this.backend.hasFeature( 'float32-filterable' ) ) { -+ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - -@@ -82838,6 +83312,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82852,7 +83335,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84107,6 +84590,19 @@ class WebGPUBackend extends Backend { - */ - this.gpu = null; - -+ /** -+ * A reference to the adapter this device was requested from. -+ * -+ * The adapter owns the wire instance every operation in flight completes on. Chromium -+ * collects an adapter the page stops referencing and the instance goes with it, so every -+ * operation still running completes with a shutdown the page reads as -+ * 'A valid external Instance reference no longer exists.' and presentation stops for. -+ * -+ * @type {?GPUAdapter} -+ * @default null -+ */ -+ this.adapter = null; -+ - /** - * A reference to the device. - * -@@ -84223,6 +84719,8 @@ class WebGPUBackend extends Backend { - - const adapter = this.gpu !== null ? await this.gpu.requestAdapter( adapterOptions ) : null; - -+ this.adapter = adapter; -+ - if ( adapter === null ) { - - throw new Error( 'THREE.WebGPUBackend: Unable to create WebGPU adapter.' ); -@@ -84448,7 +84946,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84475,6 +84975,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86215,6 +86718,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/build/three.webgpu.js b/build/three.webgpu.js -index bdffb9b4069f7beafa85913b155e92d3e83f12d0..a308a892c59283b3b9a71477b3b02d64db40becd 100644 ---- a/build/three.webgpu.js -+++ b/build/three.webgpu.js -@@ -12307,7 +12307,8 @@ class TextureSizeNode extends Node { - const textureProperty = this.textureNode.build( builder, 'property' ); - const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); - -- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); -+ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; -+ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); - - } - -@@ -12958,7 +12959,7 @@ class TextureNode extends UniformNode { - - } else if ( this.sampler === false ) { - -- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); -+ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); - - } else if ( levelSnippet ) { - -@@ -18860,7 +18861,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve - - } - -- return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); -+ // The previous bones and world matrix must use the same frame's bind inverse. -+ const previousBindMatrixInverse = uniform( skinnedMesh.bindMatrixInverse, 'mat4' ).onObjectUpdate( ( { object } ) => { -+ -+ const mesh = object?.isSkinnedMesh === true ? object : skinnedMesh; -+ return mesh[ Symbol.for( 'threenative.velocity.previousBindMatrixInverse' ) ] ?? mesh.bindMatrixInverse; -+ -+ } ); -+ -+ return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, previousBindMatrixInverse, skinIndexNode, skinWeightNode ); - - } - -@@ -34249,6 +34258,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34398,7 +34411,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34562,7 +34584,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34572,7 +34603,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34589,6 +34620,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34621,9 +34663,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34801,6 +34968,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -40515,7 +40684,7 @@ class PassMultipleTextureNode extends PassTextureNode { - * @param {string} textureName - The output texture name. - * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. - */ -- constructor( passNode, textureName, previousTexture = false ) { -+ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { - - // null is passed to the super call since this class does not - // use an external texture for rendering pass data into. Instead -@@ -40536,6 +40705,8 @@ class PassMultipleTextureNode extends PassTextureNode { - * @type {boolean} - */ - this.previousTexture = previousTexture; -+ this.isRawTextureNode = rawTexture; -+ if ( rawTexture ) this.setSampler( false ); - - /** - * This flag can be used for type testing. -@@ -40557,17 +40728,33 @@ class PassMultipleTextureNode extends PassTextureNode { - - } - -+ getUniformHash( builder ) { -+ -+ const hash = super.getUniformHash( builder ); -+ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; -+ -+ } -+ - setup( builder ) { - - this.updateTexture(); - -+ if ( this.isRawTextureNode ) { -+ -+ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); -+ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); -+ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); -+ -+ } -+ - return super.setup( builder ); - - } - - clone() { - -- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); -+ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); - newNode.uvNode = this.uvNode; - newNode.levelNode = this.levelNode; - newNode.biasNode = this.biasNode; -@@ -40748,6 +40935,8 @@ class PassNode extends TempNode { - * @type {Object} - */ - this._textureNodes = {}; -+ this._rawTextureNodes = {}; -+ this._rawTexturesDisposed = false; - - /** - * A dictionary holding the internal depth nodes. -@@ -41054,6 +41243,7 @@ class PassNode extends TempNode { - this._previousTextures[ name ] = texture; - - this._textureNodes[ name ].updateTexture(); -+ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); - this._previousTextureNodes[ name ].updateTexture(); - - } -@@ -41066,6 +41256,17 @@ class PassNode extends TempNode { - * @param {string} [name='output'] - The output name to get the texture node for. - * @return {TextureNode} The texture node. - */ -+ getRawTextureNode( name = 'output' ) { -+ -+ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); -+ let node = this._rawTextureNodes[ name ]; -+ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); -+ node.updateTexture(); -+ return node; -+ -+ } -+ - getTextureNode( name = 'output' ) { - - let textureNode = this._textureNodes[ name ]; -@@ -41408,6 +41609,7 @@ class PassNode extends TempNode { - */ - dispose() { - -+ this._rawTexturesDisposed = true; - this.renderTarget.dispose(); - - } -@@ -44676,6 +44878,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44730,8 +44972,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44762,7 +45004,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45831,8 +46073,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45868,9 +46110,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -61391,6 +61633,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -62384,6 +62638,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -76680,6 +76942,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76896,7 +77161,12 @@ class WebGPUTextureUtils { - - } - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat ) { -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && -+ texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; -+ -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -76969,6 +77239,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -77009,7 +77295,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -77034,8 +77320,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77058,64 +77344,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -- -- return backend.get( depthTexture ).texture; -- -- } -- -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -+ let cache = this._depthBuffers.get( depthTexture ); - -- let format, type; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -- -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -- -- return depthTextureGPU; -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77270,6 +77551,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77343,6 +77761,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -79269,7 +79689,7 @@ class WGSLNodeBuilder extends NodeBuilder { - * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. - * @return {string} The WGSL snippet. - */ -- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { -+ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { - - if ( levelSnippet === null ) levelSnippet = '0u'; - -@@ -79293,7 +79713,7 @@ class WGSLNodeBuilder extends NodeBuilder { - const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; - const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; - const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); -- const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; -+ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; - - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79835,7 +80255,7 @@ class WGSLNodeBuilder extends NodeBuilder { - - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - // Also textureGather always need sampler. -- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; -+ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); - - if ( needsSampler ) { - -@@ -80265,7 +80685,8 @@ ${ flowData.code } - - for ( const { name, property, type } of builtins.values() ) { - -- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); -+ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; -+ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); - - } - -@@ -80521,6 +80942,8 @@ ${ flowData.code } - getVaryings( shaderStage ) { - - const snippets = []; -+ const policy = this.context.sampleInterpolation; -+ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; - - if ( shaderStage === 'vertex' ) { - -@@ -80543,7 +80966,12 @@ ${ flowData.code } - - let attributesSnippet = `@location( ${ varyingIndex ++ } )`; - -- if ( varying.interpolationType ) { -+ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { -+ -+ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); -+ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; -+ -+ } else if ( varying.interpolationType ) { - - const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; - -@@ -80647,7 +81075,7 @@ ${ flowData.code } - - const { primarySamples } = backend.utils.getTextureSampleData( texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { - - multisampled = '_multisampled'; - -@@ -81402,6 +81830,15 @@ class WebGPUAttributeUtils { - buffer.unmap(); - - bufferData.buffer = buffer; -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ -+ } else if ( bufferData.version < bufferAttribute.version || -+ ( bufferAttribute.usage === DynamicDrawUsage && bufferData.uploadCall !== backend.renderer.info.calls ) ) { -+ -+ // A rebuilt shader can create a fresh attribute wrapper for this existing buffer. -+ // Synchronize its dirty bytes before Attributes records the wrapper as current. -+ this.updateAttribute( attribute ); - - } - -@@ -81422,6 +81859,11 @@ class WebGPUAttributeUtils { - const bufferData = backend.get( bufferAttribute ); - const buffer = backend.get( bufferAttribute ).buffer; - -+ // A fresh wrapper may already have uploaded this shared buffer in the same render or compute call. -+ // Do not repeat that write after its partial ranges have been consumed. -+ if ( bufferData.version === bufferAttribute.version && -+ ( bufferAttribute.usage !== DynamicDrawUsage || bufferData.uploadCall === backend.renderer.info.calls ) ) return; -+ - let array = bufferAttribute.array; - - const itemSize = bufferData._itemSize; -@@ -81503,6 +81945,9 @@ class WebGPUAttributeUtils { - - } - -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ - } - - /** -@@ -82128,7 +82573,20 @@ class WebGPUBindingUtils { - - let resourceGPU; - -- if ( textureData.externalTexture !== undefined ) { -+ if ( binding.textureNode?.isRawTextureNode === true ) { -+ -+ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; -+ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); -+ if ( textureData.rawTextureViewSource !== raw ) { -+ -+ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); -+ textureData.rawTextureViewSource = raw; -+ -+ } -+ resourceGPU = textureData.rawTextureView; -+ -+ } else if ( textureData.externalTexture !== undefined ) { - - resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); - -@@ -82299,7 +82757,7 @@ class WebGPUBindingUtils { - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { - - texture.multisampled = true; - -@@ -82337,7 +82795,7 @@ class WebGPUBindingUtils { - - } else if ( type === FloatType ) { - -- if ( this.backend.hasFeature( 'float32-filterable' ) ) { -+ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - -@@ -82854,6 +83312,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82868,7 +83335,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84123,6 +84590,19 @@ class WebGPUBackend extends Backend { - */ - this.gpu = null; - -+ /** -+ * A reference to the adapter this device was requested from. -+ * -+ * The adapter owns the wire instance every operation in flight completes on. Chromium -+ * collects an adapter the page stops referencing and the instance goes with it, so every -+ * operation still running completes with a shutdown the page reads as -+ * 'A valid external Instance reference no longer exists.' and presentation stops for. -+ * -+ * @type {?GPUAdapter} -+ * @default null -+ */ -+ this.adapter = null; -+ - /** - * A reference to the device. - * -@@ -84239,6 +84719,8 @@ class WebGPUBackend extends Backend { - - const adapter = this.gpu !== null ? await this.gpu.requestAdapter( adapterOptions ) : null; - -+ this.adapter = adapter; -+ - if ( adapter === null ) { - - throw new Error( 'THREE.WebGPUBackend: Unable to create WebGPU adapter.' ); -@@ -84464,7 +84946,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84491,6 +84975,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86231,6 +86718,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/build/three.webgpu.js b/build/three.webgpu.js -index efd0e16719cc7bbef1fbc18c241948e25eb85881..a308a892c59283b3b9a71477b3b02d64db40becd 100644 ---- a/build/three.webgpu.js -+++ b/build/three.webgpu.js -@@ -12307,7 +12307,8 @@ class TextureSizeNode extends Node { - const textureProperty = this.textureNode.build( builder, 'property' ); - const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); - -- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); -+ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; -+ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); - - } - -@@ -12958,7 +12959,7 @@ class TextureNode extends UniformNode { - - } else if ( this.sampler === false ) { - -- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); -+ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); - - } else if ( levelSnippet ) { - -@@ -34257,6 +34258,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34406,7 +34411,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34570,7 +34584,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34580,7 +34603,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34597,6 +34620,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34629,9 +34663,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34809,6 +34968,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -40523,7 +40684,7 @@ class PassMultipleTextureNode extends PassTextureNode { - * @param {string} textureName - The output texture name. - * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. - */ -- constructor( passNode, textureName, previousTexture = false ) { -+ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { - - // null is passed to the super call since this class does not - // use an external texture for rendering pass data into. Instead -@@ -40544,6 +40705,8 @@ class PassMultipleTextureNode extends PassTextureNode { - * @type {boolean} - */ - this.previousTexture = previousTexture; -+ this.isRawTextureNode = rawTexture; -+ if ( rawTexture ) this.setSampler( false ); - - /** - * This flag can be used for type testing. -@@ -40565,17 +40728,33 @@ class PassMultipleTextureNode extends PassTextureNode { - - } - -+ getUniformHash( builder ) { -+ -+ const hash = super.getUniformHash( builder ); -+ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; -+ -+ } -+ - setup( builder ) { - - this.updateTexture(); - -+ if ( this.isRawTextureNode ) { -+ -+ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); -+ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); -+ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); -+ -+ } -+ - return super.setup( builder ); - - } - - clone() { - -- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); -+ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); - newNode.uvNode = this.uvNode; - newNode.levelNode = this.levelNode; - newNode.biasNode = this.biasNode; -@@ -40756,6 +40935,8 @@ class PassNode extends TempNode { - * @type {Object} - */ - this._textureNodes = {}; -+ this._rawTextureNodes = {}; -+ this._rawTexturesDisposed = false; - - /** - * A dictionary holding the internal depth nodes. -@@ -41062,6 +41243,7 @@ class PassNode extends TempNode { - this._previousTextures[ name ] = texture; - - this._textureNodes[ name ].updateTexture(); -+ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); - this._previousTextureNodes[ name ].updateTexture(); - - } -@@ -41074,6 +41256,17 @@ class PassNode extends TempNode { - * @param {string} [name='output'] - The output name to get the texture node for. - * @return {TextureNode} The texture node. - */ -+ getRawTextureNode( name = 'output' ) { -+ -+ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); -+ let node = this._rawTextureNodes[ name ]; -+ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); -+ node.updateTexture(); -+ return node; -+ -+ } -+ - getTextureNode( name = 'output' ) { - - let textureNode = this._textureNodes[ name ]; -@@ -41416,6 +41609,7 @@ class PassNode extends TempNode { - */ - dispose() { - -+ this._rawTexturesDisposed = true; - this.renderTarget.dispose(); - - } -@@ -44684,6 +44878,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44738,8 +44972,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44770,7 +45004,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45839,8 +46073,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45876,9 +46110,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -61399,6 +61633,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -62392,6 +62638,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -76688,6 +76942,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76904,7 +77161,12 @@ class WebGPUTextureUtils { - - } - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat ) { -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && -+ texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; -+ -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -76977,6 +77239,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -77017,7 +77295,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -77042,8 +77320,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77066,64 +77344,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -- -- return backend.get( depthTexture ).texture; -+ let cache = this._depthBuffers.get( depthTexture ); - -- } -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -- -- let format, type; -- -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -- -- return depthTextureGPU; -+ if ( buffer === undefined ) { - -- } -- -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77278,6 +77551,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77351,6 +77761,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -79277,7 +79689,7 @@ class WGSLNodeBuilder extends NodeBuilder { - * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. - * @return {string} The WGSL snippet. - */ -- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { -+ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { - - if ( levelSnippet === null ) levelSnippet = '0u'; - -@@ -79301,7 +79713,7 @@ class WGSLNodeBuilder extends NodeBuilder { - const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; - const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; - const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); -- const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; -+ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; - - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79843,7 +80255,7 @@ class WGSLNodeBuilder extends NodeBuilder { - - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - // Also textureGather always need sampler. -- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; -+ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); - - if ( needsSampler ) { - -@@ -80273,7 +80685,8 @@ ${ flowData.code } - - for ( const { name, property, type } of builtins.values() ) { - -- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); -+ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; -+ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); - - } - -@@ -80529,6 +80942,8 @@ ${ flowData.code } - getVaryings( shaderStage ) { - - const snippets = []; -+ const policy = this.context.sampleInterpolation; -+ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; - - if ( shaderStage === 'vertex' ) { - -@@ -80551,7 +80966,12 @@ ${ flowData.code } - - let attributesSnippet = `@location( ${ varyingIndex ++ } )`; - -- if ( varying.interpolationType ) { -+ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { -+ -+ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); -+ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; -+ -+ } else if ( varying.interpolationType ) { - - const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; - -@@ -80655,7 +81075,7 @@ ${ flowData.code } - - const { primarySamples } = backend.utils.getTextureSampleData( texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { - - multisampled = '_multisampled'; - -@@ -82153,7 +82573,20 @@ class WebGPUBindingUtils { - - let resourceGPU; - -- if ( textureData.externalTexture !== undefined ) { -+ if ( binding.textureNode?.isRawTextureNode === true ) { -+ -+ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; -+ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); -+ if ( textureData.rawTextureViewSource !== raw ) { -+ -+ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); -+ textureData.rawTextureViewSource = raw; -+ -+ } -+ resourceGPU = textureData.rawTextureView; -+ -+ } else if ( textureData.externalTexture !== undefined ) { - - resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); - -@@ -82324,7 +82757,7 @@ class WebGPUBindingUtils { - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { - - texture.multisampled = true; - -@@ -82362,7 +82795,7 @@ class WebGPUBindingUtils { - - } else if ( type === FloatType ) { - -- if ( this.backend.hasFeature( 'float32-filterable' ) ) { -+ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - -@@ -82879,6 +83312,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82893,7 +83335,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84148,6 +84590,19 @@ class WebGPUBackend extends Backend { - */ - this.gpu = null; - -+ /** -+ * A reference to the adapter this device was requested from. -+ * -+ * The adapter owns the wire instance every operation in flight completes on. Chromium -+ * collects an adapter the page stops referencing and the instance goes with it, so every -+ * operation still running completes with a shutdown the page reads as -+ * 'A valid external Instance reference no longer exists.' and presentation stops for. -+ * -+ * @type {?GPUAdapter} -+ * @default null -+ */ -+ this.adapter = null; -+ - /** - * A reference to the device. - * -@@ -84264,6 +84719,8 @@ class WebGPUBackend extends Backend { - - const adapter = this.gpu !== null ? await this.gpu.requestAdapter( adapterOptions ) : null; - -+ this.adapter = adapter; -+ - if ( adapter === null ) { - - throw new Error( 'THREE.WebGPUBackend: Unable to create WebGPU adapter.' ); -@@ -84489,7 +84946,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84516,6 +84975,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86256,6 +86718,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js -index 1245f0da9bb6a0f731b8c75a64490d7c91238700..95669d28b54250a3857dcb7d1b1b0e30dd19e0cb 100644 ---- a/build/three.webgpu.nodes.js -+++ b/build/three.webgpu.nodes.js -@@ -12307,7 +12307,8 @@ class TextureSizeNode extends Node { - const textureProperty = this.textureNode.build( builder, 'property' ); - const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); - -- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); -+ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; -+ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); - - } - -@@ -12958,7 +12959,7 @@ class TextureNode extends UniformNode { - - } else if ( this.sampler === false ) { - -- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); -+ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); - - } else if ( levelSnippet ) { - -@@ -18846,7 +18847,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve - - } - -- return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); -+ // The previous bones and world matrix must use the same frame's bind inverse. -+ const previousBindMatrixInverse = uniform( skinnedMesh.bindMatrixInverse, 'mat4' ).onObjectUpdate( ( { object } ) => { -+ -+ const mesh = object?.isSkinnedMesh === true ? object : skinnedMesh; -+ return mesh[ Symbol.for( 'threenative.velocity.previousBindMatrixInverse' ) ] ?? mesh.bindMatrixInverse; -+ -+ } ); -+ -+ return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, previousBindMatrixInverse, skinIndexNode, skinWeightNode ); - - } - -@@ -34205,6 +34214,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34354,7 +34367,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34518,7 +34540,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34528,7 +34559,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34545,6 +34576,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34577,9 +34619,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34757,6 +34924,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -40470,7 +40639,7 @@ class PassMultipleTextureNode extends PassTextureNode { - * @param {string} textureName - The output texture name. - * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. - */ -- constructor( passNode, textureName, previousTexture = false ) { -+ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { - - // null is passed to the super call since this class does not - // use an external texture for rendering pass data into. Instead -@@ -40491,6 +40660,8 @@ class PassMultipleTextureNode extends PassTextureNode { - * @type {boolean} - */ - this.previousTexture = previousTexture; -+ this.isRawTextureNode = rawTexture; -+ if ( rawTexture ) this.setSampler( false ); - - /** - * This flag can be used for type testing. -@@ -40512,17 +40683,33 @@ class PassMultipleTextureNode extends PassTextureNode { - - } - -+ getUniformHash( builder ) { -+ -+ const hash = super.getUniformHash( builder ); -+ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; -+ -+ } -+ - setup( builder ) { - - this.updateTexture(); - -+ if ( this.isRawTextureNode ) { -+ -+ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); -+ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); -+ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); -+ -+ } -+ - return super.setup( builder ); - - } - - clone() { - -- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); -+ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); - newNode.uvNode = this.uvNode; - newNode.levelNode = this.levelNode; - newNode.biasNode = this.biasNode; -@@ -40703,6 +40890,8 @@ class PassNode extends TempNode { - * @type {Object} - */ - this._textureNodes = {}; -+ this._rawTextureNodes = {}; -+ this._rawTexturesDisposed = false; - - /** - * A dictionary holding the internal depth nodes. -@@ -41009,6 +41198,7 @@ class PassNode extends TempNode { - this._previousTextures[ name ] = texture; - - this._textureNodes[ name ].updateTexture(); -+ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); - this._previousTextureNodes[ name ].updateTexture(); - - } -@@ -41021,6 +41211,17 @@ class PassNode extends TempNode { - * @param {string} [name='output'] - The output name to get the texture node for. - * @return {TextureNode} The texture node. - */ -+ getRawTextureNode( name = 'output' ) { -+ -+ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); -+ let node = this._rawTextureNodes[ name ]; -+ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); -+ node.updateTexture(); -+ return node; -+ -+ } -+ - getTextureNode( name = 'output' ) { - - let textureNode = this._textureNodes[ name ]; -@@ -41363,6 +41564,7 @@ class PassNode extends TempNode { - */ - dispose() { - -+ this._rawTexturesDisposed = true; - this.renderTarget.dispose(); - - } -@@ -44631,6 +44833,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44685,8 +44927,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44717,7 +44959,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45786,8 +46028,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45823,9 +46065,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -61346,6 +61588,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -62339,6 +62593,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -76624,6 +76886,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76840,7 +77105,12 @@ class WebGPUTextureUtils { - - } - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat ) { -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && -+ texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; -+ -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -76913,6 +77183,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -76953,7 +77239,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -76978,8 +77264,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77002,64 +77288,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -+ let cache = this._depthBuffers.get( depthTexture ); - -- return backend.get( depthTexture ).texture; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- } -- -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -- -- let format, type; -- -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -- -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -- -- return depthTextureGPU; -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77214,6 +77495,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77287,6 +77705,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -79213,7 +79633,7 @@ class WGSLNodeBuilder extends NodeBuilder { - * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. - * @return {string} The WGSL snippet. - */ -- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { -+ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { - - if ( levelSnippet === null ) levelSnippet = '0u'; - -@@ -79237,7 +79657,7 @@ class WGSLNodeBuilder extends NodeBuilder { - const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; - const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; - const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); -- const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; -+ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; - - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79779,7 +80199,7 @@ class WGSLNodeBuilder extends NodeBuilder { - - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - // Also textureGather always need sampler. -- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; -+ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); - - if ( needsSampler ) { - -@@ -80208,7 +80628,8 @@ ${ flowData.code } - - for ( const { name, property, type } of builtins.values() ) { - -- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); -+ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; -+ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); - - } - -@@ -80464,6 +80885,8 @@ ${ flowData.code } - getVaryings( shaderStage ) { - - const snippets = []; -+ const policy = this.context.sampleInterpolation; -+ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; - - if ( shaderStage === 'vertex' ) { - -@@ -80486,7 +80909,12 @@ ${ flowData.code } - - let attributesSnippet = `@location( ${ varyingIndex ++ } )`; - -- if ( varying.interpolationType ) { -+ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { -+ -+ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); -+ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; -+ -+ } else if ( varying.interpolationType ) { - - const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; - -@@ -80587,7 +81015,7 @@ ${ flowData.code } - - const { primarySamples } = backend.utils.getTextureSampleData( texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { - - multisampled = '_multisampled'; - -@@ -81342,6 +81770,15 @@ class WebGPUAttributeUtils { - buffer.unmap(); - - bufferData.buffer = buffer; -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ -+ } else if ( bufferData.version < bufferAttribute.version || -+ ( bufferAttribute.usage === DynamicDrawUsage && bufferData.uploadCall !== backend.renderer.info.calls ) ) { -+ -+ // A rebuilt shader can create a fresh attribute wrapper for this existing buffer. -+ // Synchronize its dirty bytes before Attributes records the wrapper as current. -+ this.updateAttribute( attribute ); - - } - -@@ -81362,6 +81799,11 @@ class WebGPUAttributeUtils { - const bufferData = backend.get( bufferAttribute ); - const buffer = backend.get( bufferAttribute ).buffer; - -+ // A fresh wrapper may already have uploaded this shared buffer in the same render or compute call. -+ // Do not repeat that write after its partial ranges have been consumed. -+ if ( bufferData.version === bufferAttribute.version && -+ ( bufferAttribute.usage !== DynamicDrawUsage || bufferData.uploadCall === backend.renderer.info.calls ) ) return; -+ - let array = bufferAttribute.array; - - const itemSize = bufferData._itemSize; -@@ -81443,6 +81885,9 @@ class WebGPUAttributeUtils { - - } - -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ - } - - /** -@@ -82070,7 +82515,20 @@ class WebGPUBindingUtils { - - let resourceGPU; - -- if ( textureData.externalTexture !== undefined ) { -+ if ( binding.textureNode?.isRawTextureNode === true ) { -+ -+ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; -+ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); -+ if ( textureData.rawTextureViewSource !== raw ) { -+ -+ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); -+ textureData.rawTextureViewSource = raw; -+ -+ } -+ resourceGPU = textureData.rawTextureView; -+ -+ } else if ( textureData.externalTexture !== undefined ) { - - resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); - -@@ -82241,7 +82699,7 @@ class WebGPUBindingUtils { - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { - - texture.multisampled = true; - -@@ -82279,7 +82737,7 @@ class WebGPUBindingUtils { - - } else if ( type === FloatType ) { - -- if ( this.backend.hasFeature( 'float32-filterable' ) ) { -+ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - -@@ -82793,6 +83251,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82807,7 +83274,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84393,7 +84860,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84420,6 +84889,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86156,6 +86628,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js -index 1b3a6e96291af0077f7f247040e657cf4aa0cb33..95669d28b54250a3857dcb7d1b1b0e30dd19e0cb 100644 ---- a/build/three.webgpu.nodes.js -+++ b/build/three.webgpu.nodes.js -@@ -12307,7 +12307,8 @@ class TextureSizeNode extends Node { - const textureProperty = this.textureNode.build( builder, 'property' ); - const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); - -- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); -+ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; -+ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); - - } - -@@ -12958,7 +12959,7 @@ class TextureNode extends UniformNode { - - } else if ( this.sampler === false ) { - -- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); -+ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); - - } else if ( levelSnippet ) { - -@@ -34213,6 +34214,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34362,7 +34367,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34526,7 +34540,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34536,7 +34559,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34553,6 +34576,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34585,9 +34619,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34765,6 +34924,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -40478,7 +40639,7 @@ class PassMultipleTextureNode extends PassTextureNode { - * @param {string} textureName - The output texture name. - * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. - */ -- constructor( passNode, textureName, previousTexture = false ) { -+ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { - - // null is passed to the super call since this class does not - // use an external texture for rendering pass data into. Instead -@@ -40499,6 +40660,8 @@ class PassMultipleTextureNode extends PassTextureNode { - * @type {boolean} - */ - this.previousTexture = previousTexture; -+ this.isRawTextureNode = rawTexture; -+ if ( rawTexture ) this.setSampler( false ); - - /** - * This flag can be used for type testing. -@@ -40520,17 +40683,33 @@ class PassMultipleTextureNode extends PassTextureNode { - - } - -+ getUniformHash( builder ) { -+ -+ const hash = super.getUniformHash( builder ); -+ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; -+ -+ } -+ - setup( builder ) { - - this.updateTexture(); - -+ if ( this.isRawTextureNode ) { -+ -+ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); -+ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); -+ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); -+ -+ } -+ - return super.setup( builder ); - - } - - clone() { - -- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); -+ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); - newNode.uvNode = this.uvNode; - newNode.levelNode = this.levelNode; - newNode.biasNode = this.biasNode; -@@ -40711,6 +40890,8 @@ class PassNode extends TempNode { - * @type {Object} - */ - this._textureNodes = {}; -+ this._rawTextureNodes = {}; -+ this._rawTexturesDisposed = false; - - /** - * A dictionary holding the internal depth nodes. -@@ -41017,6 +41198,7 @@ class PassNode extends TempNode { - this._previousTextures[ name ] = texture; - - this._textureNodes[ name ].updateTexture(); -+ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); - this._previousTextureNodes[ name ].updateTexture(); - - } -@@ -41029,6 +41211,17 @@ class PassNode extends TempNode { - * @param {string} [name='output'] - The output name to get the texture node for. - * @return {TextureNode} The texture node. - */ -+ getRawTextureNode( name = 'output' ) { -+ -+ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); -+ let node = this._rawTextureNodes[ name ]; -+ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); -+ node.updateTexture(); -+ return node; -+ -+ } -+ - getTextureNode( name = 'output' ) { - - let textureNode = this._textureNodes[ name ]; -@@ -41371,6 +41564,7 @@ class PassNode extends TempNode { - */ - dispose() { - -+ this._rawTexturesDisposed = true; - this.renderTarget.dispose(); - - } -@@ -44639,6 +44833,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44693,8 +44927,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44725,7 +44959,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45794,8 +46028,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45831,9 +46065,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -61354,6 +61588,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -62347,6 +62593,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -76632,6 +76886,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76848,7 +77105,12 @@ class WebGPUTextureUtils { - - } - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat ) { -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && -+ texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; -+ -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -76921,6 +77183,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -76961,7 +77239,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -76986,8 +77264,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77010,64 +77288,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -- -- return backend.get( depthTexture ).texture; -- -- } -- -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -+ let cache = this._depthBuffers.get( depthTexture ); - -- let format, type; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -- -- return depthTextureGPU; -- -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77222,6 +77495,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77295,6 +77705,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -79221,7 +79633,7 @@ class WGSLNodeBuilder extends NodeBuilder { - * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. - * @return {string} The WGSL snippet. - */ -- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { -+ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { - - if ( levelSnippet === null ) levelSnippet = '0u'; - -@@ -79245,7 +79657,7 @@ class WGSLNodeBuilder extends NodeBuilder { - const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; - const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; - const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); -- const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; -+ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; - - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79787,7 +80199,7 @@ class WGSLNodeBuilder extends NodeBuilder { - - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - // Also textureGather always need sampler. -- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; -+ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); - - if ( needsSampler ) { - -@@ -80216,7 +80628,8 @@ ${ flowData.code } - - for ( const { name, property, type } of builtins.values() ) { - -- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); -+ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; -+ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); - - } - -@@ -80472,6 +80885,8 @@ ${ flowData.code } - getVaryings( shaderStage ) { - - const snippets = []; -+ const policy = this.context.sampleInterpolation; -+ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; - - if ( shaderStage === 'vertex' ) { - -@@ -80494,7 +80909,12 @@ ${ flowData.code } - - let attributesSnippet = `@location( ${ varyingIndex ++ } )`; - -- if ( varying.interpolationType ) { -+ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { -+ -+ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); -+ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; -+ -+ } else if ( varying.interpolationType ) { - - const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; - -@@ -80595,7 +81015,7 @@ ${ flowData.code } - - const { primarySamples } = backend.utils.getTextureSampleData( texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { - - multisampled = '_multisampled'; - -@@ -82095,7 +82515,20 @@ class WebGPUBindingUtils { - - let resourceGPU; - -- if ( textureData.externalTexture !== undefined ) { -+ if ( binding.textureNode?.isRawTextureNode === true ) { -+ -+ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; -+ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); -+ if ( textureData.rawTextureViewSource !== raw ) { -+ -+ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); -+ textureData.rawTextureViewSource = raw; -+ -+ } -+ resourceGPU = textureData.rawTextureView; -+ -+ } else if ( textureData.externalTexture !== undefined ) { - - resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); - -@@ -82266,7 +82699,7 @@ class WebGPUBindingUtils { - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { - - texture.multisampled = true; - -@@ -82304,7 +82737,7 @@ class WebGPUBindingUtils { - - } else if ( type === FloatType ) { - -- if ( this.backend.hasFeature( 'float32-filterable' ) ) { -+ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - -@@ -82818,6 +83251,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82832,7 +83274,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84418,7 +84860,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84445,6 +84889,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86181,6 +86628,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js -index 3d47c1151d563f1e0ae2bb4b92edd057fe0df2b3..95669d28b54250a3857dcb7d1b1b0e30dd19e0cb 100644 ---- a/build/three.webgpu.nodes.js -+++ b/build/three.webgpu.nodes.js -@@ -12307,7 +12307,8 @@ class TextureSizeNode extends Node { - const textureProperty = this.textureNode.build( builder, 'property' ); - const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); - -- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); -+ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; -+ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); - - } - -@@ -12958,7 +12959,7 @@ class TextureNode extends UniformNode { - - } else if ( this.sampler === false ) { - -- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); -+ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); - - } else if ( levelSnippet ) { - -@@ -18539,7 +18540,7 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { - // Synchronization of dynamic buffer updates per frame - if ( interleavedMatrix !== null || interleavedColor !== null ) { - -- OnFrameUpdate( () => { -+ OnBeforeFrameUpdate( () => { - - if ( interleavedMatrix !== null ) { - -@@ -18580,11 +18581,28 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { - - const instancedMesh = builder.object; - -- OnObjectUpdate( ( { object } ) => { -+ const updatePrevious = ( object, array ) => { -+ -+ const { previousInstanceMatrix } = _previousInstanceMatrices.get( object ); -+ previousInstanceMatrix.array.set( array ); -+ previousInstanceMatrix.needsUpdate = true; -+ const interleaved = _matrixBuffers.get( previousInstanceMatrix ); -+ if ( interleaved !== undefined ) interleaved.needsUpdate = true; -+ -+ }; - -- const previousInstanceData = _previousInstanceMatrices.get( object ); -+ // Scheduled history must reach the GPU in this draw, before the geometry uploads. -+ OnBeforeObjectUpdate( ( { object } ) => { - -- previousInstanceData.previousInstanceMatrix.array.set( matrices.array ); -+ const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ]; -+ if ( scheduledPrevious !== undefined ) updatePrevious( object, scheduledPrevious ); -+ -+ } ); -+ -+ // Without a scheduler, retain Three's draw-time snapshot for the next draw. -+ OnObjectUpdate( ( { object } ) => { -+ -+ if ( object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ] === undefined ) updatePrevious( object, matrices.array ); - - } ); - -@@ -18829,7 +18847,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve - - } - -- return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); -+ // The previous bones and world matrix must use the same frame's bind inverse. -+ const previousBindMatrixInverse = uniform( skinnedMesh.bindMatrixInverse, 'mat4' ).onObjectUpdate( ( { object } ) => { -+ -+ const mesh = object?.isSkinnedMesh === true ? object : skinnedMesh; -+ return mesh[ Symbol.for( 'threenative.velocity.previousBindMatrixInverse' ) ] ?? mesh.bindMatrixInverse; -+ -+ } ); -+ -+ return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, previousBindMatrixInverse, skinIndexNode, skinWeightNode ); - - } - -@@ -34188,6 +34214,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34337,7 +34367,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34501,7 +34540,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34511,7 +34559,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34528,6 +34576,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34560,9 +34619,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34740,6 +34924,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -40453,7 +40639,7 @@ class PassMultipleTextureNode extends PassTextureNode { - * @param {string} textureName - The output texture name. - * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. - */ -- constructor( passNode, textureName, previousTexture = false ) { -+ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { - - // null is passed to the super call since this class does not - // use an external texture for rendering pass data into. Instead -@@ -40474,6 +40660,8 @@ class PassMultipleTextureNode extends PassTextureNode { - * @type {boolean} - */ - this.previousTexture = previousTexture; -+ this.isRawTextureNode = rawTexture; -+ if ( rawTexture ) this.setSampler( false ); - - /** - * This flag can be used for type testing. -@@ -40495,17 +40683,33 @@ class PassMultipleTextureNode extends PassTextureNode { - - } - -+ getUniformHash( builder ) { -+ -+ const hash = super.getUniformHash( builder ); -+ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; -+ -+ } -+ - setup( builder ) { - - this.updateTexture(); - -+ if ( this.isRawTextureNode ) { -+ -+ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); -+ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); -+ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); -+ -+ } -+ - return super.setup( builder ); - - } - - clone() { - -- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); -+ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); - newNode.uvNode = this.uvNode; - newNode.levelNode = this.levelNode; - newNode.biasNode = this.biasNode; -@@ -40686,6 +40890,8 @@ class PassNode extends TempNode { - * @type {Object} - */ - this._textureNodes = {}; -+ this._rawTextureNodes = {}; -+ this._rawTexturesDisposed = false; - - /** - * A dictionary holding the internal depth nodes. -@@ -40992,6 +41198,7 @@ class PassNode extends TempNode { - this._previousTextures[ name ] = texture; - - this._textureNodes[ name ].updateTexture(); -+ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); - this._previousTextureNodes[ name ].updateTexture(); - - } -@@ -41004,6 +41211,17 @@ class PassNode extends TempNode { - * @param {string} [name='output'] - The output name to get the texture node for. - * @return {TextureNode} The texture node. - */ -+ getRawTextureNode( name = 'output' ) { -+ -+ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); -+ let node = this._rawTextureNodes[ name ]; -+ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); -+ node.updateTexture(); -+ return node; -+ -+ } -+ - getTextureNode( name = 'output' ) { - - let textureNode = this._textureNodes[ name ]; -@@ -41346,6 +41564,7 @@ class PassNode extends TempNode { - */ - dispose() { - -+ this._rawTexturesDisposed = true; - this.renderTarget.dispose(); - - } -@@ -44614,6 +44833,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44668,8 +44927,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44700,7 +44959,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45769,8 +46028,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45806,9 +46065,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -61329,6 +61588,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -62322,6 +62593,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -63104,8 +63383,8 @@ class Renderer { - // attribute scan - because each of those answers for itself, and a step left out here - // cannot: a group a node added after this object's first draw still has to be created, and - // the pipeline reads that group's layout whatever this draw decided to do with it. -- this._geometries.updateForRender( renderObject ); - this._nodes.updateBefore( renderObject ); -+ this._geometries.updateForRender( renderObject ); - this._nodes.updateForRender( renderObject ); - this._bindings.updateForRender( renderObject ); - -@@ -76607,6 +76886,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76823,12 +77105,12 @@ class WebGPUTextureUtils { - - } - -- // Compute-only 3D storage has no attachment or render-pass mipmap consumer. -- // Requesting the unused attachment usage rejects the texture on wgpu-native. -- const computeOnlyStorage3D = dimension === GPUTextureDimension.ThreeD && texture.isStorageTexture === true && -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && - texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! computeOnlyStorage3D ) { -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -76901,6 +77183,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -76941,7 +77239,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -76966,8 +77264,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -76990,64 +77288,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -- -- return backend.get( depthTexture ).texture; -+ let cache = this._depthBuffers.get( depthTexture ); - -- } -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -- -- let format, type; -- -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -- -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- return depthTextureGPU; -- -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77202,6 +77495,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77275,6 +77705,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -79201,7 +79633,7 @@ class WGSLNodeBuilder extends NodeBuilder { - * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. - * @return {string} The WGSL snippet. - */ -- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { -+ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { - - if ( levelSnippet === null ) levelSnippet = '0u'; - -@@ -79225,7 +79657,7 @@ class WGSLNodeBuilder extends NodeBuilder { - const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; - const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; - const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); -- const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; -+ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; - - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79767,7 +80199,7 @@ class WGSLNodeBuilder extends NodeBuilder { - - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - // Also textureGather always need sampler. -- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; -+ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); - - if ( needsSampler ) { - -@@ -80196,7 +80628,8 @@ ${ flowData.code } - - for ( const { name, property, type } of builtins.values() ) { - -- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); -+ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; -+ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); - - } - -@@ -80452,6 +80885,8 @@ ${ flowData.code } - getVaryings( shaderStage ) { - - const snippets = []; -+ const policy = this.context.sampleInterpolation; -+ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; - - if ( shaderStage === 'vertex' ) { - -@@ -80474,7 +80909,12 @@ ${ flowData.code } - - let attributesSnippet = `@location( ${ varyingIndex ++ } )`; - -- if ( varying.interpolationType ) { -+ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { -+ -+ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); -+ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; -+ -+ } else if ( varying.interpolationType ) { - - const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; - -@@ -80575,7 +81015,7 @@ ${ flowData.code } - - const { primarySamples } = backend.utils.getTextureSampleData( texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { - - multisampled = '_multisampled'; - -@@ -81330,6 +81770,15 @@ class WebGPUAttributeUtils { - buffer.unmap(); - - bufferData.buffer = buffer; -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ -+ } else if ( bufferData.version < bufferAttribute.version || -+ ( bufferAttribute.usage === DynamicDrawUsage && bufferData.uploadCall !== backend.renderer.info.calls ) ) { -+ -+ // A rebuilt shader can create a fresh attribute wrapper for this existing buffer. -+ // Synchronize its dirty bytes before Attributes records the wrapper as current. -+ this.updateAttribute( attribute ); - - } - -@@ -81350,6 +81799,11 @@ class WebGPUAttributeUtils { - const bufferData = backend.get( bufferAttribute ); - const buffer = backend.get( bufferAttribute ).buffer; - -+ // A fresh wrapper may already have uploaded this shared buffer in the same render or compute call. -+ // Do not repeat that write after its partial ranges have been consumed. -+ if ( bufferData.version === bufferAttribute.version && -+ ( bufferAttribute.usage !== DynamicDrawUsage || bufferData.uploadCall === backend.renderer.info.calls ) ) return; -+ - let array = bufferAttribute.array; - - const itemSize = bufferData._itemSize; -@@ -81431,6 +81885,9 @@ class WebGPUAttributeUtils { - - } - -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ - } - - /** -@@ -82058,7 +82515,20 @@ class WebGPUBindingUtils { - - let resourceGPU; - -- if ( textureData.externalTexture !== undefined ) { -+ if ( binding.textureNode?.isRawTextureNode === true ) { -+ -+ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; -+ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); -+ if ( textureData.rawTextureViewSource !== raw ) { -+ -+ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); -+ textureData.rawTextureViewSource = raw; -+ -+ } -+ resourceGPU = textureData.rawTextureView; -+ -+ } else if ( textureData.externalTexture !== undefined ) { - - resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); - -@@ -82229,7 +82699,7 @@ class WebGPUBindingUtils { - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { - - texture.multisampled = true; - -@@ -82267,7 +82737,7 @@ class WebGPUBindingUtils { - - } else if ( type === FloatType ) { - -- if ( this.backend.hasFeature( 'float32-filterable' ) ) { -+ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - -@@ -82781,6 +83251,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82795,7 +83274,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84381,7 +84860,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84408,6 +84889,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86144,6 +86628,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js -index 81c24b2e05e5047e1a02410a2056949d39b1a05c..95669d28b54250a3857dcb7d1b1b0e30dd19e0cb 100644 ---- a/build/three.webgpu.nodes.js -+++ b/build/three.webgpu.nodes.js -@@ -12307,7 +12307,8 @@ class TextureSizeNode extends Node { - const textureProperty = this.textureNode.build( builder, 'property' ); - const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); - -- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); -+ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; -+ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); - - } - -@@ -12958,7 +12959,7 @@ class TextureNode extends UniformNode { - - } else if ( this.sampler === false ) { - -- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); -+ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); - - } else if ( levelSnippet ) { - -@@ -18846,7 +18847,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve - - } - -- return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); -+ // The previous bones and world matrix must use the same frame's bind inverse. -+ const previousBindMatrixInverse = uniform( skinnedMesh.bindMatrixInverse, 'mat4' ).onObjectUpdate( ( { object } ) => { -+ -+ const mesh = object?.isSkinnedMesh === true ? object : skinnedMesh; -+ return mesh[ Symbol.for( 'threenative.velocity.previousBindMatrixInverse' ) ] ?? mesh.bindMatrixInverse; -+ -+ } ); -+ -+ return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, previousBindMatrixInverse, skinIndexNode, skinWeightNode ); - - } - -@@ -34205,6 +34214,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34354,7 +34367,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34518,7 +34540,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34528,7 +34559,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34545,6 +34576,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34577,9 +34619,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34757,6 +34924,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -40470,7 +40639,7 @@ class PassMultipleTextureNode extends PassTextureNode { - * @param {string} textureName - The output texture name. - * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. - */ -- constructor( passNode, textureName, previousTexture = false ) { -+ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { - - // null is passed to the super call since this class does not - // use an external texture for rendering pass data into. Instead -@@ -40491,6 +40660,8 @@ class PassMultipleTextureNode extends PassTextureNode { - * @type {boolean} - */ - this.previousTexture = previousTexture; -+ this.isRawTextureNode = rawTexture; -+ if ( rawTexture ) this.setSampler( false ); - - /** - * This flag can be used for type testing. -@@ -40512,17 +40683,33 @@ class PassMultipleTextureNode extends PassTextureNode { - - } - -+ getUniformHash( builder ) { -+ -+ const hash = super.getUniformHash( builder ); -+ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; -+ -+ } -+ - setup( builder ) { - - this.updateTexture(); - -+ if ( this.isRawTextureNode ) { -+ -+ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); -+ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); -+ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); -+ -+ } -+ - return super.setup( builder ); - - } - - clone() { - -- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); -+ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); - newNode.uvNode = this.uvNode; - newNode.levelNode = this.levelNode; - newNode.biasNode = this.biasNode; -@@ -40703,6 +40890,8 @@ class PassNode extends TempNode { - * @type {Object} - */ - this._textureNodes = {}; -+ this._rawTextureNodes = {}; -+ this._rawTexturesDisposed = false; - - /** - * A dictionary holding the internal depth nodes. -@@ -41009,6 +41198,7 @@ class PassNode extends TempNode { - this._previousTextures[ name ] = texture; - - this._textureNodes[ name ].updateTexture(); -+ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); - this._previousTextureNodes[ name ].updateTexture(); - - } -@@ -41021,6 +41211,17 @@ class PassNode extends TempNode { - * @param {string} [name='output'] - The output name to get the texture node for. - * @return {TextureNode} The texture node. - */ -+ getRawTextureNode( name = 'output' ) { -+ -+ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); -+ let node = this._rawTextureNodes[ name ]; -+ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); -+ node.updateTexture(); -+ return node; -+ -+ } -+ - getTextureNode( name = 'output' ) { - - let textureNode = this._textureNodes[ name ]; -@@ -41363,6 +41564,7 @@ class PassNode extends TempNode { - */ - dispose() { - -+ this._rawTexturesDisposed = true; - this.renderTarget.dispose(); - - } -@@ -44631,6 +44833,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44685,8 +44927,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44717,7 +44959,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45786,8 +46028,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45823,9 +46065,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -61346,6 +61588,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -62339,6 +62593,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -76624,6 +76886,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76840,7 +77105,12 @@ class WebGPUTextureUtils { - - } - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat ) { -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && -+ texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; -+ -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -76913,6 +77183,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -76953,7 +77239,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -76978,8 +77264,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77002,64 +77288,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -- -- return backend.get( depthTexture ).texture; -- -- } -- -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -+ let cache = this._depthBuffers.get( depthTexture ); - -- let format, type; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -- -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- return depthTextureGPU; -- -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77214,6 +77495,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77287,6 +77705,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -79213,7 +79633,7 @@ class WGSLNodeBuilder extends NodeBuilder { - * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. - * @return {string} The WGSL snippet. - */ -- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { -+ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { - - if ( levelSnippet === null ) levelSnippet = '0u'; - -@@ -79237,7 +79657,7 @@ class WGSLNodeBuilder extends NodeBuilder { - const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; - const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; - const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); -- const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; -+ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; - - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79779,7 +80199,7 @@ class WGSLNodeBuilder extends NodeBuilder { - - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - // Also textureGather always need sampler. -- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; -+ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); - - if ( needsSampler ) { - -@@ -80208,7 +80628,8 @@ ${ flowData.code } - - for ( const { name, property, type } of builtins.values() ) { - -- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); -+ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; -+ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); - - } - -@@ -80464,6 +80885,8 @@ ${ flowData.code } - getVaryings( shaderStage ) { - - const snippets = []; -+ const policy = this.context.sampleInterpolation; -+ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; - - if ( shaderStage === 'vertex' ) { - -@@ -80486,7 +80909,12 @@ ${ flowData.code } - - let attributesSnippet = `@location( ${ varyingIndex ++ } )`; - -- if ( varying.interpolationType ) { -+ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { -+ -+ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); -+ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; -+ -+ } else if ( varying.interpolationType ) { - - const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; - -@@ -80587,7 +81015,7 @@ ${ flowData.code } - - const { primarySamples } = backend.utils.getTextureSampleData( texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { - - multisampled = '_multisampled'; - -@@ -82087,7 +82515,20 @@ class WebGPUBindingUtils { - - let resourceGPU; - -- if ( textureData.externalTexture !== undefined ) { -+ if ( binding.textureNode?.isRawTextureNode === true ) { -+ -+ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; -+ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); -+ if ( textureData.rawTextureViewSource !== raw ) { -+ -+ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); -+ textureData.rawTextureViewSource = raw; -+ -+ } -+ resourceGPU = textureData.rawTextureView; -+ -+ } else if ( textureData.externalTexture !== undefined ) { - - resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); - -@@ -82258,7 +82699,7 @@ class WebGPUBindingUtils { - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { - - texture.multisampled = true; - -@@ -82296,7 +82737,7 @@ class WebGPUBindingUtils { - - } else if ( type === FloatType ) { - -- if ( this.backend.hasFeature( 'float32-filterable' ) ) { -+ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - -@@ -82810,6 +83251,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82824,7 +83274,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84410,7 +84860,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84437,6 +84889,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86173,6 +86628,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js -index b8a7b2fd0a2e95911e7e12fd5674592107f012de..95669d28b54250a3857dcb7d1b1b0e30dd19e0cb 100644 ---- a/build/three.webgpu.nodes.js -+++ b/build/three.webgpu.nodes.js -@@ -12307,7 +12307,8 @@ class TextureSizeNode extends Node { - const textureProperty = this.textureNode.build( builder, 'property' ); - const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); - -- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); -+ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; -+ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); - - } - -@@ -12958,7 +12959,7 @@ class TextureNode extends UniformNode { - - } else if ( this.sampler === false ) { - -- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); -+ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); - - } else if ( levelSnippet ) { - -@@ -18539,7 +18540,7 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { - // Synchronization of dynamic buffer updates per frame - if ( interleavedMatrix !== null || interleavedColor !== null ) { - -- OnFrameUpdate( () => { -+ OnBeforeFrameUpdate( () => { - - if ( interleavedMatrix !== null ) { - -@@ -18580,11 +18581,28 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { - - const instancedMesh = builder.object; - -- OnObjectUpdate( ( { object } ) => { -+ const updatePrevious = ( object, array ) => { -+ -+ const { previousInstanceMatrix } = _previousInstanceMatrices.get( object ); -+ previousInstanceMatrix.array.set( array ); -+ previousInstanceMatrix.needsUpdate = true; -+ const interleaved = _matrixBuffers.get( previousInstanceMatrix ); -+ if ( interleaved !== undefined ) interleaved.needsUpdate = true; -+ -+ }; - -- const previousInstanceData = _previousInstanceMatrices.get( object ); -+ // Scheduled history must reach the GPU in this draw, before the geometry uploads. -+ OnBeforeObjectUpdate( ( { object } ) => { - -- previousInstanceData.previousInstanceMatrix.array.set( matrices.array ); -+ const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ]; -+ if ( scheduledPrevious !== undefined ) updatePrevious( object, scheduledPrevious ); -+ -+ } ); -+ -+ // Without a scheduler, retain Three's draw-time snapshot for the next draw. -+ OnObjectUpdate( ( { object } ) => { -+ -+ if ( object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ] === undefined ) updatePrevious( object, matrices.array ); - - } ); - -@@ -18829,7 +18847,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve - - } - -- return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); -+ // The previous bones and world matrix must use the same frame's bind inverse. -+ const previousBindMatrixInverse = uniform( skinnedMesh.bindMatrixInverse, 'mat4' ).onObjectUpdate( ( { object } ) => { -+ -+ const mesh = object?.isSkinnedMesh === true ? object : skinnedMesh; -+ return mesh[ Symbol.for( 'threenative.velocity.previousBindMatrixInverse' ) ] ?? mesh.bindMatrixInverse; -+ -+ } ); -+ -+ return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, previousBindMatrixInverse, skinIndexNode, skinWeightNode ); - - } - -@@ -34188,6 +34214,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34337,7 +34367,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34501,7 +34540,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34511,7 +34559,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34528,6 +34576,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34560,9 +34619,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34740,6 +34924,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -40453,7 +40639,7 @@ class PassMultipleTextureNode extends PassTextureNode { - * @param {string} textureName - The output texture name. - * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. - */ -- constructor( passNode, textureName, previousTexture = false ) { -+ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { - - // null is passed to the super call since this class does not - // use an external texture for rendering pass data into. Instead -@@ -40474,6 +40660,8 @@ class PassMultipleTextureNode extends PassTextureNode { - * @type {boolean} - */ - this.previousTexture = previousTexture; -+ this.isRawTextureNode = rawTexture; -+ if ( rawTexture ) this.setSampler( false ); - - /** - * This flag can be used for type testing. -@@ -40495,17 +40683,33 @@ class PassMultipleTextureNode extends PassTextureNode { - - } - -+ getUniformHash( builder ) { -+ -+ const hash = super.getUniformHash( builder ); -+ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; -+ -+ } -+ - setup( builder ) { - - this.updateTexture(); - -+ if ( this.isRawTextureNode ) { -+ -+ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); -+ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); -+ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); -+ -+ } -+ - return super.setup( builder ); - - } - - clone() { - -- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); -+ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); - newNode.uvNode = this.uvNode; - newNode.levelNode = this.levelNode; - newNode.biasNode = this.biasNode; -@@ -40686,6 +40890,8 @@ class PassNode extends TempNode { - * @type {Object} - */ - this._textureNodes = {}; -+ this._rawTextureNodes = {}; -+ this._rawTexturesDisposed = false; - - /** - * A dictionary holding the internal depth nodes. -@@ -40992,6 +41198,7 @@ class PassNode extends TempNode { - this._previousTextures[ name ] = texture; - - this._textureNodes[ name ].updateTexture(); -+ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); - this._previousTextureNodes[ name ].updateTexture(); - - } -@@ -41004,6 +41211,17 @@ class PassNode extends TempNode { - * @param {string} [name='output'] - The output name to get the texture node for. - * @return {TextureNode} The texture node. - */ -+ getRawTextureNode( name = 'output' ) { -+ -+ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); -+ let node = this._rawTextureNodes[ name ]; -+ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); -+ node.updateTexture(); -+ return node; -+ -+ } -+ - getTextureNode( name = 'output' ) { - - let textureNode = this._textureNodes[ name ]; -@@ -41346,6 +41564,7 @@ class PassNode extends TempNode { - */ - dispose() { - -+ this._rawTexturesDisposed = true; - this.renderTarget.dispose(); - - } -@@ -44614,6 +44833,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44668,8 +44927,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44700,7 +44959,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45769,8 +46028,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45806,9 +46065,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -61329,6 +61588,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -62322,6 +62593,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -63104,8 +63383,8 @@ class Renderer { - // attribute scan - because each of those answers for itself, and a step left out here - // cannot: a group a node added after this object's first draw still has to be created, and - // the pipeline reads that group's layout whatever this draw decided to do with it. -- this._geometries.updateForRender( renderObject ); - this._nodes.updateBefore( renderObject ); -+ this._geometries.updateForRender( renderObject ); - this._nodes.updateForRender( renderObject ); - this._bindings.updateForRender( renderObject ); - -@@ -76607,6 +76886,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76823,7 +77105,12 @@ class WebGPUTextureUtils { - - } - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat ) { -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && -+ texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; -+ -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -76896,6 +77183,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -76936,7 +77239,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -76961,8 +77264,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -76985,64 +77288,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -+ let cache = this._depthBuffers.get( depthTexture ); - -- return backend.get( depthTexture ).texture; -- -- } -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -- -- let format, type; -- -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -- -- return depthTextureGPU; -- -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77197,6 +77495,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77270,6 +77705,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -79196,7 +79633,7 @@ class WGSLNodeBuilder extends NodeBuilder { - * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. - * @return {string} The WGSL snippet. - */ -- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { -+ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { - - if ( levelSnippet === null ) levelSnippet = '0u'; - -@@ -79220,7 +79657,7 @@ class WGSLNodeBuilder extends NodeBuilder { - const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; - const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; - const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); -- const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; -+ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; - - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79762,7 +80199,7 @@ class WGSLNodeBuilder extends NodeBuilder { - - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - // Also textureGather always need sampler. -- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; -+ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); - - if ( needsSampler ) { - -@@ -80191,7 +80628,8 @@ ${ flowData.code } - - for ( const { name, property, type } of builtins.values() ) { - -- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); -+ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; -+ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); - - } - -@@ -80447,6 +80885,8 @@ ${ flowData.code } - getVaryings( shaderStage ) { - - const snippets = []; -+ const policy = this.context.sampleInterpolation; -+ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; - - if ( shaderStage === 'vertex' ) { - -@@ -80469,7 +80909,12 @@ ${ flowData.code } - - let attributesSnippet = `@location( ${ varyingIndex ++ } )`; - -- if ( varying.interpolationType ) { -+ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { -+ -+ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); -+ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; -+ -+ } else if ( varying.interpolationType ) { - - const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; - -@@ -80570,7 +81015,7 @@ ${ flowData.code } - - const { primarySamples } = backend.utils.getTextureSampleData( texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { - - multisampled = '_multisampled'; - -@@ -81325,6 +81770,15 @@ class WebGPUAttributeUtils { - buffer.unmap(); - - bufferData.buffer = buffer; -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ -+ } else if ( bufferData.version < bufferAttribute.version || -+ ( bufferAttribute.usage === DynamicDrawUsage && bufferData.uploadCall !== backend.renderer.info.calls ) ) { -+ -+ // A rebuilt shader can create a fresh attribute wrapper for this existing buffer. -+ // Synchronize its dirty bytes before Attributes records the wrapper as current. -+ this.updateAttribute( attribute ); - - } - -@@ -81345,6 +81799,11 @@ class WebGPUAttributeUtils { - const bufferData = backend.get( bufferAttribute ); - const buffer = backend.get( bufferAttribute ).buffer; - -+ // A fresh wrapper may already have uploaded this shared buffer in the same render or compute call. -+ // Do not repeat that write after its partial ranges have been consumed. -+ if ( bufferData.version === bufferAttribute.version && -+ ( bufferAttribute.usage !== DynamicDrawUsage || bufferData.uploadCall === backend.renderer.info.calls ) ) return; -+ - let array = bufferAttribute.array; - - const itemSize = bufferData._itemSize; -@@ -81426,6 +81885,9 @@ class WebGPUAttributeUtils { - - } - -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ - } - - /** -@@ -82053,7 +82515,20 @@ class WebGPUBindingUtils { - - let resourceGPU; - -- if ( textureData.externalTexture !== undefined ) { -+ if ( binding.textureNode?.isRawTextureNode === true ) { -+ -+ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; -+ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); -+ if ( textureData.rawTextureViewSource !== raw ) { -+ -+ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); -+ textureData.rawTextureViewSource = raw; -+ -+ } -+ resourceGPU = textureData.rawTextureView; -+ -+ } else if ( textureData.externalTexture !== undefined ) { - - resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); - -@@ -82224,7 +82699,7 @@ class WebGPUBindingUtils { - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { - - texture.multisampled = true; - -@@ -82262,7 +82737,7 @@ class WebGPUBindingUtils { - - } else if ( type === FloatType ) { - -- if ( this.backend.hasFeature( 'float32-filterable' ) ) { -+ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - -@@ -82776,6 +83251,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82790,7 +83274,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84376,7 +84860,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84403,6 +84889,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86139,6 +86628,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/src/nodes/accessors/Instance.js b/src/nodes/accessors/Instance.js -index ed00e75ba4fd7b49343cfada26eacdef8940a4f4..18e2b7943393fd07b7989b1e3a5231a61ca24187 100644 ---- a/src/nodes/accessors/Instance.js -+++ b/src/nodes/accessors/Instance.js -@@ -1,6 +1,6 @@ - - import { vec3, mat4, Fn } from '../tsl/TSLBase.js'; --import { OnFrameUpdate, OnObjectUpdate } from '../utils/EventNode.js'; -+import { OnBeforeFrameUpdate, OnBeforeObjectUpdate, OnObjectUpdate } from '../utils/EventNode.js'; - import { normalLocal, transformNormal } from './Normal.js'; - import { positionLocal, positionPrevious } from './Position.js'; - import { varyingProperty } from '../core/PropertyNode.js'; -@@ -177,7 +177,7 @@ export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder - // Synchronization of dynamic buffer updates per frame - if ( interleavedMatrix !== null || interleavedColor !== null ) { - -- OnFrameUpdate( () => { -+ OnBeforeFrameUpdate( () => { - - if ( interleavedMatrix !== null ) { - -@@ -218,12 +218,28 @@ export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder - - const instancedMesh = builder.object; - -- OnObjectUpdate( ( { object } ) => { -+ const updatePrevious = ( object, array ) => { -+ -+ const { previousInstanceMatrix } = _previousInstanceMatrices.get( object ); -+ previousInstanceMatrix.array.set( array ); -+ previousInstanceMatrix.needsUpdate = true; -+ const interleaved = _matrixBuffers.get( previousInstanceMatrix ); -+ if ( interleaved !== undefined ) interleaved.needsUpdate = true; - -- const previousInstanceData = _previousInstanceMatrices.get( object ); -+ }; -+ -+ // Scheduled history must reach the GPU in this draw, before the geometry uploads. -+ OnBeforeObjectUpdate( ( { object } ) => { - - const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ]; -- previousInstanceData.previousInstanceMatrix.array.set( scheduledPrevious ?? matrices.array ); -+ if ( scheduledPrevious !== undefined ) updatePrevious( object, scheduledPrevious ); -+ -+ } ); -+ -+ // Without a scheduler, retain Three's draw-time snapshot for the next draw. -+ OnObjectUpdate( ( { object } ) => { -+ -+ if ( object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ] === undefined ) updatePrevious( object, matrices.array ); - - } ); - -diff --git a/src/nodes/accessors/Skinning.js b/src/nodes/accessors/Skinning.js -index aaeea633a8ece291be5faf868e312867ddad01b6..76303e765bc29853cfd7f139dd3ec76545b70798 100644 ---- a/src/nodes/accessors/Skinning.js -+++ b/src/nodes/accessors/Skinning.js -@@ -117,7 +117,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve - - } - -- return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); -+ // The previous bones and world matrix must use the same frame's bind inverse. -+ const previousBindMatrixInverse = uniform( skinnedMesh.bindMatrixInverse, 'mat4' ).onObjectUpdate( ( { object } ) => { -+ -+ const mesh = object?.isSkinnedMesh === true ? object : skinnedMesh; -+ return mesh[ Symbol.for( 'threenative.velocity.previousBindMatrixInverse' ) ] ?? mesh.bindMatrixInverse; -+ -+ } ); -+ -+ return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, previousBindMatrixInverse, skinIndexNode, skinWeightNode ); - - } - -diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js -index 89b33efb80ca1dcb9db931071dfda62ffdba3526..b1293bfbe2ccf0583711813c625ddd5c5ba78620 100644 ---- a/src/renderers/common/Renderer.js -+++ b/src/renderers/common/Renderer.js -@@ -1937,6 +1937,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -2930,6 +2942,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -3723,8 +3743,8 @@ class Renderer { - // attribute scan - because each of those answers for itself, and a step left out here - // cannot: a group a node added after this object's first draw still has to be created, and - // the pipeline reads that group's layout whatever this draw decided to do with it. -- this._geometries.updateForRender( renderObject ); - this._nodes.updateBefore( renderObject ); -+ this._geometries.updateForRender( renderObject ); - this._nodes.updateForRender( renderObject ); - this._bindings.updateForRender( renderObject ); - -diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js -index 69b8b67fdeec5be24de34d8382beda9020642f77..5636b40e6708a490e926e5f590675834d23bf326 100644 ---- a/src/renderers/webgpu/WebGPUBackend.js -+++ b/src/renderers/webgpu/WebGPUBackend.js -@@ -110,6 +110,19 @@ class WebGPUBackend extends Backend { - */ - this.gpu = null; - -+ /** -+ * A reference to the adapter this device was requested from. -+ * -+ * The adapter owns the wire instance every operation in flight completes on. Chromium -+ * collects an adapter the page stops referencing and the instance goes with it, so every -+ * operation still running completes with a shutdown the page reads as -+ * 'A valid external Instance reference no longer exists.' and presentation stops for. -+ * -+ * @type {?GPUAdapter} -+ * @default null -+ */ -+ this.adapter = null; -+ - /** - * A reference to the device. - * -@@ -226,6 +239,8 @@ class WebGPUBackend extends Backend { - - const adapter = this.gpu !== null ? await this.gpu.requestAdapter( adapterOptions ) : null; - -+ this.adapter = adapter; -+ - if ( adapter === null ) { - - throw new Error( 'THREE.WebGPUBackend: Unable to create WebGPU adapter.' ); -@@ -451,7 +466,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -478,6 +495,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -2218,6 +2238,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/src/renderers/webgpu/utils/WebGPUAttributeUtils.js b/src/renderers/webgpu/utils/WebGPUAttributeUtils.js -index 83bbe189915c8450c4e14108e6a3f4ed98c4b016..16bd72d0c0f1d693481efc98140a0f1b73905149 100644 ---- a/src/renderers/webgpu/utils/WebGPUAttributeUtils.js -+++ b/src/renderers/webgpu/utils/WebGPUAttributeUtils.js -@@ -3,6 +3,7 @@ import { submit } from './WebGPUUtils.js'; - import GPUBufferDescriptor from '../descriptors/GPUBufferDescriptor.js'; - import GPUCommandEncoderDescriptor from '../descriptors/GPUCommandEncoderDescriptor.js'; - -+import { DynamicDrawUsage } from '../../../constants.js'; - import { Float16BufferAttribute } from '../../../core/BufferAttribute.js'; - import { isTypedArray, error } from '../../../utils.js'; - -@@ -174,6 +175,15 @@ class WebGPUAttributeUtils { - buffer.unmap(); - - bufferData.buffer = buffer; -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ -+ } else if ( bufferData.version < bufferAttribute.version || -+ ( bufferAttribute.usage === DynamicDrawUsage && bufferData.uploadCall !== backend.renderer.info.calls ) ) { -+ -+ // A rebuilt shader can create a fresh attribute wrapper for this existing buffer. -+ // Synchronize its dirty bytes before Attributes records the wrapper as current. -+ this.updateAttribute( attribute ); - - } - -@@ -194,6 +204,11 @@ class WebGPUAttributeUtils { - const bufferData = backend.get( bufferAttribute ); - const buffer = backend.get( bufferAttribute ).buffer; - -+ // A fresh wrapper may already have uploaded this shared buffer in the same render or compute call. -+ // Do not repeat that write after its partial ranges have been consumed. -+ if ( bufferData.version === bufferAttribute.version && -+ ( bufferAttribute.usage !== DynamicDrawUsage || bufferData.uploadCall === backend.renderer.info.calls ) ) return; -+ - let array = bufferAttribute.array; - - const itemSize = bufferData._itemSize; -@@ -275,6 +290,9 @@ class WebGPUAttributeUtils { - - } - -+ bufferData.version = bufferAttribute.version; -+ bufferData.uploadCall = backend.renderer.info.calls; -+ - } - - /** -diff --git a/src/renderers/webgpu/utils/WebGPUTextureUtils.js b/src/renderers/webgpu/utils/WebGPUTextureUtils.js -index ff8ee2dbace2816105aa7152891c04f2333f33e8..57c1eb62f1e0de063f22e08ec879e31d2a3240b6 100644 ---- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js -+++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js -@@ -41,6 +41,7 @@ import { - Compatibility - } from '../../../constants.js'; - import { CubeTexture } from '../../../textures/CubeTexture.js'; -+import { DepthTexture } from '../../../textures/DepthTexture.js'; - import { Texture } from '../../../textures/Texture.js'; - import { warn, error } from '../../../utils.js'; - -@@ -143,6 +144,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -359,7 +363,12 @@ class WebGPUTextureUtils { - - } - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat ) { -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && -+ texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; -+ -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -432,6 +441,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -472,7 +497,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -497,8 +522,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -521,64 +546,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -- -- return backend.get( depthTexture ).texture; -- -- } -- -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -+ let cache = this._depthBuffers.get( depthTexture ); - -- let format, type; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -- -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- return depthTextureGPU; -+ if ( buffer === undefined ) { - -- } -- -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -733,6 +753,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -806,6 +963,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -diff --git a/build/three.webgpu.js b/build/three.webgpu.js -index 9166d6f2dd1ac1505fa27b625f1976dff6b8ebc1..a308a892c59283b3b9a71477b3b02d64db40becd 100644 ---- a/build/three.webgpu.js -+++ b/build/three.webgpu.js -@@ -12307,7 +12307,8 @@ class TextureSizeNode extends Node { - const textureProperty = this.textureNode.build( builder, 'property' ); - const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); - -- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); -+ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; -+ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); - - } - -@@ -12958,7 +12959,7 @@ class TextureNode extends UniformNode { - - } else if ( this.sampler === false ) { - -- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); -+ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); - - } else if ( levelSnippet ) { - -@@ -34257,6 +34258,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34406,7 +34411,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34570,7 +34584,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34580,7 +34603,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34597,6 +34620,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34629,9 +34663,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34809,6 +34968,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -40523,7 +40684,7 @@ class PassMultipleTextureNode extends PassTextureNode { - * @param {string} textureName - The output texture name. - * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. - */ -- constructor( passNode, textureName, previousTexture = false ) { -+ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { - - // null is passed to the super call since this class does not - // use an external texture for rendering pass data into. Instead -@@ -40544,6 +40705,8 @@ class PassMultipleTextureNode extends PassTextureNode { - * @type {boolean} - */ - this.previousTexture = previousTexture; -+ this.isRawTextureNode = rawTexture; -+ if ( rawTexture ) this.setSampler( false ); - - /** - * This flag can be used for type testing. -@@ -40565,17 +40728,33 @@ class PassMultipleTextureNode extends PassTextureNode { - - } - -+ getUniformHash( builder ) { -+ -+ const hash = super.getUniformHash( builder ); -+ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; -+ -+ } -+ - setup( builder ) { - - this.updateTexture(); - -+ if ( this.isRawTextureNode ) { -+ -+ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); -+ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); -+ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); -+ -+ } -+ - return super.setup( builder ); - - } - - clone() { - -- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); -+ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); - newNode.uvNode = this.uvNode; - newNode.levelNode = this.levelNode; - newNode.biasNode = this.biasNode; -@@ -40756,6 +40935,8 @@ class PassNode extends TempNode { - * @type {Object} - */ - this._textureNodes = {}; -+ this._rawTextureNodes = {}; -+ this._rawTexturesDisposed = false; - - /** - * A dictionary holding the internal depth nodes. -@@ -41062,6 +41243,7 @@ class PassNode extends TempNode { - this._previousTextures[ name ] = texture; - - this._textureNodes[ name ].updateTexture(); -+ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); - this._previousTextureNodes[ name ].updateTexture(); - - } -@@ -41074,6 +41256,17 @@ class PassNode extends TempNode { - * @param {string} [name='output'] - The output name to get the texture node for. - * @return {TextureNode} The texture node. - */ -+ getRawTextureNode( name = 'output' ) { -+ -+ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); -+ let node = this._rawTextureNodes[ name ]; -+ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); -+ node.updateTexture(); -+ return node; -+ -+ } -+ - getTextureNode( name = 'output' ) { - - let textureNode = this._textureNodes[ name ]; -@@ -41416,6 +41609,7 @@ class PassNode extends TempNode { - */ - dispose() { - -+ this._rawTexturesDisposed = true; - this.renderTarget.dispose(); - - } -@@ -44684,6 +44878,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44738,8 +44972,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44770,7 +45004,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45839,8 +46073,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45876,9 +46110,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -61399,6 +61633,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -62392,6 +62638,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -76688,6 +76942,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76904,12 +77161,12 @@ class WebGPUTextureUtils { - - } - -- // Compute-only 3D storage has no attachment or render-pass mipmap consumer. -- // Requesting the unused attachment usage rejects the texture on wgpu-native. -- const computeOnlyStorage3D = dimension === GPUTextureDimension.ThreeD && texture.isStorageTexture === true && -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && - texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! computeOnlyStorage3D ) { -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -76982,6 +77239,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -77022,7 +77295,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -77047,8 +77320,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77071,64 +77344,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -- -- return backend.get( depthTexture ).texture; -- -- } -- -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -+ let cache = this._depthBuffers.get( depthTexture ); - -- let format, type; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -- -- return depthTextureGPU; -- -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77283,6 +77551,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77356,6 +77761,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -79282,7 +79689,7 @@ class WGSLNodeBuilder extends NodeBuilder { - * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. - * @return {string} The WGSL snippet. - */ -- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { -+ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { - - if ( levelSnippet === null ) levelSnippet = '0u'; - -@@ -79306,7 +79713,7 @@ class WGSLNodeBuilder extends NodeBuilder { - const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; - const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; - const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); -- const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; -+ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; - - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79848,7 +80255,7 @@ class WGSLNodeBuilder extends NodeBuilder { - - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - // Also textureGather always need sampler. -- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; -+ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); - - if ( needsSampler ) { - -@@ -80278,7 +80685,8 @@ ${ flowData.code } - - for ( const { name, property, type } of builtins.values() ) { - -- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); -+ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; -+ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); - - } - -@@ -80534,6 +80942,8 @@ ${ flowData.code } - getVaryings( shaderStage ) { - - const snippets = []; -+ const policy = this.context.sampleInterpolation; -+ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; - - if ( shaderStage === 'vertex' ) { - -@@ -80556,7 +80966,12 @@ ${ flowData.code } - - let attributesSnippet = `@location( ${ varyingIndex ++ } )`; - -- if ( varying.interpolationType ) { -+ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { -+ -+ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); -+ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; -+ -+ } else if ( varying.interpolationType ) { - - const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; - -@@ -80660,7 +81075,7 @@ ${ flowData.code } - - const { primarySamples } = backend.utils.getTextureSampleData( texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { - - multisampled = '_multisampled'; - -@@ -82158,7 +82573,20 @@ class WebGPUBindingUtils { - - let resourceGPU; - -- if ( textureData.externalTexture !== undefined ) { -+ if ( binding.textureNode?.isRawTextureNode === true ) { -+ -+ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; -+ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); -+ if ( textureData.rawTextureViewSource !== raw ) { -+ -+ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); -+ textureData.rawTextureViewSource = raw; -+ -+ } -+ resourceGPU = textureData.rawTextureView; -+ -+ } else if ( textureData.externalTexture !== undefined ) { - - resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); - -@@ -82329,7 +82757,7 @@ class WebGPUBindingUtils { - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { - - texture.multisampled = true; - -@@ -82367,7 +82795,7 @@ class WebGPUBindingUtils { - - } else if ( type === FloatType ) { - -- if ( this.backend.hasFeature( 'float32-filterable' ) ) { -+ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - -@@ -82884,6 +83312,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82898,7 +83335,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84509,7 +84946,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84536,6 +84975,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86276,6 +86718,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js -index 9e7c7f74be1191b761711bd70ee59cc033e586cb..95669d28b54250a3857dcb7d1b1b0e30dd19e0cb 100644 ---- a/build/three.webgpu.nodes.js -+++ b/build/three.webgpu.nodes.js -@@ -12307,7 +12307,8 @@ class TextureSizeNode extends Node { - const textureProperty = this.textureNode.build( builder, 'property' ); - const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); - -- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); -+ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; -+ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); - - } - -@@ -12958,7 +12959,7 @@ class TextureNode extends UniformNode { - - } else if ( this.sampler === false ) { - -- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); -+ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); - - } else if ( levelSnippet ) { - -@@ -34213,6 +34214,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34362,7 +34367,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34526,7 +34540,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34536,7 +34559,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34553,6 +34576,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34585,9 +34619,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34765,6 +34924,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -40478,7 +40639,7 @@ class PassMultipleTextureNode extends PassTextureNode { - * @param {string} textureName - The output texture name. - * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. - */ -- constructor( passNode, textureName, previousTexture = false ) { -+ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { - - // null is passed to the super call since this class does not - // use an external texture for rendering pass data into. Instead -@@ -40499,6 +40660,8 @@ class PassMultipleTextureNode extends PassTextureNode { - * @type {boolean} - */ - this.previousTexture = previousTexture; -+ this.isRawTextureNode = rawTexture; -+ if ( rawTexture ) this.setSampler( false ); - - /** - * This flag can be used for type testing. -@@ -40520,17 +40683,33 @@ class PassMultipleTextureNode extends PassTextureNode { - - } - -+ getUniformHash( builder ) { -+ -+ const hash = super.getUniformHash( builder ); -+ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; -+ -+ } -+ - setup( builder ) { - - this.updateTexture(); - -+ if ( this.isRawTextureNode ) { -+ -+ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); -+ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); -+ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); -+ -+ } -+ - return super.setup( builder ); - - } - - clone() { - -- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); -+ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); - newNode.uvNode = this.uvNode; - newNode.levelNode = this.levelNode; - newNode.biasNode = this.biasNode; -@@ -40711,6 +40890,8 @@ class PassNode extends TempNode { - * @type {Object} - */ - this._textureNodes = {}; -+ this._rawTextureNodes = {}; -+ this._rawTexturesDisposed = false; - - /** - * A dictionary holding the internal depth nodes. -@@ -41017,6 +41198,7 @@ class PassNode extends TempNode { - this._previousTextures[ name ] = texture; - - this._textureNodes[ name ].updateTexture(); -+ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); - this._previousTextureNodes[ name ].updateTexture(); - - } -@@ -41029,6 +41211,17 @@ class PassNode extends TempNode { - * @param {string} [name='output'] - The output name to get the texture node for. - * @return {TextureNode} The texture node. - */ -+ getRawTextureNode( name = 'output' ) { -+ -+ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); -+ let node = this._rawTextureNodes[ name ]; -+ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); -+ node.updateTexture(); -+ return node; -+ -+ } -+ - getTextureNode( name = 'output' ) { - - let textureNode = this._textureNodes[ name ]; -@@ -41371,6 +41564,7 @@ class PassNode extends TempNode { - */ - dispose() { - -+ this._rawTexturesDisposed = true; - this.renderTarget.dispose(); - - } -@@ -44639,6 +44833,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44693,8 +44927,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44725,7 +44959,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45794,8 +46028,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45831,9 +46065,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -61354,6 +61588,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -62347,6 +62593,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -76632,6 +76886,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76848,12 +77105,12 @@ class WebGPUTextureUtils { - - } - -- // Compute-only 3D storage has no attachment or render-pass mipmap consumer. -- // Requesting the unused attachment usage rejects the texture on wgpu-native. -- const computeOnlyStorage3D = dimension === GPUTextureDimension.ThreeD && texture.isStorageTexture === true && -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && - texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! computeOnlyStorage3D ) { -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -76926,6 +77183,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -76966,7 +77239,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -76991,8 +77264,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77015,64 +77288,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -- -- return backend.get( depthTexture ).texture; -- -- } -- -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -+ let cache = this._depthBuffers.get( depthTexture ); - -- let format, type; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -- -- return depthTextureGPU; -- -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77227,6 +77495,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77300,6 +77705,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -79226,7 +79633,7 @@ class WGSLNodeBuilder extends NodeBuilder { - * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. - * @return {string} The WGSL snippet. - */ -- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { -+ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { - - if ( levelSnippet === null ) levelSnippet = '0u'; - -@@ -79250,7 +79657,7 @@ class WGSLNodeBuilder extends NodeBuilder { - const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; - const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; - const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); -- const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; -+ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; - - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79792,7 +80199,7 @@ class WGSLNodeBuilder extends NodeBuilder { - - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - // Also textureGather always need sampler. -- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; -+ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); - - if ( needsSampler ) { - -@@ -80221,7 +80628,8 @@ ${ flowData.code } - - for ( const { name, property, type } of builtins.values() ) { - -- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); -+ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; -+ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); - - } - -@@ -80477,6 +80885,8 @@ ${ flowData.code } - getVaryings( shaderStage ) { - - const snippets = []; -+ const policy = this.context.sampleInterpolation; -+ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; - - if ( shaderStage === 'vertex' ) { - -@@ -80499,7 +80909,12 @@ ${ flowData.code } - - let attributesSnippet = `@location( ${ varyingIndex ++ } )`; - -- if ( varying.interpolationType ) { -+ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { -+ -+ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); -+ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; -+ -+ } else if ( varying.interpolationType ) { - - const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; - -@@ -80600,7 +81015,7 @@ ${ flowData.code } - - const { primarySamples } = backend.utils.getTextureSampleData( texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { - - multisampled = '_multisampled'; - -@@ -82100,7 +82515,20 @@ class WebGPUBindingUtils { - - let resourceGPU; - -- if ( textureData.externalTexture !== undefined ) { -+ if ( binding.textureNode?.isRawTextureNode === true ) { -+ -+ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; -+ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); -+ if ( textureData.rawTextureViewSource !== raw ) { -+ -+ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); -+ textureData.rawTextureViewSource = raw; -+ -+ } -+ resourceGPU = textureData.rawTextureView; -+ -+ } else if ( textureData.externalTexture !== undefined ) { - - resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); - -@@ -82271,7 +82699,7 @@ class WebGPUBindingUtils { - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { - - texture.multisampled = true; - -@@ -82309,7 +82737,7 @@ class WebGPUBindingUtils { - - } else if ( type === FloatType ) { - -- if ( this.backend.hasFeature( 'float32-filterable' ) ) { -+ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - -@@ -82823,6 +83251,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82837,7 +83274,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84423,7 +84860,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84450,6 +84889,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86186,6 +86628,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/src/nodes/accessors/TextureNode.js b/src/nodes/accessors/TextureNode.js -index f886ed21397ab39d6fdbc58e5ac57729f1871f8a..1a138b1a055d0867e7686531930d4d557e1a20df 100644 ---- a/src/nodes/accessors/TextureNode.js -+++ b/src/nodes/accessors/TextureNode.js -@@ -524,7 +524,7 @@ class TextureNode extends UniformNode { - - } else if ( this.sampler === false ) { - -- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); -+ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); - - } else if ( levelSnippet ) { - -diff --git a/src/nodes/accessors/TextureSizeNode.js b/src/nodes/accessors/TextureSizeNode.js -index d4668281657ef8b1acad761e0d86a0716909eee4..43eccaa01bcb12ea6e02c94232d5ee76bcac0d01 100644 ---- a/src/nodes/accessors/TextureSizeNode.js -+++ b/src/nodes/accessors/TextureSizeNode.js -@@ -57,7 +57,8 @@ class TextureSizeNode extends Node { - const textureProperty = this.textureNode.build( builder, 'property' ); - const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); - -- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); -+ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; -+ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); - - } - -diff --git a/src/nodes/display/PassNode.js b/src/nodes/display/PassNode.js -index cc827a55cb82a6325296dcc5c55a4246f4f70aed..3cdf66cfe0760066bc062bf7ce3f3a8000eb2b0d 100644 ---- a/src/nodes/display/PassNode.js -+++ b/src/nodes/display/PassNode.js -@@ -95,7 +95,7 @@ class PassMultipleTextureNode extends PassTextureNode { - * @param {string} textureName - The output texture name. - * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. - */ -- constructor( passNode, textureName, previousTexture = false ) { -+ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { - - // null is passed to the super call since this class does not - // use an external texture for rendering pass data into. Instead -@@ -116,6 +116,8 @@ class PassMultipleTextureNode extends PassTextureNode { - * @type {boolean} - */ - this.previousTexture = previousTexture; -+ this.isRawTextureNode = rawTexture; -+ if ( rawTexture ) this.setSampler( false ); - - /** - * This flag can be used for type testing. -@@ -137,17 +139,33 @@ class PassMultipleTextureNode extends PassTextureNode { - - } - -+ getUniformHash( builder ) { -+ -+ const hash = super.getUniformHash( builder ); -+ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; -+ -+ } -+ - setup( builder ) { - - this.updateTexture(); - -+ if ( this.isRawTextureNode ) { -+ -+ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); -+ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); -+ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); -+ -+ } -+ - return super.setup( builder ); - - } - - clone() { - -- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); -+ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); - newNode.uvNode = this.uvNode; - newNode.levelNode = this.levelNode; - newNode.biasNode = this.biasNode; -@@ -328,6 +346,8 @@ class PassNode extends TempNode { - * @type {Object} - */ - this._textureNodes = {}; -+ this._rawTextureNodes = {}; -+ this._rawTexturesDisposed = false; - - /** - * A dictionary holding the internal depth nodes. -@@ -634,6 +654,7 @@ class PassNode extends TempNode { - this._previousTextures[ name ] = texture; - - this._textureNodes[ name ].updateTexture(); -+ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); - this._previousTextureNodes[ name ].updateTexture(); - - } -@@ -646,6 +667,17 @@ class PassNode extends TempNode { - * @param {string} [name='output'] - The output name to get the texture node for. - * @return {TextureNode} The texture node. - */ -+ getRawTextureNode( name = 'output' ) { -+ -+ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); -+ let node = this._rawTextureNodes[ name ]; -+ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); -+ node.updateTexture(); -+ return node; -+ -+ } -+ - getTextureNode( name = 'output' ) { - - let textureNode = this._textureNodes[ name ]; -@@ -988,6 +1020,7 @@ class PassNode extends TempNode { - */ - dispose() { - -+ this._rawTexturesDisposed = true; - this.renderTarget.dispose(); - - } -diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js -index 51590d5a23a93365e5cd9fb0360678750270d5bf..b1293bfbe2ccf0583711813c625ddd5c5ba78620 100644 ---- a/src/renderers/common/Renderer.js -+++ b/src/renderers/common/Renderer.js -@@ -1937,6 +1937,18 @@ class Renderer { - - } - -+ /** -+ * Releases an exclusively owned attribute through the backend and memory accounting. -+ * Wait for asynchronous readers before releasing storage shared by compute dispatches. -+ * @param {BufferAttribute} attribute - The owned attribute to release. -+ * @return {?Object} The removed attribute data, or null when absent/uninitialized. -+ */ -+ deleteAttribute( attribute ) { -+ -+ return this._attributes === null ? null : this._attributes.delete( attribute ); -+ -+ } -+ - /** - * Can be used to transfer buffer data from a storage buffer attribute - * from the GPU to the CPU in context of compute shaders. -@@ -2930,6 +2942,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js -index 2acc832a628f43adac49614dad8e593ef058c398..1cbc9b3e02c9832d69a8aadb3a6003392b443c8d 100644 ---- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js -+++ b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js -@@ -723,7 +723,7 @@ class WGSLNodeBuilder extends NodeBuilder { - * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. - * @return {string} The WGSL snippet. - */ -- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { -+ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { - - if ( levelSnippet === null ) levelSnippet = '0u'; - -@@ -747,7 +747,7 @@ class WGSLNodeBuilder extends NodeBuilder { - const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; - const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; - const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); -- const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; -+ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; - - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -1289,7 +1289,7 @@ class WGSLNodeBuilder extends NodeBuilder { - - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - // Also textureGather always need sampler. -- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; -+ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); - - if ( needsSampler ) { - -@@ -1719,7 +1719,8 @@ ${ flowData.code } - - for ( const { name, property, type } of builtins.values() ) { - -- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); -+ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; -+ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); - - } - -@@ -1975,6 +1976,8 @@ ${ flowData.code } - getVaryings( shaderStage ) { - - const snippets = []; -+ const policy = this.context.sampleInterpolation; -+ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; - - if ( shaderStage === 'vertex' ) { - -@@ -1997,7 +2000,12 @@ ${ flowData.code } - - let attributesSnippet = `@location( ${ varyingIndex ++ } )`; - -- if ( varying.interpolationType ) { -+ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { -+ -+ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); -+ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; -+ -+ } else if ( varying.interpolationType ) { - - const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; - -@@ -2101,7 +2109,7 @@ ${ flowData.code } - - const { primarySamples } = backend.utils.getTextureSampleData( texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { - - multisampled = '_multisampled'; - -diff --git a/src/renderers/webgpu/utils/WebGPUBindingUtils.js b/src/renderers/webgpu/utils/WebGPUBindingUtils.js -index a5da7ca09f746c0b2859ef173df663ccdc851ebc..c479d0a88794b01bca531e7cbfb780cf93fc4721 100644 ---- a/src/renderers/webgpu/utils/WebGPUBindingUtils.js -+++ b/src/renderers/webgpu/utils/WebGPUBindingUtils.js -@@ -327,7 +327,20 @@ class WebGPUBindingUtils { - - let resourceGPU; - -- if ( textureData.externalTexture !== undefined ) { -+ if ( binding.textureNode?.isRawTextureNode === true ) { -+ -+ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); -+ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; -+ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); -+ if ( textureData.rawTextureViewSource !== raw ) { -+ -+ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); -+ textureData.rawTextureViewSource = raw; -+ -+ } -+ resourceGPU = textureData.rawTextureView; -+ -+ } else if ( textureData.externalTexture !== undefined ) { - - resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); - -@@ -498,7 +511,7 @@ class WebGPUBindingUtils { - - const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); - -- if ( primarySamples > 1 ) { -+ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { - - texture.multisampled = true; - -@@ -536,7 +549,7 @@ class WebGPUBindingUtils { - - } else if ( type === FloatType ) { - -- if ( this.backend.hasFeature( 'float32-filterable' ) ) { -+ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { - - texture.sampleType = GPUTextureSampleType.Float; - -diff --git a/build/three.webgpu.js b/build/three.webgpu.js -index 941ec68ef0b8174a633054043edb94d9d3d58f5e..a308a892c59283b3b9a71477b3b02d64db40becd 100644 ---- a/build/three.webgpu.js -+++ b/build/three.webgpu.js -@@ -34258,6 +34258,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34407,7 +34411,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34571,7 +34584,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34581,7 +34603,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34598,6 +34620,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34630,9 +34663,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34810,6 +34968,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -44718,6 +44878,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44772,8 +44972,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44804,7 +45004,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45873,8 +46073,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45910,9 +46110,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -62438,6 +62638,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -76734,6 +76942,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76950,12 +77161,12 @@ class WebGPUTextureUtils { - - } - -- // Compute-only 3D storage has no attachment or render-pass mipmap consumer. -- // Requesting the unused attachment usage rejects the texture on wgpu-native. -- const computeOnlyStorage3D = dimension === GPUTextureDimension.ThreeD && texture.isStorageTexture === true && -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && - texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! computeOnlyStorage3D ) { -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -77028,6 +77239,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -77068,7 +77295,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -77093,8 +77320,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77117,64 +77344,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -- -- return backend.get( depthTexture ).texture; -- -- } -- -- // -+ let cache = this._depthBuffers.get( depthTexture ); - -- const depthTextureGPU = backend.get( depthTexture ).texture; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- let format, type; -- -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -- -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -- -- return depthTextureGPU; -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77329,6 +77551,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77402,6 +77761,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -82951,6 +83312,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82965,7 +83335,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84576,7 +84946,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84603,6 +84975,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86343,6 +86718,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js -index 57f69c17352b936a9eca118a9801805c95c401b6..95669d28b54250a3857dcb7d1b1b0e30dd19e0cb 100644 ---- a/build/three.webgpu.nodes.js -+++ b/build/three.webgpu.nodes.js -@@ -34214,6 +34214,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34363,7 +34367,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34527,7 +34540,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34537,7 +34559,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34554,6 +34576,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34586,9 +34619,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34766,6 +34924,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -44673,6 +44833,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ - - const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); - -+const _shadowUniforms = /*@__PURE__*/ new WeakMap(); -+ -+/** -+ * Returns the uniform `create` makes for this shadow and name, and makes it only once. -+ * -+ * A filter body runs once per node builder. A uniform that the body makes directly is a new node -+ * for each material, and a new render-group node gives each material its own copy of the shared -+ * render group: a buffer that each material compares and writes on each render call. One node per -+ * shadow lets every material that reads it share one render-group buffer. -+ * -+ * @private -+ * @param {LightShadow} shadow - The shadow that owns the uniform. -+ * @param {string} name - The uniform's name within that shadow. -+ * @param {Function} create - Makes the uniform the first time. -+ * @return {Node} The uniform node. -+ */ -+const getShadowUniform = ( shadow, name, create ) => { -+ -+ let uniforms = _shadowUniforms.get( shadow ); -+ -+ if ( uniforms === undefined ) { -+ -+ uniforms = new Map(); -+ _shadowUniforms.set( shadow, uniforms ); -+ -+ } -+ -+ let node = uniforms.get( name ); -+ -+ if ( node === undefined ) { -+ -+ node = create(); -+ uniforms.set( name, node ); -+ -+ } -+ -+ return node; -+ -+}; -+ - /** - * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map - * with a binary `[0,1]` result. -@@ -44727,8 +44927,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - - }; - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - const radiusScaled = radius.mul( texelSize.x ); -@@ -44759,7 +44959,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, - */ - const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { - -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = vec2( 1 ).div( mapSize ); - -@@ -45828,8 +46028,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) - */ - const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { - -- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); -- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); -+ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); -+ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); - - const texelSize = radius.div( mapSize.x ); - -@@ -45865,9 +46065,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo - const shadowPositionAbs = shadowPosition.abs().toConst(); - const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); - -- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); -- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); -- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); -+ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); -+ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); -+ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); - - const result = float( 1.0 ).toVar(); - -@@ -62393,6 +62593,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -76678,6 +76886,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -76894,12 +77105,12 @@ class WebGPUTextureUtils { - - } - -- // Compute-only 3D storage has no attachment or render-pass mipmap consumer. -- // Requesting the unused attachment usage rejects the texture on wgpu-native. -- const computeOnlyStorage3D = dimension === GPUTextureDimension.ThreeD && texture.isStorageTexture === true && -+ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for -+ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. -+ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && - texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; - -- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! computeOnlyStorage3D ) { -+ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { - - usage |= GPUTextureUsage.RENDER_ATTACHMENT; - -@@ -76972,6 +77183,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -77012,7 +77239,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -77037,8 +77264,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77061,64 +77288,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -- -- return backend.get( depthTexture ).texture; -- -- } -- -- // -+ let cache = this._depthBuffers.get( depthTexture ); - -- const depthTextureGPU = backend.get( depthTexture ).texture; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- let format, type; -- -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -- -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -- -- return depthTextureGPU; -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77273,6 +77495,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77346,6 +77705,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -82890,6 +83251,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82904,7 +83274,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84490,7 +84860,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84517,6 +84889,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86253,6 +86628,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/build/three.webgpu.js b/build/three.webgpu.js -index ef853fd6f7382a8c7ff66cda43471344aeb20770..a308a892c59283b3b9a71477b3b02d64db40becd 100644 ---- a/build/three.webgpu.js -+++ b/build/three.webgpu.js -@@ -34258,6 +34258,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34407,7 +34411,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34571,7 +34584,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34581,7 +34603,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34598,6 +34620,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34630,9 +34663,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34810,6 +34968,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -62478,6 +62638,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -76774,6 +76942,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -77068,6 +77239,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -77108,7 +77295,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -77133,8 +77320,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77157,64 +77344,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -+ let cache = this._depthBuffers.get( depthTexture ); - -- return backend.get( depthTexture ).texture; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- } -- -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -- -- let format, type; -- -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -- -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -- -- return depthTextureGPU; -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77369,6 +77551,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77442,6 +77761,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -82991,6 +83312,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -83005,7 +83335,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84616,7 +84946,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84643,6 +84975,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86383,6 +86718,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js -index e7956e565579aa6ec550ca001336d5a0d76c652a..95669d28b54250a3857dcb7d1b1b0e30dd19e0cb 100644 ---- a/build/three.webgpu.nodes.js -+++ b/build/three.webgpu.nodes.js -@@ -34214,6 +34214,10 @@ class Textures extends DataMap { - * @type {Set} - */ - this._htmlTextures = new Set(); -+ this._pendingUploads = new Map(); -+ this._uploadTail = Promise.resolve(); -+ this._uploadSlice = { started: null }; -+ this._uploadDisposed = false; - - } - -@@ -34363,7 +34367,16 @@ class Textures extends DataMap { - */ - updateTexture( texture, options = {} ) { - -+ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); -+ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; -+ return this._updateTexture( texture, options ); -+ -+ } -+ -+ _updateTexture( texture, options = {}, upload = null ) { -+ - const textureData = this.get( texture ); -+ let uploadPromise; - if ( textureData.initialized === true && textureData.version === texture.version ) return; - - const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34527,7 +34540,16 @@ class Textures extends DataMap { - - } - -- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); -+ if ( texture.source.dataReady === true ) { -+ -+ if ( upload ) { -+ -+ this._initializeTexture( texture, textureData ); -+ uploadPromise = upload( texture, options ); -+ -+ } else backend.updateTexture( texture, options ); -+ -+ } - - const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; - -@@ -34537,7 +34559,7 @@ class Textures extends DataMap { - - } - -- if ( texture.onUpdate ) texture.onUpdate( texture ); -+ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); - - } - -@@ -34554,6 +34576,17 @@ class Textures extends DataMap { - - } - -+ this._initializeTexture( texture, textureData ); -+ if ( upload ) return uploadPromise; -+ -+ // -+ -+ textureData.version = texture.version; -+ -+ } -+ -+ _initializeTexture( texture, textureData ) { -+ - // dispose handler - - if ( textureData.initialized !== true ) { -@@ -34586,9 +34619,134 @@ class Textures extends DataMap { - - } - -- // - -- textureData.version = texture.version; -+ } -+ -+ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. -+ updateTextureAsync( texture, options = {} ) { -+ -+ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); -+ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); -+ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { -+ -+ this._uploadLossSubscribed = true; -+ const lost = error => { -+ -+ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); -+ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); -+ -+ }; -+ this.backend.device.lost.then( lost, lost ); -+ -+ } -+ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); -+ const pending = this._pendingUploads.get( texture ); -+ if ( pending ) { -+ -+ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); -+ return this._waitForUpload( pending, options.signal ); -+ -+ } -+ const data = this.get( texture ); -+ if ( data.uploadError ) return Promise.reject( data.uploadError ); -+ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); -+ if ( data.initialized === true && data.version === texture.version ) { -+ -+ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); -+ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); -+ -+ } -+ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); -+ const controller = new AbortController(); -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const image = texture.image; -+ const size = { width: image?.width, height: image?.height, depth: image?.depth }; -+ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const device = this.backend.device; -+ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; -+ const checkQueued = () => { -+ -+ if ( controller.signal.aborted ) throw controller.signal.reason; -+ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); -+ -+ }; -+ let backendData, gpuTexture; -+ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ texture.addEventListener( 'dispose', onQueuedDispose ); -+ this._pendingUploads.set( texture, job ); -+ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); -+ const work = this._uploadTail.then( () => { -+ -+ checkQueued(); -+ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); -+ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); -+ backendData = this.backend.get( texture ); -+ gpuTexture = backendData.texture; -+ return result; -+ -+ } ); -+ job.promise = Promise.race( [ work, cancelled ] ).then( () => { -+ -+ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); -+ data.version = job.version; -+ delete data.uploadError; -+ if ( texture.onUpdate ) texture.onUpdate( texture ); -+ -+ } ).catch( error => { -+ -+ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; -+ throw error; -+ -+ } ).finally( () => { -+ -+ texture.removeEventListener( 'dispose', onQueuedDispose ); -+ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); -+ -+ } ); -+ // Caller cancellation is prompt; the backend lane stays owned until validation drains. -+ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); -+ return this._waitForUpload( job, options.signal ); -+ -+ } -+ -+ _waitForUpload( job, signal ) { -+ -+ job.users ++; -+ return new Promise( ( resolve, reject ) => { -+ -+ let settled = false; -+ const finish = ( error ) => { -+ -+ if ( settled ) return; -+ settled = true; -+ signal?.removeEventListener( 'abort', abort ); -+ job.users --; -+ if ( error ) reject( error ); else resolve(); -+ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); -+ -+ }; -+ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); -+ signal?.addEventListener( 'abort', abort, { once: true } ); -+ job.promise.then( () => finish(), finish ); -+ -+ } ); -+ -+ } -+ -+ dispose() { -+ -+ this._uploadDisposed = true; -+ for ( const job of this._pendingUploads.values() ) { -+ -+ job.controller.abort( new Error( 'Texture manager disposed.' ) ); -+ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); -+ -+ } -+ this._pendingUploads.clear(); -+ super.dispose(); - - } - -@@ -34766,6 +34924,8 @@ class Textures extends DataMap { - */ - _destroyTexture( texture ) { - -+ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); -+ - if ( this.has( texture ) === true ) { - - const textureData = this.get( texture ); -@@ -62433,6 +62593,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -@@ -76718,6 +76886,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -77012,6 +77183,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -77052,7 +77239,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -77077,8 +77264,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -77101,64 +77288,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -+ let cache = this._depthBuffers.get( depthTexture ); - -- return backend.get( depthTexture ).texture; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- } -- -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -- -- let format, type; -- -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -- -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -- -- return depthTextureGPU; -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- } -+ if ( buffer === undefined ) { - -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -77313,6 +77495,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -77386,6 +77705,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } -@@ -82930,6 +83251,15 @@ class WebGPUPipelineUtils { - - _renderPipelineDescriptor.reset(); - -+ // Close the scope before yielding so unrelated render commands cannot enter it. -+ // Handle rejection immediately even if the pipeline promise is still pending. -+ const errorScopePromise = device.popErrorScope().catch( ( err ) => { -+ -+ asyncError ??= err; -+ return null; -+ -+ } ); -+ - if ( pipelinePromise !== null ) { - - try { -@@ -82944,7 +83274,7 @@ class WebGPUPipelineUtils { - - } - -- const errorScope = await device.popErrorScope(); -+ const errorScope = await errorScopePromise; - - if ( errorScope !== null || asyncError !== null ) { - -@@ -84530,7 +84860,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84557,6 +84889,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -86293,6 +86628,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js -index 730ce0ccd0f56d70c4a7abe7c514c5077d544d1e..b1293bfbe2ccf0583711813c625ddd5c5ba78620 100644 ---- a/src/renderers/common/Renderer.js -+++ b/src/renderers/common/Renderer.js -@@ -2942,6 +2942,14 @@ class Renderer { - - } - -+ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. -+ async prepareTextureAsync( texture, options = {} ) { -+ -+ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); -+ await this._textures.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Initializes the given render target. - * -diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js -index 927e3a1ad5a4665d77238ffbab4c57a603b86828..5636b40e6708a490e926e5f590675834d23bf326 100644 ---- a/src/renderers/webgpu/WebGPUBackend.js -+++ b/src/renderers/webgpu/WebGPUBackend.js -@@ -466,7 +466,9 @@ class WebGPUBackend extends Backend { - - let descriptor = canvasData.descriptor; - -- if ( descriptor === undefined || canvasData.samples !== samples ) { -+ if ( descriptor === undefined || canvasData.samples !== samples || -+ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || -+ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { - - descriptor = new GPURenderPassDescriptor(); - descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -493,6 +495,9 @@ class WebGPUBackend extends Backend { - - canvasData.descriptor = descriptor; - canvasData.samples = samples; -+ canvasData.depth = renderer.depth; -+ canvasData.stencil = renderer.stencil; -+ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; - - } - -@@ -2233,6 +2238,12 @@ class WebGPUBackend extends Backend { - - } - -+ updateTextureAsync( texture, options ) { -+ -+ return this.textureUtils.updateTextureAsync( texture, options ); -+ -+ } -+ - /** - * Generates mipmaps for the given texture. - * -diff --git a/src/renderers/webgpu/utils/WebGPUTextureUtils.js b/src/renderers/webgpu/utils/WebGPUTextureUtils.js -index c46f6ab0e06d93f124c9fc62ece8859529461d5e..57c1eb62f1e0de063f22e08ec879e31d2a3240b6 100644 ---- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js -+++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js -@@ -41,6 +41,7 @@ import { - Compatibility - } from '../../../constants.js'; - import { CubeTexture } from '../../../textures/CubeTexture.js'; -+import { DepthTexture } from '../../../textures/DepthTexture.js'; - import { Texture } from '../../../textures/Texture.js'; - import { warn, error } from '../../../utils.js'; - -@@ -143,6 +144,9 @@ class WebGPUTextureUtils { - */ - this._samplerCache = new Map(); - -+ // Own default-canvas depth variants until resize, explicit disposal or teardown. -+ this._depthBuffers = new Map(); -+ - } - - /** -@@ -437,6 +441,22 @@ class WebGPUTextureUtils { - destroyTexture( texture, isDefaultTexture = false ) { - - const backend = this.backend; -+ -+ const depthBuffers = this._depthBuffers.get( texture ); -+ -+ if ( depthBuffers !== undefined ) { -+ -+ this._depthBuffers.delete( texture ); -+ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); -+ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); -+ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); -+ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; -+ backend.delete( texture ); -+ return; -+ -+ } -+ - const textureData = backend.get( texture ); - - if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -477,7 +497,7 @@ class WebGPUTextureUtils { - const colorTexture = canvasTarget.colorTexture; - const colorTextureData = backend.get( colorTexture ); - -- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { -+ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - - return colorTextureData.texture; - -@@ -502,8 +522,8 @@ class WebGPUTextureUtils { - - // - -- colorTexture.source.width = width; -- colorTexture.source.height = height; -+ colorTexture.image.width = width; -+ colorTexture.image.height = height; - colorTexture.samples = samples; - - colorTextureData.texture = colorBuffer; -@@ -526,64 +546,59 @@ class WebGPUTextureUtils { - const canvasTarget = backend.renderer.getCanvasTarget(); - const { width, height } = backend.getDrawingBufferSize(); - const samples = backend.renderer.currentSamples; -- -+ const sampleCount = backend.utils.getSampleCount( samples ); - const depthTexture = canvasTarget.depthTexture; -+ const format = stencil ? DepthStencilFormat : DepthFormat; -+ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); - -- if ( depthTexture.width === width && -- depthTexture.height === height && -- depthTexture.samples === samples && -- depthTexture.depth === depth && -- depthTexture.stencil === stencil ) { -+ let cache = this._depthBuffers.get( depthTexture ); - -- return backend.get( depthTexture ).texture; -+ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { - -- } -- -- // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; -- -- let format, type; -- -- if ( stencil ) { -+ this.destroyTexture( depthTexture ); -+ cache = undefined; - -- format = DepthStencilFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; -+ } - -- } else if ( depth ) { -+ if ( cache === undefined ) { - -- format = DepthFormat; -- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; -+ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; -+ this._depthBuffers.set( depthTexture, cache ); -+ canvasTarget.addEventListener( 'resize', cache.onDispose ); -+ canvasTarget.addEventListener( 'dispose', cache.onDispose ); -+ depthTexture.addEventListener( 'dispose', cache.onDispose ); - - } - -- if ( depthTextureGPU !== undefined ) { -+ // Output and direct overlay passes alternate effective samples 4 and 1. -+ // Retain at most those two buffers for this canvas size and depth format. -+ let buffer = cache.textures.get( sampleCount ); - -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { -+ if ( buffer === undefined ) { - -- return depthTextureGPU; -- -- } -- -- this.destroyTexture( depthTexture ); -+ buffer = new DepthTexture( width, height, type ); -+ buffer.name = 'depthBuffer'; -+ buffer.format = format; -+ buffer.samples = sampleCount; -+ this.createTexture( buffer, { width, height } ); -+ cache.textures.set( sampleCount, buffer ); - - } - -- // recreate -- -+ // Preserve the canonical texture identity and its currently selected GPU data. - depthTexture.name = 'depthBuffer'; - depthTexture.format = format; - depthTexture.type = type; - depthTexture.image.width = width; - depthTexture.image.height = height; - depthTexture.samples = samples; -+ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); - -- this.createTexture( depthTexture, { width, height } ); -- -- return backend.get( depthTexture ).texture; -+ return backend.get( buffer ).texture; - - } - -+ - /** - * Uploads the updated texture data to the GPU. - * -@@ -738,6 +753,143 @@ class WebGPUTextureUtils { - - } - -+ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. -+ async updateTextureAsync( texture, options = {} ) { -+ -+ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; -+ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); -+ const backend = this.backend; -+ const device = backend.device; -+ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ const textureData = backend.get( texture ); -+ const gpuTexture = textureData.texture; -+ const descriptor = textureData.textureDescriptorGPU; -+ const block = descriptor && this._getBlockData( descriptor.format ); -+ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); -+ const version = texture.version; -+ const source = texture.source; -+ const mipmaps = texture.mipmaps; -+ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); -+ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; -+ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); -+ const check = () => { -+ -+ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); -+ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); -+ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); -+ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); -+ -+ }; -+ function* tiles() { -+ -+ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { -+ -+ const mip = captured[ mipLevel ]; -+ const columns = Math.ceil( mip.width / block.width ); -+ const rows = Math.ceil( mip.height / block.height ); -+ const stride = columns * block.byteLength; -+ const imageBytes = stride * rows; -+ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); -+ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); -+ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; -+ for ( const layer of layers ) { -+ -+ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); -+ for ( let y = 0; y < rows; y += tileRows ) { -+ -+ for ( let x = 0; x < columns; x += tileColumns ) { -+ -+ const width = Math.min( tileColumns, columns - x ); -+ const height = Math.min( tileRows, rows - y ); -+ const offset = layer * imageBytes + y * stride + x * block.byteLength; -+ const length = ( height - 1 ) * stride + width * block.byteLength; -+ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ -+ } -+ const slice = options.uploadSlice || { started: null }; -+ const yieldHost = async () => { -+ -+ await new Promise( resolve => setTimeout( resolve, 0 ) ); -+ slice.started = performance.now(); -+ check(); -+ -+ }; -+ const iterator = tiles(); -+ const validations = []; -+ const failures = []; -+ let next = iterator.next(); -+ let uploadFailed = false; -+ let uploadError; -+ try { -+ -+ while ( ! next.done && failures.length === 0 ) { -+ -+ check(); -+ if ( slice.started === null ) slice.started = performance.now(); -+ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); -+ if ( failures.length > 0 ) break; -+ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; -+ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } -+ try { -+ -+ do { -+ -+ check(); -+ const tile = next.value; -+ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); -+ next = iterator.next(); -+ -+ } while ( ! next.done && performance.now() - slice.started < budgetMs ); -+ -+ } finally { -+ -+ // Pop synchronously: a loading-screen render must not enter our error scopes. -+ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { -+ -+ for ( const result of results ) { -+ -+ if ( result.status === 'rejected' ) failures.push( result.reason ); -+ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); -+ -+ } -+ -+ } ) ); -+ -+ } -+ // Bound validation latency without changing the host slice or byte budgets. -+ if ( validations.length === 4 ) await validations.shift(); -+ check(); -+ if ( ! next.done && failures.length === 0 ) await yieldHost(); -+ -+ } -+ -+ } catch ( error ) { -+ -+ uploadFailed = true; -+ uploadError = error; -+ -+ } finally { -+ -+ // Observe every issued scope, including after cancellation or a write failure. -+ await Promise.all( validations ); -+ -+ } -+ if ( uploadFailed ) throw uploadError; -+ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); -+ check(); -+ textureData.version = version; -+ if ( updates.length > 0 ) texture.clearLayerUpdates(); -+ -+ } -+ - /** - * Returns texture data as a typed array. - * -@@ -811,6 +963,8 @@ class WebGPUTextureUtils { - */ - dispose() { - -+ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); -+ - this._samplerCache.clear(); - - } +{ + "version": 2, + "generator": "scripts/generate-three-patch.mjs", + "pin": { + "commit": "2431a09f46f34c560bc8e44b33be0e567723d5b9", + "version": "0.185.1" + }, + "acceptedLegacyCompatPairs": [ + { + "path": "build/three.webgpu.js", + "oldHash": "148869ee7f18c62570ac1eafb7df5775e5a89581", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "5c5ef3c06885c724c1c2288295e7e68483ee1f6e", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "7aa62adb9e78ba8f0a67709d3141bf93c67419cc", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "bdffb9b4069f7beafa85913b155e92d3e83f12d0", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "efd0e16719cc7bbef1fbc18c241948e25eb85881", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "1245f0da9bb6a0f731b8c75a64490d7c91238700", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "1b3a6e96291af0077f7f247040e657cf4aa0cb33", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "3d47c1151d563f1e0ae2bb4b92edd057fe0df2b3", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "81c24b2e05e5047e1a02410a2056949d39b1a05c", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "b8a7b2fd0a2e95911e7e12fd5674592107f012de", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "src/nodes/accessors/Instance.js", + "oldHash": "ed00e75ba4fd7b49343cfada26eacdef8940a4f4", + "newHash": "18e2b7943393fd07b7989b1e3a5231a61ca24187" + }, + { + "path": "src/nodes/accessors/Skinning.js", + "oldHash": "aaeea633a8ece291be5faf868e312867ddad01b6", + "newHash": "76303e765bc29853cfd7f139dd3ec76545b70798" + }, + { + "path": "src/renderers/common/Renderer.js", + "oldHash": "89b33efb80ca1dcb9db931071dfda62ffdba3526", + "newHash": "b1293bfbe2ccf0583711813c625ddd5c5ba78620" + }, + { + "path": "src/renderers/webgpu/WebGPUBackend.js", + "oldHash": "69b8b67fdeec5be24de34d8382beda9020642f77", + "newHash": "5636b40e6708a490e926e5f590675834d23bf326" + }, + { + "path": "src/renderers/webgpu/utils/WebGPUAttributeUtils.js", + "oldHash": "83bbe189915c8450c4e14108e6a3f4ed98c4b016", + "newHash": "16bd72d0c0f1d693481efc98140a0f1b73905149" + }, + { + "path": "src/renderers/webgpu/utils/WebGPUTextureUtils.js", + "oldHash": "ff8ee2dbace2816105aa7152891c04f2333f33e8", + "newHash": "57c1eb62f1e0de063f22e08ec879e31d2a3240b6" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "9166d6f2dd1ac1505fa27b625f1976dff6b8ebc1", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "9e7c7f74be1191b761711bd70ee59cc033e586cb", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "src/nodes/accessors/TextureNode.js", + "oldHash": "f886ed21397ab39d6fdbc58e5ac57729f1871f8a", + "newHash": "1a138b1a055d0867e7686531930d4d557e1a20df" + }, + { + "path": "src/nodes/accessors/TextureSizeNode.js", + "oldHash": "d4668281657ef8b1acad761e0d86a0716909eee4", + "newHash": "43eccaa01bcb12ea6e02c94232d5ee76bcac0d01" + }, + { + "path": "src/nodes/display/PassNode.js", + "oldHash": "cc827a55cb82a6325296dcc5c55a4246f4f70aed", + "newHash": "3cdf66cfe0760066bc062bf7ce3f3a8000eb2b0d" + }, + { + "path": "src/renderers/common/Renderer.js", + "oldHash": "51590d5a23a93365e5cd9fb0360678750270d5bf", + "newHash": "b1293bfbe2ccf0583711813c625ddd5c5ba78620" + }, + { + "path": "src/renderers/webgpu/nodes/WGSLNodeBuilder.js", + "oldHash": "2acc832a628f43adac49614dad8e593ef058c398", + "newHash": "1cbc9b3e02c9832d69a8aadb3a6003392b443c8d" + }, + { + "path": "src/renderers/webgpu/utils/WebGPUBindingUtils.js", + "oldHash": "a5da7ca09f746c0b2859ef173df663ccdc851ebc", + "newHash": "c479d0a88794b01bca531e7cbfb780cf93fc4721" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "941ec68ef0b8174a633054043edb94d9d3d58f5e", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "57f69c17352b936a9eca118a9801805c95c401b6", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "ef853fd6f7382a8c7ff66cda43471344aeb20770", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "e7956e565579aa6ec550ca001336d5a0d76c652a", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "src/renderers/common/Renderer.js", + "oldHash": "730ce0ccd0f56d70c4a7abe7c514c5077d544d1e", + "newHash": "b1293bfbe2ccf0583711813c625ddd5c5ba78620" + }, + { + "path": "src/renderers/webgpu/WebGPUBackend.js", + "oldHash": "927e3a1ad5a4665d77238ffbab4c57a603b86828", + "newHash": "5636b40e6708a490e926e5f590675834d23bf326" + }, + { + "path": "src/renderers/webgpu/utils/WebGPUTextureUtils.js", + "oldHash": "c46f6ab0e06d93f124c9fc62ece8859529461d5e", + "newHash": "57c1eb62f1e0de063f22e08ec879e31d2a3240b6" + } + ], + "acceptedLegacyFinalHashes": { + "build/three.cjs": "b0f917241f1f2cd25baa21dec20edda32176ef6a", + "build/three.core.js": "868a634b4294be5f32664912a50813d55d6dd6e7", + "build/three.webgpu.js": "a308a892c59283b3b9a71477b3b02d64db40becd", + "build/three.webgpu.nodes.js": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb", + "src/materials/nodes/manager/NodeMaterialObserver.js": "7e2cb9926878b43aa2b8b3a146e3427711be7210", + "src/nodes/accessors/Batch.js": "2cf4bb6039853d5c922e89041419250b243694ef", + "src/nodes/accessors/Instance.js": "18e2b7943393fd07b7989b1e3a5231a61ca24187", + "src/nodes/accessors/Skinning.js": "76303e765bc29853cfd7f139dd3ec76545b70798", + "src/nodes/accessors/TextureNode.js": "1a138b1a055d0867e7686531930d4d557e1a20df", + "src/nodes/accessors/TextureSizeNode.js": "43eccaa01bcb12ea6e02c94232d5ee76bcac0d01", + "src/nodes/accessors/VelocityNode.js": "c48002abbce20ee8b29908cf314d7b03afe0db03", + "src/nodes/core/NodeBuilder.js": "1990da8fe8f1f29a7b53030756dfd966d8de3cb6", + "src/nodes/display/PassNode.js": "3cdf66cfe0760066bc062bf7ce3f3a8000eb2b0d", + "src/nodes/lighting/PointShadowNode.js": "f9019a72890a30fded9f7ce76982722c2f7d6a3f", + "src/nodes/lighting/ShadowFilterNode.js": "fe8b50c8f76d0b6ce3d2fa1452751b2469a821e4", + "src/objects/BatchedMesh.js": "35cc0ffe83a69ea9fa5fca6dec2d820ca193f88d", + "src/renderers/common/Bindings.js": "a3cec4ed844ab88f870b46714c56c941e0cc7a8e", + "src/renderers/common/Geometries.js": "979da2ffc4d0dcda6fdd72747a7e7125e3f8a3a3", + "src/renderers/common/RenderContexts.js": "7319e25e280452b69c6f0c4c2f8c93c955ff9be3", + "src/renderers/common/RenderObject.js": "a3dcba2f1fb0df19f3ddb3039a97ae408e2938b3", + "src/renderers/common/RenderObjects.js": "72569d0339314d26eb7a35ff2a8462a803c852a6", + "src/renderers/common/Renderer.js": "b1293bfbe2ccf0583711813c625ddd5c5ba78620", + "src/renderers/common/Textures.js": "5b305173fa4a2814ec74563d2ae5cdc70e7505e1", + "src/renderers/common/nodes/NodeManager.js": "033128c396c2cad5c5d1d362f2d261e9adbd6bdc", + "src/renderers/webgpu/WebGPUBackend.js": "5636b40e6708a490e926e5f590675834d23bf326", + "src/renderers/webgpu/nodes/WGSLNodeBuilder.js": "1cbc9b3e02c9832d69a8aadb3a6003392b443c8d", + "src/renderers/webgpu/utils/WebGPUAttributeUtils.js": "16bd72d0c0f1d693481efc98140a0f1b73905149", + "src/renderers/webgpu/utils/WebGPUBindingUtils.js": "c479d0a88794b01bca531e7cbfb780cf93fc4721", + "src/renderers/webgpu/utils/WebGPUPipelineUtils.js": "3480fea1e86f1f237cb6ee2ea87b5826d4fc404d", + "src/renderers/webgpu/utils/WebGPUTextureUtils.js": "57c1eb62f1e0de063f22e08ec879e31d2a3240b6" + }, + "generatedFinalHashes": { + "build/three.cjs": "b0f917241f1f2cd25baa21dec20edda32176ef6a", + "build/three.core.js": "868a634b4294be5f32664912a50813d55d6dd6e7", + "build/three.webgpu.js": "e594543d1e604de1ac9f45c6260e213ad0e2d7b5", + "build/three.webgpu.nodes.js": "6581a8b0e69dea0ce5bfdc644f75a40f6aaac963", + "src/materials/nodes/manager/NodeMaterialObserver.js": "7e2cb9926878b43aa2b8b3a146e3427711be7210", + "src/nodes/accessors/Batch.js": "2cf4bb6039853d5c922e89041419250b243694ef", + "src/nodes/accessors/Instance.js": "18e2b7943393fd07b7989b1e3a5231a61ca24187", + "src/nodes/accessors/Skinning.js": "76303e765bc29853cfd7f139dd3ec76545b70798", + "src/nodes/accessors/TextureNode.js": "1a138b1a055d0867e7686531930d4d557e1a20df", + "src/nodes/accessors/TextureSizeNode.js": "43eccaa01bcb12ea6e02c94232d5ee76bcac0d01", + "src/nodes/accessors/VelocityNode.js": "c48002abbce20ee8b29908cf314d7b03afe0db03", + "src/nodes/core/NodeBuilder.js": "1990da8fe8f1f29a7b53030756dfd966d8de3cb6", + "src/nodes/display/PassNode.js": "3cdf66cfe0760066bc062bf7ce3f3a8000eb2b0d", + "src/nodes/lighting/PointShadowNode.js": "f9019a72890a30fded9f7ce76982722c2f7d6a3f", + "src/nodes/lighting/ShadowFilterNode.js": "fe8b50c8f76d0b6ce3d2fa1452751b2469a821e4", + "src/objects/BatchedMesh.js": "35cc0ffe83a69ea9fa5fca6dec2d820ca193f88d", + "src/renderers/common/Bindings.js": "a3cec4ed844ab88f870b46714c56c941e0cc7a8e", + "src/renderers/common/Geometries.js": "979da2ffc4d0dcda6fdd72747a7e7125e3f8a3a3", + "src/renderers/common/RenderContexts.js": "7319e25e280452b69c6f0c4c2f8c93c955ff9be3", + "src/renderers/common/RenderObject.js": "a3dcba2f1fb0df19f3ddb3039a97ae408e2938b3", + "src/renderers/common/RenderObjects.js": "72569d0339314d26eb7a35ff2a8462a803c852a6", + "src/renderers/common/Renderer.js": "b1293bfbe2ccf0583711813c625ddd5c5ba78620", + "src/renderers/common/Textures.js": "5b305173fa4a2814ec74563d2ae5cdc70e7505e1", + "src/renderers/common/nodes/NodeManager.js": "033128c396c2cad5c5d1d362f2d261e9adbd6bdc", + "src/renderers/webgpu/WebGPUBackend.js": "5636b40e6708a490e926e5f590675834d23bf326", + "src/renderers/webgpu/nodes/WGSLNodeBuilder.js": "1cbc9b3e02c9832d69a8aadb3a6003392b443c8d", + "src/renderers/webgpu/utils/WebGPUAttributeUtils.js": "16bd72d0c0f1d693481efc98140a0f1b73905149", + "src/renderers/webgpu/utils/WebGPUBindingUtils.js": "c479d0a88794b01bca531e7cbfb780cf93fc4721", + "src/renderers/webgpu/utils/WebGPUPipelineUtils.js": "3480fea1e86f1f237cb6ee2ea87b5826d4fc404d", + "src/renderers/webgpu/utils/WebGPUTextureUtils.js": "57c1eb62f1e0de063f22e08ec879e31d2a3240b6" + }, + "bundleBrotliBase64": "n19kUCkZwEH1AACw5u6hUUhenD18BxTs/Bt2hklpVwpUDwAAffYeAFVVVVX17QOMRWCb7Mzyt8+38M/zz7egiGILiii2oIhiC4ootqCIYguKKLagiGILiii2oIhiC4ootqCIYguKKLagiGILiii2oIhiC4ootqCIYtujXoVVPTrCsKchrKxH4Z4oDR2Pqdewe8t96tdxnlCfTjLsO4Wwsp6G+6I0dDyjfsPuLw9oUGfxbA3oHIYD5yKsrOfBA1EaOp6vQcMeLA9pWBfwQg3pIoZDFyOsrJfAQ1EaOl6qYcMeLo9oVJfxco3oCoYjVyKsrFfBI1EaOl6tUcMeLY9pXNfwWo3pOoZj1yOsrDfAY1EaOt6occMeL09oUjfxZk3oFoYTtyKsrLfBE1EaOt6uScOeLE9pWnfwTk3pLoZTdyOsrPfAU1EaOt6racOeLs9oVvfxfs3oAYYzDyKsrA/BM1EaOj6sWcOeLc9pXo/wUc3pMYZzjyOsrE/Ac1EaOj6pecOeLy9oUU/xaS3oGYYLzyKsrM/BC1EaOj6vRcNeLC9pWS/wRS3pJYZLLyOsrK/AS1EaOr6qZcNeLq9oVa/xda3oDYYrbyKsrG/BK1EaOr6tVcNeLa9pXe/wXa3pPYZr7yOsrB/Aa1EaOn6odcNeL29oUx/xY23oE4YbnyKsrJ/BG1EaOn6uTcPeLG9pW1/wS23pK4ZbXyOsrN/AW1EaOn6rbcPeLu9oV9/xe+3oB4Y7PyKsrD/BO1EaOv6sXcPeLe9pX7/wV+3pN4Z7vyOsrH/Ae1EaOv6pfcPeLx/oUH/xbx3oH4YH/yKsrP/BB1EaOv6vQ8M+LO8gIABws0XpCBg0ZNiIUWPGTZg0ZdqMWXPmLVi0ZNmKVWvWbdi0ZduOXXv2HTh05NiJU2fOXbh05dqNW3fuPXj05NmLV2/effj05duPX3/89c9/B4BvP52yHOfBo8hP2p005HULw3RsdVuqzy9yufKqeMQmiu0hL6luS1X6he3s4qssRLuligw50i/lgAJJS1XXwcOUytS24ixcBBVCevFdfN/U7HRNOzJVk2R3b3afUjXbCkR+k3xDgXwAZFsbAAHgGGd4DT4MPtF6X5uLBdHr+Nw/LxF7AVAgPf7T/62v31O+2ZO1mvaBGVbZWpR2zZkM4nvyEUWEZzRqgy3R7ltuS/U2khvIn//co9kSJVGKPN/aKNUXxSIamy44fnHLDadOd729bpmIhwzmcUVb6EAaGC3olz/Lvn6fLw1bFuJChEvJMeM6a1HP0DWipVI36+XKgIcs1WdyYrNJkB6fzdp6fcEbTjXs70Q861krQ8NZW6i5pWq1GLqDKw5xe+mGaAAEQIH0mKTbtAY9TP0pKimmvv2cyr4CZblmpPpHQo1dgQaAqpvKViLgGf0oFH0hH4+RELTNVK8Z4koz1OcNP8ZQBjp10+vd9e4QdgwLFvnDaKmPgeIQL5tWr2/vJTRTjKgzXMIbsydviDJqjKpEN6du2cvWkC4gYA3i0uNzWc5eb+2aiZ4YJ6Kv3gjUMoFKOMfaZqq2tQXfdE3mIYL+w/PBpS20hbQACADCxZHOpnX+RBF278/V12+uDvMcNFGvYWVLadmSEHGP3b3IScACmK8/ndYLKi/11xVYRZx+t2pwiRxy2VFtS9k5EKXZ/rugNAkGoSAg1pXqtqsPZ7ebTrykxlzKTqIwfx64e2CVETQD/PNtVl+/b2YPKVuKXB37y54LSLNFH4QKTvYitZatZ6OOkDx6AuMUcsqRBm1ZyNHx8OQN0uPic1vzv9O5tMiw9eYY3VJLIsp/eQUciW5Bj86Rni2B3jpWGQABED0W5tbrm2Jbh1DOTGZT8GAVcAhgXqlempCVVqE8V+Ifam6pEgnsNkXmW0jZFLC2meqFKAPD4vGvMPCd+B+KLoy+Mq3mUvbG15iUU9gUaDzwk/t9m6+nq27hJ5ufUzd0rCnbAvRqJGLLjI5ywQsQs2xa1UYsPSMTB0qgW+kND6zEbHNK0+owd9iIoJLYwwD96i8NgADInhO6Zbl8fTNNcwpt7zZnLuzFhMqEiRFO0ci2qjbo2e4E2gviHbzZvKjp5lK1zcOQc1But1TBIISynLInJFMYeMuYiqS/ZkPa32atqeyP/6nwD6RypbT2IGKPUsDTt9l//c47YyRj9uCr2QvKYxcJWq52L1YbE0+T0TO25XGAVDVswJEOlMtSCeLScy7V1zvZ2O3+Wbpe9/plceASXSTACpBZ1E2v12+fx18wg4IzEisbe2oABECCnPRc5tbrm9MN+GIWXNIzrJqZFLHASQ8T7JzWe6FN4RTQCNq23LQ6/oPY2Zen2xA4xP7QHERwLdIBO+D+Knq6iNz1q22f6k3HQzPfgy8ds7foZar5+u7dJSE1swbdXuR1lypeRrK7XCBhRWJILBAAVMkYMJEjjTVKvydIj89mTl/vDq14/MydSsBBZ7UCZOpGyiI08l3p37PmSVnWGwnqEI4ACIAAOVhnqtce0CTdiBDqFJhbqrsYNk5UtxAqlttS/coGd/cw9lBIBEtwNkNTp7mtVC/EGQxNwdJEp/IPDsQX3m3YKWkWJdDuOdfUsmrXUbwHwacqZ8fEHysJH2Ixi16H51R0jSvMrUF6Ynxu2nRaE5aSAo1VAWBDSX73fTWFaBjW2nSmLMqScFAy9vF0cvmcAtKEsZmAo8oAd01rjeSUX+AFwb9rS/Vl2e1zB4ZjzVAFuxZNKy6IqpvK4oCkWdYXzjZ94S70hTnU3FK9Ugi2xNBg0cQcvLrN++/Kt8NXJJQEmR0VdC3VXt/N1RSEIqyU9op1pXQs1tZO0MKwuM5wCeISViAuW3M6McwANcH5CfN5LKYpkZ4sVZFNKTZzQTBJkwVAAATAwDxTXZNDsRPAw4r9fao7l56gz+lOsC1HlbLn3plZIDzNunuNvCogSZ2QSzQ50gNsbaEGErRXdVN28RdoUBd8FVWQ1z6Ii4uFZIRTNZ1WOqV+uaKeKQri6d2Fn4woLEdZAENZ5d992JaU3FzkDMyz1cEdcAJ4qNtSXTMLJttBXTFOWBAWAAWYGQCLbl5vL2DiVjrxqxTLNLPMjP5+ke483h9RIDkeeVVgflbNmrIrI/Y68/U8OYtFgaQKFeEvULqp7An5lBbASciSJRNmBbPWVPbGF5JT/TpA9cCSKNkCnWrqtN4WFT09PPa1p+QnkKnfZ9DkxT6AElOSoioD3qdmGFtbgFilW9fmEO2Y2bgjIh/MVTeVRSLmSeZIK1FF/JmpmLp6ACSiJ2/CxMgs2TmtN1bJuZiIFOZr+/uR7sYvkgbyk8q7zbalSuUOd8+2fwE7pWkzGYqQ2qbsCUm3EXCC5BUShfnWmluqoz8GQ3v48S7okmCTxzF2QQ4GW03T15fpVADZ+65rZM5nl85mHKGUKYiSz+4IgABZ2GaLbCS8LSHiFxVzW6kaJxgMVdmpDw5k7z9rFwABEABLefX6LlQnWfMgVcz0BGHMqeyrdpwC5+ZmBHNGDDK3VL8OF4I6ZwWibaZ6ITrZcHmpPMIWHvz0L9NXp3tvX0F2FozsD6wwoTagVvCypb6+2xPSWeT0eEHvnCQ3Tm86rQyLo7Gi1WgFNp2q0toC5FzNXu+2J0g4/ZEbOFGbddK1LEPr8N/Nab2tIg37cSJyw38kGgqAACiQnmO6nNZQI6kCL5uIkFrMXvyuSqv9z6lsVNthBnzvD41FzojO5kap3mYlHK8wWjEV4XOvbFrfVpwdh1AvxR+ltXj1C1Gn+5/BdoiUucezsogFSWW2m5na64uf5BkEeu0l9bZ7OKdynS1lQjUSitQQVBztlMGWID0+59Ls9a4OAyLtiTJLkqv1d0LnwXXcsGJIZdVtqRqqdaadHxFBQRzYHpoBEADZElN3LKevL7+UKUcq7sxC6htUkGRmpW1Vu4hnEqTjMNUXRc05UzauK4TwNpc4MaiRM4NobqleiE4cFpspvD7jcSAIvrj9LJ1TtvEzlvD3qb/SkO/9afb1O0nuzvg9WaRUk9207m2NLbI02HMrJKIRYPxQg7iIuWkQlx6fq7l8vcfNkD3AV4doJ3dRLS2wxmkPQbZ5vfLX8fRJRQhiUPM2AAIgYUB6rsl0Wtkixsf8I+YtRJwjD/VhmKvANlMdcke1RNpVd0PU1maqXnf4PFNgVkrdnWGh9dnz81K9oQt9pb8PjVTnVLZyYkYW6re/PvDwVwXGe7+W7+k63U0LQxZPsetq0nUhMio/CCmDGt/7BlmDnPW4vGazqs4rG9EuE15EOsNOOAEG9ogXRf1F/25j6/okqMYPb3iqQFIa0DCRVGwNzSvVNR9Yxwo16U0tN5WNa5kztkLxSLbmqmBba5QpuzJiaOrpF8IpuyrCn6qbshOyHdvfOfUE85CgIIHf8haT+qF4u0lvJyTBnnSJbapNK1t4lz59aX00bysEF8ln4YQfyLC0ufAZmGcrA0MBdWHzCYjLxUxoOFbWfJdXtAVAAGDEjvQsrXwewlv/+7LVf/2mZzlzGy02Kcgi6mQDqOMecXwQh91TfaZtaTezJbDkBnEq/v+/bf7X716RlYR4zXlJgFLOCy5G66p0MNjxqmFQS4cjJQdse6Nlqlm121GCV+fZubtlsvN96gzRqvzUJ5qwOicC/ATxrfTzdO110m0335wUfkrSd54qFh4C4ZrkAc0A3sw0X18/e1za43zRNgtzyogYSHKU3sgKrjirAgpl5vjz/Vn///W7qmaqbObIooPdOTlSPzaBkKEueWxMMunguURYstFESN5acsC7InDIkR6ioKUSeAnS43PZyur1bfookyiI8tE9sUs4Td81nuPytl27bcYkEioUDZKMssC9oMfvM8dyVa8vv7WMJS3ZZE8KqM5wTYAtaTmrE9MAKUDKgum536v69UuSjkXLIWWtAahY2FscZ8AvvuWvVe1DPLO57H40MD3diWnOAm7TScrixicXHkoBWl0lO+FHFI6lsuPqgYypUA/Aya1Jbo4LsgxQ07Q+HSbJG+a//l7r69f7ptIJNJi20s2+0sxmn01Hk+6bihowwTuOTX0dQqqlX1zI5CZIj881XVWtGHti32WGF3UkQmQjQPxUN5Vd/IJAFgWEBbECF8/lg/sVARAAf/SWWl1BElQ7aRy1xrqbg9caa88WwnM9/P9nznRmVm2rDDa6AGI2AJARAwjaaLnn/s9vsoplQG2hQGmKINVNsh1IcToo7XpS0nh7umucOW/s7ZiAeiI4xp72uNfd0xzX///e/09LyYOEK5WM9Y2OWhE/In5UeRnjGy0Cy5V03723pO9U+vEjhhQRgIYAgg0A5NAgM3Otudbex97333//R+CHATIQAJkgk5kFMplVzKys6jBTrkrW9BrlZF1PvcZ574MaHyAbVMkxq2RKxvWknvpdNdpqyv/eNy3TlU3j9aFcls28a6oEoHvNGBlr0nvPPe/1dw2QIGeqlrtyLrNBLKWpjYNGA1xwTImST/cx1Hq/l9ExA2oEHEi7bYz5//6coSIijvKaj2FWm3s6+z2NAlECCKLdWUbNunFtu6fnfR0hQAhfgCTa9+PLfJM92mY22yN/PNGvCIho+hjzv+vdXKWIM8SF9P39sOzZvZMedxi+WkVDMwRmTWSLOvVnqSg0lSRTfhnTen253euPVhlhCiEycsswxeau6e7a/UlABiEeAwYjyXuIs74vvtetjSghXuC2kdZ/1bPxTdj5d7YbECBCFnEZZrqs3Xe+XNstsWUQQrzms2DbcH0/3Pnal7dbLEgpRUOgy7DKWifZSa7628uUQYB4XmPAAjZJrSp5/TN3lsP9WmKDCYzNZkjQIZpat3JO/kQuf/ESrRN1MACxjGW16N3f3uOgBAghQWAVLToeY1qdeyX/yeaqUoZSRWZALMfYqvtI73MVhlRwpHSox5j1/twaDmyIVoeY17ZMm/aXw8nHcAuEOKcWmN1lzKp3rzZHqUqJiBiSyO8Pe/X9bneSdkfMKUAGg7FNkFC3MavZ+9KufFMhFCkhBNSleD+m9c+9+81c9rcEhjbQxQGV4DXUbJz0K0n+bVGbKAtCsMqs0lz7slek24G0BqlgEvsYZlqdbO/ZrPO0Ngz8AiQkQMwAx9jbW/O6TXvZ+2+i8B9AAZHBYew1pjXe9+RLiy6tijRhuZ+hml8n17v+OE76Gwv2YwgBYonhY6z8r5jrH528mdnt1gIBkUtEziqXMcN6Hdv2x6ksWUIMQUDQZay0bj9t93oIfwBBihow5zJWk/q1/pPPsY2JYGZK1DV0Nei1SmkhpIj4fyywLrFP9qsKFGbP0bGKdPdDReXz29sImcA6Zv9/zFsvySmUKaDeCi20shz2frr/b91xlaoSXfCOEZQfh721u3f2uIUhiujPjGzwxDJm1e6VOvt1hAihqqBxDghwDLUszl2Sf2/7Yok2lCoYkAGJa7iqE/tZua+6GcQCjYQQLE3+hmbv+Oz4ErdfbhcevQgBUn6Gpb0dZ3/S2nG8/ZnhuIQQxy144D9Z6v/XSW/7X2w3fyYGLftBG0Kgg5D9GFt9ej+vVxxqRKzgEXuNaU2+tFyZ+UtgoUtRRFgQzCU6qXEKuQ+Q3H1AWq3Vui25chxmtXtn976nZQwmiK8ikXcZM+uPfe1vR2CbIKcEguNnmNr33SuzSdPOTiyuW4AQPAHClyy1oRdCL66bzgSk1ffxLJIQGP+Mtf6/U9dmfk1vm2Q5rCIuIQiifnvG9Ouy/bbUBPoDaSIqOpj8TVTt3H6c70j6w+ZYAqeoBRKUnG2Y1mT3LjeTtPc14bfAINsYHCSI20tsVkVml9wNYZiqgNR6/lZblmW7MWTvx7TvuXs3vzWli4BGEQZCy25j9v347n690lyJgFIEbCiYhD3G5v+v2Frq6GNvEgZPUJOhWvtzymsCCcgJVi1xJrJJ9uZVCl7IJ50Yu8bC3+xr3Dl9wgp45WSTSeBr06e4OrennTnraiQJ/GSbkt+ujz8J8OEYYps+TN8csmcOvCIdaYOjCnapyD07C9pkJPgt01dOe4IJeL6JN3/g2uXX9f3qwJYPGfITvV8evGIKyO9X/GobdqnijPRW5cvCh7oTF39nhUk+f/lQd+ICyJw4OWRtLS1jE4in3770c8tFnS8JOxOH3ZPLVUJmMry/B6IOeFsHSJgS0kMdfCT79Rh/vQ4Qx/xsxi1ayxydLzv3O8uvg04/bxThXp0B8u9nh2UeDpBFy3kR/q0dOTONkUzMqYBM/BDnUeOrb9m/Ht8JUb8F99XD6Xm2SeAMVXNkhMtqeQ9WhAv03doqU3hucVqqgqcCwAubdaKys+8zvC9kdDz+qhhJoLZVMYiDJcH9OjgJ/qVXgCOehoLla5wDkvoZnAOeeCj1mrCQUBed44r+GU8TNj789+8sI5VbNDc1A773wRUvfhG031OZfvruAouQG9nPihJ/Y8yJGw/zI4QOTlmceLhPLZzPL9z/SVWfezTsJUvYUMSfB6NFCM3MuJpPlYQEFWUx+BYv79uKQXKrNlJbZoa/IGyW/H6GwiCZSqg3WUKzB59YH0oIwxXzcqpX7nCUmjjCfl6Bj2QzXxHPaPqbyzMkwt4v+bxjHWEyR3HRXcMenGhO6P8G54AVRgi5z8rWKQu8Nf1RDgh8uZnhVurC/QJpJt7gqM6Qt8piuZUZfBQ18eOSezs3GAeITa976CtHd01gxOHjJcpBWN/J0+e2Z2+6iF8jeShmxZmbXViqrTuz4a9xirbVfHTIM5/jm8Wa3PYVcQ74+Ydlfu77narU1DfiQBIIfMWs2cv4a1BqenZl6ENuYZ7a9BAskg2H/xR3ZtP/FPQU6ultD2LrMnFvexDb54W6ks4GrnLNpaBvgr6KedD7F2hqicrnbobFOy+rIUI2UFQs4GF9A/tcSrcX6nAUu+h5sOl5tQtJyXzA1yqi3vZltS/a84O/dvdw8zNIxoEFUxXWdYSn/q+GtypfY1dxK54Udwd1YbNOBYEkpKLCinB35yrmjLfP+URcRYVSHf9kxU2/ilmhbJDD+WDQw+Nn66BI6uMDOAdUZ9iSqMJkqkGv0SFQWEVC4qN892v7zyjh2bfI7MTNVs5l3yEVsxTVwxNxIp7Po7K1Y8fU6sjN51ZKZfwan8uzMnU1cOspySAsiR88cfjtw2U2bfbXCuLEhRcOCcljUpvGVAgEN14GvKQ6fl01GttjQmbnbZw+aQU+knZP1px4rul1RZ/DM09blhcCKGeh+683y1pr680qpijLWZwbhxuWs+hWD5uebKw37jp48TpnKduGehfj/K9awr+AbFTdr4sKUXsoH4FQn07azBMzP4qSBUXLcSDGkzmI0qRhtPg8TSeap8Ke+Ut/JJ0inK9vk71K+luEu7sOxt/CNY6XZA3qHORp+i+B/TN9RXqGYePTmfNiJthiZngw+JcD9ojwsDc6VF/oVO8U7lMXPKBcFQwi/h/VPhcH+i8w51LO9vVc6tm+DZv/c7Gz/XCwIJqDBcNdCYYDPwN3julJ5d7+trAUgcRzH7fve5lAMkb4LK40MlSvfz9UIlUQnthAplS5MFvhu40BFWfpnTgQLJa669I+9m6w7VC9uv/tM2x5NtyQo6V/33fVPnwVqmheGSsSl5N9fRHCgwDBQeoTH2OBSIg9AjF2tyX00hhc/TWoYaf3y9s0xqOECf1b+D1HyDwJWhD0cHyl1PmehljZYcVZLdklBnw2lmo3islUTNrJm77dzJWpQD8r6G0RoPn7aDYccpDrJces2gMB4eaxh6iDmE1n/asGMyeSgHhLFqPy+DuBrsIc/zJmf9z7/9vprY2TCZUTmIFucVpT0Eo2VbDMehnD8ICHy69OHy99hp1B5pL5CoyRVwuczSXcom0tah55CU3bp9w6wIEXCx7q0/HVlNvIKZDjvbc6/2HSiBbBkEXl0yj8KuUIBeqWetqztAg+vxn5hLBUmq5myPWm+sVFCvX7XIZc30ZwdjbIz7iRjtmDJHHzBF0DqJDQiEq3vkjWnjL0RG9J/D9Det//XauFeFMkNeI9kmsB8TVn6x0YQaEvW3y5dbKTVa36vGj7dpyuOxk+wdEa8TYHQ61M2Ttej6ZTUSrfg2HjVMqTRya+dRdY4n1dMlCS/clL9hp7tABStaTWJDjH2CK7JFgI61j1Nd+2ah9i9Y4RVncj1bardh6J+EoO2YuGsowLrbrBSs4qeqz7URR3yhEPsKSSO8UYNPFNyb4jv9qZeN2v5xR+r7iK8jTZ7j9xY7JEo9Xv4+4fV+jWTiz6rypSLzDQ8vcUB3hKCIq/ZOr7K1ZuM0H/iidSeF3WRWiwT86mkpuCEZMsOxKfl50MwlTIfIvsZDKFDszE8cbXvDm1L6F46hTdKJ7mhh+fkJuRA02XLNcSuVfHsr+ietkll0KwdN+xlI3UeA1gCyDOe7Y0HREjsXAPV1aE4crk8Bqqaj8YXFkyApgaMqMFszDtEdsRe/jP6OdaLb1frtxjbCVzzC8xGjv4Xtn+au2rR1pY6v7exAzR9gybMrxVdgNlCFkk1JUb17Zb6xlfvmukzaBlhDEAeDoVSovE5JaKK61rszy/b+vaHsTxwRwhqLjd7ujrOge7EtTYjqlpupZ8FUYpeL41wqJ93e9VZaTSEOK9Mx6fj+72lmG5YxtXea4HW/VhPQQGAy4SSomx0K+ijruQlLAHusucnon7bsoNptJuTjxeKeuqzUv1WxlIrfXbOyUFIatkQ0ynM0l4rFF6cIws5QFRrGl0j4xiid7ju+DU9xQgebwWmP+5NDwHS0D0IuV36YyEOOrC5bCOnpYL3ksNPqvWGMAkmewtGFFgvUuaYrU/jzEzVyo4HkiLnLDJkMnfnz1krJy9dgFLKW3SXxW2sJCUx350nY5E/X7x7jkIaqfhLdEENIa5dK/HfvDOMrk0/ull3+/s/nV1BdLTxLq95o5IpKlLw5APF6e3TA0N3B+/rtXID4tMHAumbt+C10GnssYOP0LfJLeXtn7uziEDDxxUUYMLLGKPBlfWhpo3zCcTlXZOcYTQU84towLqlsjNGHwJBYYC95Ee2wAH6FtFPx55TYJIVbpuTkFtcW7l7c0ywq+2SjmSTQKHa/1UG1HOpuOzWGvBNrnf3v/8Sc8b7PkLZ4S39qoYxGYlffor8bA/bqM/jIu/7h7KwUd67qmpPX1CWKEY+5ftSQeUn7XHtCqXHdvZ4/V8D+XWlhAvKxvHK2Yks3xQiQZO3CjQzzF5XQpfGM9+4JSD0SJvMrCXcHzTJ29I9PK5j7meZiHVOy3nTZW10Nmra+JaCtM8soqq0k7We78rIrnKYDcdeF+5IvgBwcXTO2yusMtijJdyP7tELcDGCUWnmnqfW11vmpO6e7enztNZSNUHYAUCkiv1eOur2I4pMeE4WUi6lh/FtW+Xhf27nCv3UBOi9J2yT74UG9giZ0emjRkqohdo8DGzwywc39d0LjStKVSSjcLQNTf6cXXnfwTBFHV0ySL+3KpB1MbEUAz51nsoB25v64uV0XAd783k0cL5HgokA9pSIGMnig2bAv7oEgpoBNTIRH3Q/orv9XADJOVWHLcmGRicy5gJ3uUQpnQoPeuchRfwq0rnpj41IsAGV2ewghU/Nre36X1GcosjZj8vcSNY5ZWfYqBIztGNUfB7GnJpfX2TKeqFXODUqETSibH/OFTWLIJJ1yTxyZE09lVlzcic7rw5qIR4HFXNO8BY0QUWaONgGj473GBLHyxntAy2iFep7jiUEcLIgPQ/N8sDmsvzrTycATIjCNVbgxWSsW1NfWgFZEYEUhpho6UTEDdq3xAMN8ZhO8mJ4GxRJWCm2rjiDfS9RhEmjMKsKy1Nly8Y5z2mDx6w1AS2aDzlJ6PHOI3lqAnWFKQrWxRm0IFLH4QCtkrXhOiXCdtnpN+LU6S6Q564DQuYOrEiMjG4Q2mjn+K8nGsTbASyyQtE1ccE8laZvhJuqHXzNoJAC6m1wqUMGBM3wt5VOBV9fZEZrgUWwAO9PBn5sMLDM0Q0C7Q/bYYYoNQJQCO2f56H1RjddBNa3CnGl9BxqA+2Gsn0hLClvCAwlmvyhMpmDYhEZ90/YaHrXKknoMnk5w9nrL3q32AFzPDFjKLKLO8q0JwwohNez+dsAENXJyyJc/o5SgkgQa1g6kemBIWecvkjoDKV026H1KPGsqmakQCNunGBPrBoGnrgOUUiyLG1OxHQ8S6xbJW/0G4pUCifPeHogEvWSQH2nOB7nmRTPSO/hAPl9F9mOfG9hM7cteEowQquby89/NBEQclAVOIAeppM4MNZTs8t5eufwQBTTG3vXgN+39hQrWuKr01uz97hsHgKIFq9SoUO0Oc6H5li27UlMwFP+ixQpRWudMNLYa0zTd2xZ0cFQTWnjZf4zFBC9IpWbD4ticCgTfB6x61g4wAv7mmk1RU2NrmX86CBVijWLeYHSrTLSizwErTZ/6zKPRMl5fQ9RabM1/YnLPCfjV6BtooY7RPkoWk7cYrHy2M5vWWtYYWqJUjbyaQGDKAZSUxtTLmtr8gHF5ZWI4ufx2t1tWqNhHtokd0b1YSRlIXyvNUe21T+Jbj8+rZa5s9ct6guH0B3OWVTg32I5dRVzFj78hU1Dnzk1Ekeo8O6pqi0uZ7IEhSXkH8JRYbB12rb+JfWSOiJZQMza2z72CNXG4x97iFzXThIpNGbtjJ6Dpdvac/tmE90vgBEj2j0Ua/CYpLMqQ1/jh8pCzUUqL1HCWmhYLiGgZMMFIqde9A7M9c8F9i/bsxcy7z46nfxrTimlWQijS3he3FkVvgVXfKyGI0/zF+rxEQFI7yn1Re8zKLg7KORSLln0/GzxQ/EGKHo2WrwgJlGbicZ/iv0EPfQtemQ5Ajhft2LGW2+/FqvHm0evJX/cq8wmkRiWCK9soYsH+g4F15CFEgLK6l4Jj7oeR4MjGqhIbBEO4juITERdwIQ8hvQLT8t8qgPqKSg0sfuM9LHt2yo9bfQZZHoOGOXTlgsBsoOdnGZvz00JeTw8NOQXHUTE6rt4y82FmyyAKlBWFBajJ353XI+AU70gp40bf0NsACVI5psu8OMkVT2LLKGCDwZnt+qx8SQWaJI8LMmX+xZZGNNBAlKaFUD4k9nUxyMuXnmlps9HVneVJVwUJ4sDEGfLXlHn4iDVKRvCDbUYYM5c7Fsr/7kem4UyEl6LwUb0pr/px7lLZfbwgoRYUCwE3VFiHYamtDLNSuRCCVU/8CEaptd1sJ+L//vhpmCWJ7S/r7Fsc765Hwti3IOmlLzOEhcpSsng/xM5ckrnvCmN2iM7ofPmfJSViP1B4UelfHHF7bwbqhcd/Yy3SDAh5XDMztnhryUwod2+SRYqWeR84b8VBpe/bNd3w8rkh2MPNxEJ49t0wSyoHJCIDRdYi5pbDxgTAWSYFWbF14TXMQokotZJRqxlR1JZAYYWiqfSNKqXWRTh2gpl7QADYDdQMjavtveYrhpUy/SdWXfO4t6NZgAIko2+iLnu45YYAzKGx33nMLmkC7PrCMHP8kH//dnzO66z/QNtWbh028P0qaN9wBYjbEwiLBNGV6u1ZsBqwyfl9se90EEyesLAOL+iKp63QKH+8Ph4P+DTnGi9rYuSPS1Q1swJCrn0BX3LmDr6kunQ/H6CTZkLZdqJqD3KbikH6h87TObEx21bdc7CCL7dfuuQ9KPbQ2P4LGt4RE8lueeCKFYn0vgh8lZoLtJ7BqQ9hgy+IG1EtLAIQCya6aC4O76a5WzKEFUQ/eE5mSdqSHhV2WUytmcj2CFBVtW6ft8htECjULYAfoIj30I499OovLSJPmqs0vlC6x9wKVuO+YKhbLvTBMWAgs8/203sHT5BkL7vfcd1FzI0S+HOPaAXqaPpoPvRlAYWOeN/5UjQBifA7XknFCw1DFQXF2y2D5kchqgwcGTFyOx8RyqDMXsSyL+0jJpnI1FZRRvq2PlpQl0SYtsvkzcyHceMmgVwuGnY/YlvLMN76Ecqm8ZGLQvn269qVS8uxyjc5M+hO4C8FXeKQxImGBygNziZ46UNgxF79fLgIvFcPpNjcI/miL7rZ/EjFFFb/hozGvdRzH3hDSVctJysG5GYyLVCcy3drCOYG1Ymrr09bMYZcT4Veok3K/O5AAcVKpqju1dy+0auFDL/wYQD7ulPf9qLW0E6bgfurjyXxrKCG8tDmEhFMc7+7Yk0jWCJwsvU2G1Gg9AYClnoCjB6YG6tKBN8XhT3q3eGwutaIBSoPAfV52/b21cCI4D4IXwfqPvptN6/i6uPpodk7gxpNycDvYYtHaAeEvwUw0fLmXBwYjxCAWpOew0HjxUtTkcXdwJHXRvLO4pHunH0WOXNfSDNoKNtNfIiGyqwf733S4+a67koUoWY5GzAgV2xd6dj1VLV988e1gMCs7g+NdzO3n0G5JWfFKmMs4F1aJWxd2Z98Yy9oBWojMy7+2FGhpujQov5bcuZH0h1o/t+ehTcFMPC00/9B89BjytHwVf9uE8qHMZvasw7FwedxtBwKkk4OKf/bycVgjWvGPPyT2IpVxINOGD0O13SyEQFf/DHTdnH/gg1iC5UWDCZBV9+oE5yP4KJxShLrHlyG3OG5iMXvIkcMfz17hTcNppiIKCE1bmuTjaPvzZhMPH3tt5mDU8EwIlPQ5f7bQhncZq11pBN5SjLAyFQ14NrHT6Stod/ch+78mbNMpsFE45Vqxq/xCL0QKVDsd1td0NuWOvS6BRQ0ZS/kKvknrU0kB6sAFtVF/z9P0p+HpdpR+62tJzbSXnQc/IxfQdFFS2GnTC4geFhuIe2bcppcX8Lhe2EEzQwZ+xJnK1+trrisYc7SsK0T3IIwSyviI1ehf5SIEYR4NA7F1bKPZVmku8NxEwB4eCKrjvAp9/iyBrGtoDmlNsq+nyUOaoSWjy4N5d1skHCeFsZiPM2uPby4KLpWlSwmZ7TIDfdHk8wYyHXKXk/IcGm7vXxD1rSitCfULUnB7HFf9SEvWTrmwfraFWWh9hN7ez0blWPs6g3R7RcNwe/7+VUabF6vbBkFv/yFSKj92d+hLVKdeza1pbnekZqsjLZh78rSKQ41Smt81EZiTKZ1i/lucBS0kPhf1Mb0aV9n/6mhOofgVzjRNUaJpYrH2xFv9H7QcLocntv6zn+my7fPA/CPZq+w3g3bvxFdxnrnESyCtNXOriMB7xYd9xtRzoxGK6Tg3Va9jlBn6CYunaTiyHy/LmCW5SzvClgWJbFl++33ZKKS6bSz9o+qin2RxAKyAsZGyCnCmQgb4f+pFGIo95K+GY+19Oe3xPeteUkSJabqdjVZbOzAV0d4mIQh3cja5IhBNW77y6RLoWhOUtn6j7nVylyWpBXFTV7XseU0UA1JAKKbtOPZ9rtqpC7NR3vK3DQ9cybt2n/LTwsUXkFl7gOSuHTdcLNAsPddSVRSjnwhviKfYaxJLg+ZsdE8ctcFcuAhiPmyXnqE43nIssScf2BiiIMUlMQ03ab76Bq7mWeecEx/bxjb+6M6jXExnU6g/PWTBjABX4sQ0Z1qFvAh9tT/E2B0vMNR3QFN3X32CJ995NT+nGkjWHAFkAdZFYteA1n43v1RpqmeovDeATQkdcjU7Hck4zxF2ovHpNdxMCgAbZ6hdK3yig6lzUG00pRHP9MMeGnyM9ALML8zPHUfK603OcvBYO4OCKBSrEYOlPNMUcJ2fF3DmXKWQ5N04DhNefxNQEyfXnVTUg/jxGwPyRGjdBmI6SLu36fwVGQ61KwKzjl7/O53oxZSR3LO6zUfyjwbwUN0JTe67XkKzCoMISiUhIPqrC1eC5Pg23uhEVIpbA4BWWP2d9NUoYR19NjodDzwGOpCUwKE7tMNuMfqHkkdDHCww4Fkpf9tG1m5R3dDbIS7D0DbORLneK90nHJsVIMUxJh9yHNxDqUGrQFdC7WfBheGMbSo9HGOjlexDnUkcgwKZ4xukZOgETlzZJoqtHwmAe46jVNjgQB3OEOgv9f2k/oCaCby7G2uzV4DU4PNZuCE+E18Djg3G7Lq4dgjF+fd3eN66YzTRk0OM3dpjuovX5ld7fIiYNtSwNZ+iRwxqFejVROms955KjdqdKiqIAuA/Xeu10lrG5pRXdTpaWjzNISkmR5fM6bN20sEasaaGQywrKeFq2UPVWn5FWFCt2toQ/hlMO1boEzUeagwtKZSmXj4u4UWZFoOaG3EIAcNJLehYoVMtZ+WJNwTJvdBY97I2FaTwbzjlxCzf96nGz6V5npiBusiWWfkFBEjHoSe2c7UV5XipDH0qsU0oZZuLKlv5X151PwnFudbsOCKe+2AU3+ETt5QCSuTP5mYwcCJJ78ZBHvlMioqVw0i/vfI7d8fnpn0jXOuvv1tI20IY6cJCX4dwio4AbmZqgDK8d6O2VnUJMaVp/h+WkludkJmyF9hwWsEzbfN17tml+x7hMCLu3wao0ekpMwTkWgoMWICq5wKUd8yiVstBLo87OOT/te/nAdUsmbAyLOhvy2cCUwE08Cm0PQYCoP3E4Y59AzHWE+6SAQxjBXNqqEIIFl7aBu43lBMKObwh76ycotqWDKbXXzGMYnBxVqAH+9KvOvmZoilG3S9f7971+giKJbBTdhdu/ViVKqWsVnwmHbHxYDdaTypFbFcoLxQanbfJMIp20bqWrcxi5brg/mTwpjLbLRdZ1bz1nAKFNyz6NzbGNYbkvYXDYjLyq2SyAwTU3KeguLGO2CAM2GhN+8xvaKYaL2vu8FE4UcWOnv/6CDQ9BXtN2jwU2LeR1wb7x8ZFH+Ns/g7tk5A/qrs4yTOenexV+xsi++om2597W+R3268c363XI+B+sUrkycjPBai3Bq9DxWmNXh6jK1j+/sTCdIfsjAoPgHoVvotCr8VD0UbaY2Rxzpdaw7ZQSNYfXm7CQ87PmbEQnX5EkQhsiywQTD7OMhArDXp2N6dxESOrSK51iiM4tSIY+xy/AZAFiHSAaHCsoYqwU9PdVXGfn63pcJKCXFF/++kDpnMi/2u/PcVCwrw7VU7NPIJG+y1f0uHz2/+qeF4oVpymF35pgWdw7beTfzTLguSL/bpIlzWVx7+Rv1KKay7//BfnX0GVT7S2cD/9bKSESeQLWSBfuHXBx2qHcXfczlAY39wLvO/NirK1pBjoiVakUSbVzDIp+ZXEubD6YH7guqJjsNC+P4RSpapHvB+sFVHmqQDXOAdyxN+HV4g9P7xNZUazS3WXfYJrfIIx3fOKZk5GjXK6sr0Oc6OtEVDDz6cUCdIEQ3mN+o/JREB8wT/k7Qc2nUXdwqcP8JhUL3a8FSFcoB3AidrwVNmyhNyK/6HZaXKGmqpE1VNsoGuniATEDEr6kRvRZaB4NUgv0WuC3ktYngvf0z/LchFb05oA3HKWn71194ciJesj6yZ+ZWAgZ+oZprfo080VvXhI6xBqlaqQcOLR4e6cYRNpEdYEjGulvUSWQEbdYJwAYwjoJhKi5bQuE8YBBDkv1+vzovBBxT/FvUMcu+is8MXRg5Effd/ZKs6jIMTRqYlHuhFSfW2muUAhcnEQfodDFHhWDqGp4qeTUoIu6JR1yWbE8/k9HuCEKxdbejaqpeYuA3qTKkUoMyG6rrZrrBwFyt32wNaXdHynrnWBhwX9Z5KHbNN6QjJnGqqSRgb5h120/oY1ZSrqiDShg+xihbyAG85IKXytjEQY+V7dC0H488n7Eb05U2yWNvZdU7qqzLLzt/lLX6+5oPmTF+fVMm9BcvJD/cuOtSw/92HCpM1jWkK9TuZLF5xbYBf1h2AthO8r47glOFbu5BZQwawjOtoDfVVH9/mCjCMvFKPo4QrxJkFywYQbjzpB8XWE9IgdKwTWkRrqRXYYThdxu6EZ1S7EZZP7lvl3oR9XEDwz/ZqHjeSwJuau3xdDrZh1YnmBZhYPtiOOa2iry8gB/y+CC6+pff0EAkWIP0AW+JAokwxcyjVnbYrv+lQbPr98p9hDAGzjRvnf2Z3wsaxSxcbPPIiFtZ89eaHbW15Q0OXMOVHgdblzD1KtZOGsZBPJoLNhteHoIogFdyyB0pgokY/ExN9ogOAE5vHenGJSV1XpcAv1Q/86NNLSA/iMUasyG8qIXqJZ/rIfprFbA0hf22MKmEv1K4XsDEtPQfq36vXPQGdMH9Ul2+5s+AeCIQPgLMZgIKYEqsIcJPNz2EJX7+uqGox6CUf7U9Qd2UK9YXfqtUTmQHH5LT9onAX0Czo7HfquUrV5+41DKBSLGibBuE5TBNq+XWuI/+a5u00WPbvSqNwuhhCr2FW9x0W2cBVReDuT1YBR9ALcwSYNaC4xa4X9DsBntjg1qX7Hh7Y7tomq8pJlfjVWAntdjmkQ1r6XEvJWNKakEfgzjXbuoJ0ob3n56lCv8zc+rLmv9Fzp/2ddfGB0AnBB0zW9BCzTTF12snmXd8ni858QIuVnRflPcuUbAASqYIfN1tNsWFWK3beAHU3vbvgSdWy32ZZug4+2rtIYSf4Nu82IyppVvIEwOz8P3s52i+9u6u5QvO5X379q96Rx07t64/dsXEgupy7rD56vjQzxPS8u3nDNx+3Up/vkSGfIvfr3XHynES6IjPtxuOeaXz/0Dvub+dtM3SVQDhQCa5TUf/X8KaQ+gSHhLD25Ut5DWH4CH7KW44kCAS2fRBUTZvvjr/wuzrKmB+QU+cAvJiWhE+EhxXAEpMV4+9OXvgvC40z2UOWPh7o7z0/IFUOYrjBDR4e/YmEP9g5F+ywbLWIaJ+oIp8/WlKPMV/zcODY5fn/E0ISxAy8McTC1yBKgf4Tb8iY9Q4QI+n3y/gZNSLjv8RxhVdcsEd8aVE8n4UCP5/91VOq1JC83L75cjPPvoCLBZAWAENmwboLCAsAorHzurhyU5JdIejVqgJ50mfeWq0h3wLAmdK3Kt9gBTzbbwnOS0FY66Vgr+tdzJ0O4VOuDXAOEXdDi5QypZwH1q6oadcIzn9O4fBCUPv5/fDUKAGwK0Dzrd1INFhEP8f7xQMUlqNy1EGs4eFuhIHffANpbQyW+13xuWwLYp5fMtpRbnc3me8Gny+HpdwXtbqZemw+58UpKxyJVbTIb7edsnsld4mZpXCpL49BOOYw/sVteTzlPqk65Tmif8h49O3U3tuj/RXDy6X2L6l8ZOHXGywyOhrcH0omGBBgz3nNyEhQKAgqZykbMDPfNqAAVJZZzpRUs0QkZr9zI7Rga81PgE1Mk1mqMn50HFY5Bwk+cJPbk7DyduDdZBgNWhThIVGpSaYESFjkzqgV4+82nyeHkmNCEZrvMkh9kL0amhcpcFWM+Q9umWnfwc6FV8fWu4zV+6D+w+R7OKy0TwDbdDwtgkUqBm8DKbp3W67y3vhg6GsN1jVwCihIP+RUhI7xrujriPM97RiATvPkrxSiYMxdMBT77JWgG56yGnxI0RthvCN8wMQLaO8HffvVPUJYOZVqKWFtzrjADgjcAify/vHujq4gZv2auTQx+/v16HxAfWy/vRZPo4eXZ6vXDXWu1p0ArA/D2opf7e5gmXrPBREX10yp1pL3Td5BU3rEhSRU1n2B9GN2vSm3sgvSmYI2rbgxx4TQ/sVm7PMDyn+gyjc2pOXf23Y+90lQu7Z+vIHbOBSGiEhHMXOokFva/5jOlMMS9fktET0JVsymBITR1a+k6gO1dbMSuCfTNLTl5O7DKx8AiveOPv21Hy+3PFgB9gzRCsaKflV/D9Y4MGE7FsgBZHD/w/nv7+IK3+cSGGO7F6/YCvXlM9d2R7NERjV6Y0Lx7pLwVEUdfORDvsIAkB6abnIUmhJ0iSUitjrTgpml+jwPVePjU0e8PGQRAo1bsOhGUVhjItCPrzDWaP0xdGGASHcmWhUA0WIHDAV+dOM3rx/cuaMccrYj6+0dN1Hpw7iQeoTA4PRiSZ1ZT/B4a59eCcV32e9Xk159yQHVD549/i0enqvOrzrM+ryacHlIPmoaBC2tRdZuen3LOTcbt4CEw76SnGSG4xzdVWEGy7ksMXtpO/AJA82yhqIxG+visOE9R4+m8JFhhNXs0HHvpaqchVfE6D2S35TZ1vPXZz5kSb6wT2/5e04TyMzE0AAvWjqd0QYNtOCj5pB3kQV0lyUnHAL+njoTkBCrz38CL1trQT16BVSA2xKUory7Ia6ZG0IYhvYfq4ww+ZG1Dk5QC7EHWDkC94EwWjSFnqTQwHC09UGtYNHoyrsJzEqJRFOA3hUf3a4OM86gtYn0dzoRqoPTMH1b4NHudRX8D6PJp6qVU0Ak13BNBQ2T6/n1RhDcx8Ec8Ylv6b5w7c0md1/6gX/OK2NCS7FR6ouwc+TNsQAXxX5QT/zv9z2jWh7JS+QTmR7cg2D2w5YDU8rrMC++X2eHhXGNXSWdlMAaphck1Lz0mWfUfe40MnNpjBaD7Z0Oi1GAyhpxXZsM3oPmbdj4wXQaWWLOtw+fHokbejbDNIle14Qfd0TkM+ZXsCQ50sEmGh9tA/KBzKJj3LVYLoLtDtPYEjomh4Rk8TEc1+4Qk7B4aBZecYghiYXNGgp5d03i3h2cyh9yd/F4hCgi07xgYVE4NNCjQ/b2GRr1guADjOTWpjyLNW26aHT7KVeHw0t00BkQyGOPduKmVnor99yEaLNrgPEMYb7KZhxUC5F2SLt+OTMz7OapOFcX1D4QkWCbzKHbg8xNBUeetjjc3nBu2ygRO2GfRy6BT6ru1dAkY4zL6dYlDZurUS4k80C5hXv6VvtOTub6ks8Ij4BjdFgocoqYNt+uEScnKGW/4Oe3HqpYG2qmLHDo9yoOWY8yVkaS7U8jrZozPhN9DOARrHzoTYAu08GXuyHfX+tutunTrOxMnNwcnQzinLSQiw1U6ny5NYWtvtEoIjPh+WX6OaDm0cjxdgx6PTVJGgSSlIt3WKB/fQ5I2YFH/4X8h6/bRMq31R40tetlTwJRPn1+/o8cLF9GKtfAZKxhisAz7RagSGvwn4fGUxPCypLdr7KqETgZpceG94MrVbFVFGuXT+sXbhnXlkMOfCPtWLLBjsH4Qb8rLfBCTSY1jzjt2NdSsOtAXvjeeKQj3CqGgfDU+TncM3tItRtDLNgozPwdzMUnTysxmYsvEVlPRD6nKxJqWINQ/Sx4clfL3+/m7VyO2uws3JI06dbjwVlWDOFpc9QySSb/ZWlABmQgKZLv6uxtOI13UAdHwCoz6GYHiXiEYpdkEjf92ZEJusasaT5Qn4Hj4rX4Odnsynq3HcyLxWY0q2huNvqVOuMM942XzyMr1mLk5G1/Zfiomc83j8pC04Y07Xr1e+w8fOGN6VQO/afv7jEoxu/e9fsHobS2KGuChLKwzDgG4o50qMm+A05M8IAyy4UE5Ue0yuz5+U0+qT5WRD6Kj2KFdfTspp9cnS3xAwGrHqf43Z3DoC80/FlItpVsbYN6iVk9shOKmnk9XV+9f2DOq805M5f7KXFgJP+brRbSP48Yb3DIofi9ugwDfcIaJg1qrxbLOSMEAPiSpUAjed38xozYiK0hoArHpehUxeIdKXfiG2MUTaHqMbL2QVqbZpbGq+gBlkvQKrzNw+/4qap8IE76XqHT+xMMFyYwOjtw7zfpZEZ6OE3DUAsFSOHq3ZoeB/ma/CBO/CBJ/CBE9f/FWObYJVkWYEU2THbfyxZ4PRbrWJYdS9e0SF8YE92rHAhHs1AUGLPljyL6jz1YQ4yTNme8WGVt98jhIsufUdcxed9pjDMPvV5gkNN7pTmbTBYxuV8R2JLR8ynzXiWkbb2UF1UWIrmHgKtoGBybYZQzScAHCXmGzHw/YWjasFR7wvCXz9xeHS3sWkQvvd7jvkBPKBXX28/VqcQPUc7xibmqyC2T1Mwujb3KXlOnImqt1Ct2/5q8tq6Xq0bEcX+jiFVHlfbRZE0QORb9WjWGxJZO6K3Y7w0fk7Kk5YP/G2izLJKL3ksZJwjNkyJ///Ux64YrpCxJq+segQnfCuffL9MSuPTkPnYpz3jH9el9AJengHZQ2OQE4Ohl4tIis2gFmfTi7dvhkiyW2gsWEiSuTcgrY6aa8bM7fpTIgzAGBr/ljKIpIqyOlaJwGbklloEVaRSq1Ij/SOYCt1SRbMCWiAtqZdJxkEjVOu7E81A7XP658UmbJiWv/AvXajF3KPDgQo5JVs5pVcm0A3CrkZOhfBmulVoFIhop1BJNpOedt861ljQmhy5cMa9OImbG2IDfYSWEYa41wbIQ1qEyuw63lpYsyhp++P/lZpNt7GbpJeFbA0tYcypMwUb4mqR8iYdssXGsW0OkWVbKlSpgKzCVwsaodMqh0vItgv6pXN0fthfO2jFX0TDT8VNl4gx74RdI0L5cMN/d2guTaCp+1eV5FoGvdOAMLqVdqW9ECaIHW7tU6FBavd+bZbmJPWkIhfw53CzhYgYHMQJptAeB8KUOigrIFYfVAE1FvqJgGk9nuPmvWxtAuqToZGV7CjavmVCoc9fX4cEFQhcbWmhwzsHrs0enhwXFChkffXvPlfGRRxk63pIYWPXDF2E6kz3qVJSsETu9fgcvByCagAxyJObQkXU8lHFuuvctVHy1q1U0cgLLpOqOl7wkCyZdfioHlZRe6CgRs1TIOuTQ2kLA4fmfEnRFyHtcuekVI4HN3aIwUVhMh8xWL7FfaBAu1GS4mxKhtxt7LAnHe6W87uIlaggR749HZ69Z5EAxRVubUgfhltX6BRIH3cehGiVpgoniPWKYPT6AuGX+pE/9rZKgigN+wI5GlR76nVubEsyG/1Vs7cC8xuOKnFsnGdwoBYEXRnJDCqFymYodMWM0gG+Iv/5nHj+QUyIN1MWwRoPMxeb/QESXJnmR9fmYDTDY/oHUVVLssdE/X4vOPsziN64CTXshyYqGU55DVB/NG6ECu5S2BJV9fieu71Ng6qcL4UoroOzMJAWb0cyAoVAaOVMAmYCnG+dIMZXqgCUCMTITBXKcJMwisM75sQByAv+WGMIgdWnPJeh2+imjfBJpMDFjjKMlg7jMC6nSjTePeTiPbcOaaYWrBGihphTriQIIrvvgQ3ICiVlqSFTbmsWUYktPxvk99eTm4ZslKAd3MsxSKVfyuJM3liawb0rZd/c2fO0D5yNYqFDWCzleATto7ZPaY3f+wwEfyslabou4UFyvheQ4J5DaiJNuDLsY1g21OPNxmQzNhbgSbbCUeggChe6GSyHWd2RicLJKLMXjaLInLMMZ2La0GePX3VkJXA5zKQ+EepfKT3r+mQchL5YxZXAmcG1FdD/x7yBPF0bQjRTwkZ9IyWUPkkmE/fkhpJeTfpKo5NGn3LCNEnFJZmtDiUfwCAdzP2Cu97wrcdsEAElM96o6wrScTIJFNEPYpnrPCf95CzIBMLfscL5SAKhu8N6lSlw6+mg9xVM5OlkT1X6PUJa26C12NisEJB9ZqamKMEs2lTQ9vINlrw0y60RcRJhOr93pRTmcS3+Gn/JI2QtAfsiKFEpQFn1aIyoFPmxcgSGgfTgZdSQjMP/1PkAZNL3TAXRjeGtrFsxU8AfbtrYVxyZICJ1pDP+y2eIVb1yZ/Zq3gYtKHXI1uz3UYXzYABqTGnmGoake/bNL1doce7fwhxpRO5w7K5bmr0MTLphdtgICvMUk01ZxlfWYqy55N+BEmHokvtkas7pl80fIzi9jBp01Xbd9ajeBP2FFXQBkHx2eaAUsjWQkJWbGmq4voqCQkEIZGCc3lAVxTBa7fpfXUw9jkoQe6rm02nLz7/FiNKMF7tgHfq9lHlXmb58yuVIF8eVL7G7BSwdQtcGLAJ1YcDdfRs7Dt2FKNTpsEOPZFaj9dJHwp6ysRXaqHxWDAafS5sF0qzhSmryIn6Mu0Wkobv08D+JD1blP/JtURW6WGgleXak8da2/Kur9pYnDV7PE2D9oKZZEGQWgns3NdJYzZ9OlUUsvNMRK2q6Y3TZOUJpV4JkrvR92eiNLFkNuu8rexHA9FsB/5nUIeP1Xl7Uo/7bVnqwNOVN7OOf6rimCQ/GlVIRzzHyuR1F899vlXyGaGFnKBtrzsU4WVZPmKiFxZ0thBbC6KzR/savsCMbiAxvwteqgaloVC7JlN7JVL9tTE7OnVI4oz7qS9JjrGLSsfso+hGp4LH2OtHzOHqDvURl6k0GWAmaKfsjDySujiG/+Y1YMzSyTEDy2HLUECuBWaaM5hcWlGSp5xFXaftbdw+imQ9bB/q4u5R05TC4iUNXodIxiDwYmlKst70YjiVdmqrf7X1UQ7YQuqU0Q2pe8nJsc9uxckArSL+PzLwmAJnDrfaJFFTVAvfGJOmEtThpHAAdQZ4VxAQEtPEbd38PeBnNx6RJlk/pvVR+JalWpE0xzESUOSHd0TehiDfc9EQFewd26FefMwBqp1yUIho2LWSZBlK42amWjMQDSFlk7SwZ8olmXzSPQe/YLdeQgqBiJgegnQ0tq6QiScdHM1SBHIaZ6AC/UlaCwmUAoNK25Gy4ZeL4MHcKa2o0ZTAl/UcMhA1+J5nZvq2oWg8cbaaPDhTgbScWoaTxnlQrlP13+GBzTfpJHW4LBlQEoM1sqHczj8zKEO4LO8i/1fjXZym3mk/fSS6b08adIvHianGrX4+j6hTCo5NZAYor0vh7d1KALDYkQj3Uudm6cWGlErwUw1EV5Q4LfMN6wqCenCnbQQBzJMWjEFAao0snuHh2cksXlpJwNrmMTdGePN5Dtp/7ToKsMC9u2GgjpqSSKxgnnD2uPUtXhfSSGZiquZWOLirO4SWtK1FGVHcwv4kaTw4+AJse4rrTbiE4CnFPukEy4fp9XA5ynrzcPLr4qiMJMCasXZuGvZmbujfPF0W7XjzSEJVlC5xMHeKK7GOMP8aWc22403KMq/ifvhUKC2V64+8jD4loTci3aDftHUdzu2MaGYSls4Atl6SSDdOiJEHhlOF+VzGPKXzkk632wU11TA9/u+yWlUkfau1TsmY9obrvn3d/4Cid8IobYy4WLOpAZSpnI/FbTtRR9TNm1PjF7rp8nmkkBLSVjMHgaSLWp94yIBmZiHpgzb6CCG0FFGRL+kFrMIhku8OzjIiFGM8sUkEtC1fK0ifpdANEkczhdudjySbwuLEy2EIgpIq9TXMgFcgJcqabhwQ0Y4rS9iZozXucMvuMW00gRi/fI055zTFYgkxIhxKgmQuapsBH6vU28Be2cNQvHoIgoNSZEWrLw6HO9aEcLL4Egwm1QNHRwlcCVf6XefxuTfvRuFXSxOs0ythdkgOPpD1taHvE9jXmDjl3FBOqNpP37oZHyTbELlUUSwk4oUIX2wbwj8qyvEaBspZyNWbX/9uYQa9wuFHNpILiy1QhzOeIks20ASH6446jLeN1P+rKSfYSCgqgMO62aavbSTSN4Vjq0O0ZaV4mlLLLEgx11XZVC+i8mGDVResl4Thc8Jt46NQ3R32eQQVCdn0R0lwYq3bK+tAiXhav48G8thlsP1YjBd6xGaa1PgvW3vUiVLmmkoKt0BArATrdWpRXHS7EMHDzmGFCceoSkRRvlQLr+wy9no2WB3JkgXASd8i1eZ1lupLOJKRD8Idi5bS3/yocHIJiXL6a39JyBZ26mU1F5ejLvO9DbxMt9dIAi58p1eQ1dKTLMo+uMnnYijlxfK7OBjoXCE4OOmdFVcYshjOMlYe1ZPpbJkaAinJ9k3zwsuAtUsmCSUstGhObMJJTfVJo++MjPFMWpXBRE70B1oegs1uBPxwnXrYfytIXuEkmW0fcMUvGfz/7hmaih9+4OZel+AHRkERaoqHSNQYfBLELDaRfGVCdBJXg1+6YQjVSaqP6BdN60nVUcpMQwMJ0JvBtndxUnjOLuyRKDGA6N6mMsa9eINIA6iR2D0XLCDVfbHmCPeqDOpDP+NPcWr9E1Dxm3BMJxraYAzEfUsOgACABvCsqFbBblpJxbiWRj5wpEPQzqOxyTYsJLqABFd3lBhwHBXLHwJhtj+MDOux/pVzSL6uthEOmUVVajUmPMgOn/8P4SGk75UGgeGbjhFtLTEcvDXJcZ1pUTGWtO10rg2JR8w48gvMooVI+TEpjwWorZp7Bu2wz4Ylb0SUAbcp6vx1Mzvs6Rk6hN2BfaVGVaTMaS9udUzdzBGxohfF2JlpiHDWUveEVPFEfa4+Yv6EZomSIjTZw3TbRpnieSEnSOTgsIBvCuR8QA0120kLe3oVkLe6HFVIMtkExf0HOvzj+tabjKMiMWZR8quKOYqhyml8goICZd6Jw4mt4OBZN+IMO6teQw/iwi1vPyhNVqBdX5MahM5RVTXw2zJkpTQ7KP9nHVwiQ4jDRaGiCzYm65xrhfoa6qhDFP1HQJh432a/A/geH+/V14hqPm3Et+GVYb71AFt3RKlMKHpCudWfoVhekQMjRuZ/EArawxYXaaJ7bVo7Qa0tOJGjk48XUWgn3IbmMqHwDP0pBY4BAQ5WtZFumWGgAfWNYwrUiAZ6CMZ7jxDxc+Y6WxxNqcMBovm6dkQgRwtgRNHZAWI52rh3c7Bzp4KtsLdt8mgs9aM9pkB8fLiWcqql1WiyjtCXntSEA6FBBYfcDpPyFvKQY/weKbmHJwum4ZNQLAiVIYHFYTJWAyVbJqyydCP4Cm769HjEJZsHhnnTrdCj+RLlEaJJit8S+LkrqGWqmrmTM3hdalR8T4X9lqVCjkQwHCZTgRbrWWj0ejNIppGtzLQVCdY4jeywKLsPWw8/GD3M2paq8XFGjsoxGIsRxOgN+ZSfmlCwI74wbUGUaQg2zrswY/7uUHsZv2RoEaEjRTBQLyAvP7BTKVJd6auzTITp1Ew/LGJSBenzXS8SsWVmlchSoy9zz00hpNFCvwBtZu9RteSt+tvs6fJQr1mBanZ1+g4S3gsV2qlr24Zwg1Tm700O6mtO7x+FCzQ8faEiJH443S6Q/Jiwm3l8h+So0iZq6wRDHYiTz/qlooO5GRcEWflQ6onwwX3A5LP+zgrVLz2wJUQiMzWq3v1N93VjhaN4lOQLVk8BpapOxdGAynAIh3TyfKLs7JSxhklfZjrXsYBiT8JgMc+InFYSMWhpgvYGm57iosqrNICb0mIcNB3LBJWJWbUCgVtN1qDm22dAfLm/H07kdvznpCIYIFGEpKLuNrAzAXBJ5H0I2JRvUGhgJsbss6C5spmyUdu03rOQXGqEXrnoiVY5zz/cIOrQvvmfOt2NUGtVKvcHFaQTmErIQl51iNh1wqLhQ0R/9edUpSm4vn/7017QrcPvJhf2f0bS4GI7oT1LMxZXf/Hny1LotorVPDtlB/dZjomyfQhdqb92e89deadGz3In4UNfB+VJ2h2ORFEySx+IjgRd5jUdWG9D2wZNgzf06R45E+DvcuANKC3EYapDdFAkhlbcEs1KJRtgEWViaPQrWF9M01x4/0xc7BgLJl390sGyfmw6MYc96HO4r9hGfoVkFFRu9KhY2UNuocrdZWlN++55joVYmpG0nq/ci+pvhX30I1OYJrQzG53XNZRcAlSjvjpeNV6qf4dyNDrdFuxev6PV1/qxUDbBorkHSY76lhByEYIKTuwsG6IuEybtJO9Up+htM7VOkur7wkq+XzuDOM2SvqMlJy9U5MOjgZbd3xPXApat2lrs8bAvtgNmrusmr3oaxeg5MYHZoMkIciwxDQUOgdaxpFIoymqfGGtFJVKnFqiw/NrQt9yRumLvAlgz1qw9fRnl690PRFaOvDxTswqNyx3bHgxDvdtFQgpplQIMPIGMNVRKIHb56TJv15gKBagurRJvnyAYeEQJ/FH5SLd806uWAsMD9fuLm9GufML+0RrrxNhC6Ar6LsvLY7W14guQ63nAlZqSjV2SjKi+sYonQNPN5W1EPcpFrXcKEFqMiDVSTFAeDk9wRS+8hLpsjHCwo7UmlU8JUlPo9jBH7pGveBXCyU6MFdTSPgFUSOiHYimNv1jqhxNr6EZBZr0hF2zXjMkamf31+LPh/QnCNgn2JsFeEQR+8q0TueUEOZbb1+mUMj5pn9hkb/fmDlNivgZAFEJCYC6G16YlQq7CYKzk0u328Es1LZ93tRPVAfjaU1MNXS7xDYbu38AATxqcvOLFWYC7AoC9+IbqQzy2TqgLfeuI/PvgfimTP2RTnzhNq7m7InM73K3HPyD3bl62tbCJxMpkWQCNg1D9ntpJgxtdWLE9FSueN1v60bBR36lsA06v0HlhUggqf1+K7aMKhpcSiWbgTOVTkNuIhlQ4bvGqsQrUuc28dWrrpNZuQCUNH7Gtxrut83pOmoSTBvLCwn8NBSUE78GNLhPC1AX2c/iJwYfPGIY1M7U9FXcy5Y2JTYV9qRy2yohPIAXaBsxnUR1MCJn5lmhcRtEf0IA6tufpVoaMAmzF/2EnYFs8HUEscDXhjcSj2ks0rBq9J3fLBXNRVEC+CjNq37Fpd3v14g+iiDqDPBIzT/UgdeucrCYTQTC+wQ50k4kDta50VzWaFJit9uixzaTDUvef8RBpff/8Ep1wu/cT6sT9Wt/9DmkqtgIQX9drKqu22K9bL0LYrBLAVOeJFwxVmfYuFd66Xx2K3p3XYddz+cAQq3b9K/uiqFokF9St9xy3iQcop3A0ZdhARm1CoIrWoNZhELlWektvdzmyqQII6MCJhgO/ZyVoSTNIctpW7eXE40KUlBV2L4opyYDOn/30DdSMkE3DYddbUyUNaBkapFD4DyppcFV88mPIjW4+GU5coEZVjQ8kS/BTAgYRpy93I2E6+eY5CASEoFDFlaL80G1ejUy/jGdy4/5ipCbzjk58vgSnxEESAYA6G/xpz7q2QmIi35IL/MTxaWn9+p3zsABzVqlI3B0RdZZ18W6kp9vGnZzR7itIqJ/VYWvbIvfYfiEPVPnpLqLnrkkR/YSmPbXyhl5mZ4Fq6T5r+CXNFKLVGkH3cUlv5YlJtjzlshtv6Cpe9g8ZeS1Y6w9QM9LR0Hm77h9cAGMlytjdP8v762Lt7taLjZPTNVI41ZypgB2w1bdVE7FQ6i4V6Kak/rKBBAiBUVLXma/sVEHQ+PT8wAiv9dYjXINvtFdNElg7igcsJ9O793YLErbLwJt6hMnm7GIGsGrCO79AvFaZ3Ez3Ui6iJrPZXBpUzlsCCpnUPOprTfk3h6VR+5XTnfGm57qpHbsicImHcvHxXI8p9aeLcc1cBmmpUBPAoFlBM6tRIUEcg/Tm9MrMD2hGy8sj9pHU+0JpGzQCowbIOg8e6dHoehFidh8b9B1COuvZAXfILvHGFnArti9ZUHJvZplRPQf8YnzBYlFJDZmwTsSeT/W3432zpPhUrMppm6R3gAfi0vfIVTt6ZbxtlkefgvO90vxm6U18SOpvel+bh5ThGVilYXropcflwiVBwdtfdCbNiVPJCyGtG9amrSDTLws4qSMg8uM6znvya8nZCjmZqwoHe2R1OKOQpZbwJLFECI7OEGkTSEPCRl3ZcqNUVYDpFzSpL7TrLIlMkEUSER7RDCagg2lo3WAGTlMYgJ5aDCBQ65BRgIvhKk3kaJ9Lzn9UjSkfUw1eGkkvxkPTTTXDQa9S+dG2T38PQZC/DNewI/xBJaKpRyOZXi6cYhlBFKbVWzbPfCi4tQQM59c3eeL9G5yC/toZ9RMAVhVuzhlMcoY8gbIy7r5oQoSu+ONfE6imRTy2EAFs00mMwnQl2129CFhmogvHSoiZwT/9PxSARxMyvtRcZEpW7JHe9+dQzlFSSsWkN0OoZSc2Ls90DDRdkyPUSp1x7CZtCAgTkkHPQ0hdcEe1mcrqk3K8liBL68ENpuuUOaB04rLslcRH5Ew+/HvPSUAdbCU5M7NtBBC59WlSmEZ7UuIhwzzzN9ZW6Mo1UECZZw1sAWy3QLKWPloHOcSu9wHGceLk0jwrpNjqWNoLzU22iqonfR4MOMNpWqQFTOQouBaAo5kYtRZtnAHyygtcZ0kTJfQBZ5GybpKVqokcCKVgHr85jAZ+v1bGccIHltWWAOXqj1EwspUCqTGijZc3Cmua8yXpmOAPjFdXkmb7ru2bdZfKOBS9mXd4SxQppSaRPAPsGFI3r1PmL0Ly3ODksVsQfE0OenWHEErctX1dCuWeEX0KxItsp5q47dyLqyTfM3KftqXUgePYijS0phpQneaQzNysRKIj4sYoD5g1nRcQybTEcWg1hc2EAZ0V+pIXAD7ZChNzJlQ/gz1nnacD2aOukYprXyRFJWCckA7K/rSwtaGZ1WzGJkInMezlzvT+9SAUFLz4OGJifoP/FI8w9TfDB/tMQjKSy9RCqK5DL/3m0gduXSv4UxcbS5Rdc3390lEFfignHOhPDMwqkSyoRyJN10UV7qfzgaEOtItKXwqnogxoA++YfGC3OmQB7l8sBvCvvWIgdtkUFsDhNEQYgLwkMQFcvoqWJUGcpttZxAqvQ0+jxsvqy8W7kMUqRgIHsLpZG0AukPv5IjOi0VhGwqeBTohFhBy5udLDWR2bT+GlIDHZOMpg80yr3qQkNEJ8FLUH/DHLD5nVxFkpyNR+pFx8w++lo6qw4IKuuAYZq+Ks7BctpjBD5TszaWmejaXGDSSmxsAvZ57TQGsdejbBaz4jvR1k3fOX546Hr1Y/Z1Ls9bQOe1yBxHO0NQRzOn6nQK29mH8Tf9juf+hVvuHLsRhh91Mgg6Xu5YC2QWHsrjeB4KEhVqacnFm7rD8H+muHqOkk7jjgXbFfX3R8YfXoWg/ls0p28cZSBIN/AEM47F5ZryAJ0bOkWq2pL9ZjreJWMUSB9O/nvBhw9puYdraGrwMxaLiScDYaCFJwlUgDKKj07bKY5ul6CcBl2gM35IWg7BBKvZueJFYDYylcF3PyCqiT8FV4fWd2t10iOCU/7rwfdbTplo4xdiRaNwu5XhF+UHgKOi6yapxoKHVEgjIlY1sZEmCyMUZdhDs0OVK1/3PLbdph1D3MnmKibRS8KwGGqLEvg3kSyTBx5e8Od+DVyKa+4jKnhjl6S4l8X1d1bU0b1dNg4SL6ytI5gpAjvwUnQoKKJAhMVpViDgIsEP5LJf35Y2oCpTe0qknfbTSJ45+NasYpRHxWy5k6mHxZJnU9xte4mDY/0tswBhs5N+WrclubdCcZn8tTAmkIdQdi/o4CN8/DcrFAkmy8VWlq0XO/dsmQdfVKnUCT3DldX7+0sGRMunTUy/WtiSw3y23TcFAXMil55DfeIjW9BlHUmkcLbRH35KLBk+TeWtpEgUtvapKxAolXwXmkYfSE23g88RFsHqk30hVtxaJtYoIkeAXj6oKw2gw0c00K0vaTXviw+YRXuaorb5xBdOL6U8yZ2U3CPgJpS45bjNPaxeEZTd+Nhk8t2Vos4ZjQSYZXXB7aEg0MgwGb568WU9Udai2JWFrd6vvYedYa+/q3xFgMNnNFtm2skHCAkAuCpTecY704ScbXdrnApdQLcjifAXeercq0hloDr9qVlFYxUXzX+LcdgHm6qHldLjcATrEqXqcV9BtOd9Dge9633kyQN2SYZLQlaB01HMMkFCMO2+lkU8T59uKIIpHQpuqmNzQIJfRqc7kNncq/VVCCwgEMM0ZM339XweP1ar+ZaIYA1OZwod4yTebjw0J4MRpbaRg0qXUgGe5h1f7/60FJ+f+NuR+ZG6IO98FPz0rGNbENnNelB0QP2wTa30KVnUupCqGOjIOfHJ/7YSkmyyGFovFED4JObEsgh6Jj9NLgj0KXdBQ5GP2E1c+T48bEmBEDPTH5tH9/j7XFGC78Jc9bgmn8+PkY94+UW4woABqwgify0eSIUm2+tHA7PJr5gDPD53BHF5zoe5sW47NZHwvefC+Mo86YZUTAmts7BdPUyHOb+LPIThFtG9vRJ1nxknrX1bHWsDMTGqYHSUP6gcqRXBzCDlNVHsxgtSPCm+eQ1SS7uXcN3jamtenRa61kLMe7D/bmMq2fbSyEpUTlSdaUbo3WX6D8x7qE0Hu1pIZoXwr/DfDebeebwky21v7x547W8oYdh8fV4CdJMvJYdbN062jw4yTMTTYGrqdrqMT5yKRHrvYgpsLR5ek+NHDd1GxnPwRxvddmwyX2eb0Ybk/KKcmIJu1aMOKMLgA5bhsPA1y7REjSdwrCYy7cB8xm0a6ClT6rg+TayJzSergBNMUaeUhgrd49GzMUTAk1x1Z4xARrfji9NeM8m9u8NSPBibaBvgea3y7He4Mn9Qozad/Kh9PZqhJT+h3FEpAQi2X10pY6Kg06VDbNPCtLTHEWjj1aM6KJxx+tGX9gqE88NoUsphHFWX7p0/CTn8yysWb8j4kDckbn9uRwGnpCyc/hiZi73CmHeXaHlPzEe2vGESmk0EowL3Zdluznj6csZ6YQv2aRvWay+SiifvXgszz44HLhaNDAAAz+mAkzf5cOMTPImK4j8340jJP4eAi65jOV0x2dL5cbRyvanbKSuCYalYTcmxf49xvA4whgjYz60rhLilbhvC4qx0U2q4c+oliXl7z6KNgtIDgSC9kQ6C6e5CFpXqz2NCYaxM3SET/yz7QSE6DNKfp4A1OHRk9VkM1FBkECaeih0pl4JLbe4xjuhX0CsbUfwm6GZ3cBIfPV5NxLYo8EoKCm7DfxRvrNWHVpeVnzx2JXY6Wgvu1Ld7hFzQlOE/sqall2VnolkqDVm/aWaa249PEYrBqhfU6T8fM5/ANczFuo+dGXFFD10T5SpKFHt06EkLi/djO2g0l2D63GzRjsS9PY2IxeaW0pHBvlov8/XaMefPiJVfIPtTmfWCV/zXBkaN+sSsMQLtkcIC+ZRdIjEexzRmdvTBpmSZ7JttWICMM2xkbHlV+IzSbq0Zu5Z2v1dAofbQqKbQmiofEGAn+ZLilmZbs8aP8Xywz77Ozz/tCQZjCRUmxhxNzg/okpobgetWgTg/rkZLriCHDADEeQyX5ughDQ52A37iTg/sx7CJ3+8zh9YVe8eLNKu8emH21ENushzbUxZVVTRpfGeao7gBRgml1j9VvGkYKoaYcjLvZfuNMU4lh4NgaLM1GAyZrB4E7d6Dr8BWx7gsBNtSqmSKQhbsA+qBM02kXY5nHrEvJbGPl8qCpEp7Tmygnl5le1LQNVY4UOmdEOV6bys3UQVpEXhMWuBnms1B/XaN/Yl+7Aw9ZNK+A0EUSbzbGpjTfvol15STdl5BSaUyhzZxjrTFsgDFX9pqfO2WQCjpTJ3WOUAyrYbZT11w5cLVbVUg8s7spKNarPc03wpz9sfkJ3cbn4A3ptphBKFMxhVdJ2kgFfH+OwDXLar9yNz99fXNyFejfbHbc/1gBNSXB+p5lGxR95yN+uOqMeGn6I26bGIBLSo/Sg8x+HZRUUtlnAWp0WnfS+V9ef3wYMLVzHj6sRhqpyNeSQ44VYsXwc7Nw/SLQI1VlWfEmtWYJJAp0i9ZblB0g0INtpPWXxq8GAuadoknOdkajaYGrFnJ0G7ve9+5rEGI4/MvlFfzsgBiQcDouvddwKOi7Em9Fgk6b1kMZDq6sWm5t5P1H34dyIQiqUBlplylI93hB40g+8OAjKZflWkdlDjBnTJa1UMmLpSGQt4aspho6btgZZNiqnbUxN7i3IVnNRShBK98NjqhHX1TN8zo7KVdzCKbRNQdYJ1zYSsZc4luyJnOxAPFcTy/JRMaYUkTqF1wYFKrWoNnnQNJbBXRlpJoCHjPo7WRYgBHNh/EbX2R7kcsDBzXwDql8fC3831gWKF2ppLG+p2F9fq5ex3FNm4KHvJdFzOwdQ4rAFZ3ajR1xpzSndo1ZvghaP6AhyUWc2d7OYRT00KKMMAiPzVOm4tXmhgvNJ8dogmAFlAeLjC/rElfhQA2Skium6+ooaVUaz2aJ6y9NwTg1r6LRTPeC78uHu4KD0xZLZPYe0Lbg1rb/Tw2CdOjA8Eo9somWjVAX+Qkac8OxZtY5+72toBeMqLW9zBe562vvI7KuB84E29tK97I+BsUJNLyKD0tmUVSbdUXpBeQCEsrLKWn5SEk7NN6smmez1Hu0gNLUXZEgTolPkXeLn1V3HPF5X6wraXMMW9CIQty2dECyk7E00kRl2NQqnNIHnFYjpkYIFTcT7ag0l9gxVVzzoM9mTyjB4uZdLjFel7UnccP71HG5hYamA5Of2A3RWC5Dosrl8Pde65l8dDg4nIUcgAgeowCvYxaJZ/mWS8QCw190AMGwOKlcNIIR8AwCqlVUI0G4KcRshz9ZrDQBEf7jEITjjinI/CQgWO7qaBj/uCx0+lTP//wzWGt8m2ZOYia1feubpls+weub2NjzD+pmb26hcEKyClLz9heo9Q0yQdiYviqLa34nAUOtD5KjDwClJAl690lM9nSm3cAF5cG3b5ozNDtqZFGG2yvqyDKQd5SmYnXBRFdbpB8svY/5E2PKTpGO8o815K0d0Aw0w2Mb2PxnDI+Wo2nZvCnEjB25Cx90sOW68erBRB5PkVNM1ZME3cfPbKh0Wtm6HbIlLwbhZikPap8UQiADjBgTdrYwvuGwNPQFjmGU8/V/VMlM3rO/KY8FHv9YeHAPCKM7T0wuig+/3+Tt/TMJ8aNWxibwxxfFP2YVXyYRpe/vgaqNb2sbFf3WXfXgQHb4MifOWpqPrD9I2Lq6Nlwd6CKV8Yujye6IVbfdlHIquuCZcGuODYb4r6LSw/jLr6cH9SV7SGjA5VLgTWH8Bl6Q/9vDz4MEFQtRv/alDcFytpPAZYABeLRs6rAlouwDePSf79e9ehicsCWdNaxhflncJNWjig67k5//OHxN8iRkwAIX5rr3U41mqcpLW5Mdf3liBJk2Th7+SbVy8Zm3jwvGpy7MsMQcrl9FOIvAGSFVWt/N+OGQYGWvkoPrZOIUuVdLrPE5ZJhsJGcRJNxv1d4O8ounmhWYaca2k8BYXVWDBBDsQzq73+8udWjP+YKd1nvoRP5cIaCyw7j09AGlUiBwtvNh1nHV9cvnk/MnXJ6Oz6ZOrJ7Mn3ZPpyf+7DkmE8yQ/oV/ZvWnpzcrevKuAyV3/Mx0MOIBWWB5nXp6y/I4VM/XdLsLpHsi1SuzJ9mR0egRdhf9rNxrXxtEqgmHClfMzLf1sqMd3rLgtbSJj5o+T7qQ5k2ngD3utbZZLrJ1d++YFUA8A9q1rusLGQGL+ONmdnJx5Moc/7LXuZEXJxgFl3/wgmwWdfXOX220QmC75V7N+oF3+zuyJT5THQ+3sMny3Elzya8G4yMNJ4nvfjkt7Ey72wUz+pPS5H5H0uLhOY2EqyKMtTksoimJu2Hw08XaPc5aI/SAnETQEBMWSx81cd418vAgJWgYXVrM+nu9VBHuYeYjiwxcNJPGDxdnNkxYLcmuGxVgfDSydErRcvzijyUrAsS8rfs7RzKcqBEVZQB2oGM1l2Jxh/UCMJeUO6g3iM+AMWG20G6BSAt2YJ+Ngb5UYkgod+ffDryrs7IOuCmxVIVG4wcvxNucOYrw2a380krGEiaXeO3J0R8zaH/RPdHnpgaZu6uKgMtyI7Q9PzoazJQXaMo1oG9q0sHcvbFzfbER7et79b6DD0vbxvRd4KfAokaZ20NmlU522kNLY/DB2sE1xx1C4Vfa0Vmhg+IDSjkKTaqAHNLOBULdE++nHa8QuUrEO9SetRx5NoumyJjgbAHku9K9UXl4rdpHywyYe6F/Q2dbioeRZ7KW+sBuTNl4Nhl8FX/TOpB2Pqou1/DhmcQ8SP/71dq2/bp1QY2NPJSZGQz+uCG/0A3KKn3RKEJcMD4s2TKqDVsQ5AZMU5NvzPf7bFpoaG+eubp2ZyZ6zl5HMSLtnOIEJBSsHvNYI3R4ra91QytVoQEXcCbsB3rWxlhgFtwj/jNuxMW8I/9T1v31VcIOSrcv5+uu+ZScTFTOiUIV5244mu1VsFbWY0fWvB6h1re//3vjSSYrMjEctGtc9FBacIeoZwkTBkHQ0IGU3Fy8FItQg2pRldkNl3MaYnfIbW4XJ9XtWb6olAGFdFoLn9rIFB+Mm/xaRiRbPeNmq/RXR2yq96yY1Y6LT+KnPeuKtQcdYy2fhd0pnidNa4dGujc/t4DnQ4XHKNOl/NW2cxieXxjtDWjUtplSwRLkeFOrjKxqlv8TJli27cEpxYH+0BffiPGcsgK1dOoKozAvLJ1Uc6oMuKMDz9hMVA015TcrqrBf/6M1YJpsZP/eQeyffGHdJV+aPTOK5oNHvgcqdbUy2HWw6NUVYBtoT7WFZEGMDhknrEmhj31egY56jIlJR+MY3VPI7Qw9SMJ1riRv8KzfV6fz08VM3+aghUzNFc3nLa1UjDHDmODi2VrAWC+6zEfYVdXpVCuYXj7W5mnr65+fIwA40YhNF56YTZqCIAthC4A6ku+kJ87AbYcMzNAhqamALbzNmvcUox5Knv9wm5nKot38Ig0vavdprxje+Wnra3WZ6l716p6z1Iab5RXL6yUxXoXkmeYnbdPLqAzDr6dxK72d//Ler0tMxi3sDBspI2uqQdFQ+TD03hThzdSmA/oQ9jJZSnhn4HxRvUgW+L+etBbdzV1ZGloOQ/dlZ60Oej561HpzY3kPXuoK1dormpDgcgNJB0VOZGzXwbBwnxRhE2kNFCSnuzAYTaK5v3Vg9+od0YaaMAJi4cCIAmzmhQtAIivN0jXBkzj0xqGV/REpsAxDDjgANIO77yL4cExRpFNzqoruHOmh7n9m1tVFNduVTorjEP+MaLCK1oJxDxHNSXMMPHgbh9rNUJCj7e8+750TBwoHjew/Z8DYKwfQsk88/lZbbZ31UE0GYCE5HiVQmTKVT0011gtgfjQWj6zRp92JYqmLzcRGg6oT+hC+mWnq2ciHCv9vr65ni2k7YU2y0WrUPcsDGQrA1p36hHHBAafEtI6rgdvKZrz9LKZWM86W8r3Fci5UggdFdYh27s0SjNDqMeNvAwtG3UBthzw8YSzMXqm4pP0iYY4fm8vIoflm+IlLHKiSb2y+XU+ut3NIgqVEkHAPDf/LIVvZ/9x/t9uo9bjXQVd2HevWlsylp8dhNenRDtJuFbCAsaglUbp2Lz4rknUdSguhr5hvCu3ejqHTxBj8bKK3RGcVHOMylNMiVW9+pt7Gl9eC62w+e76kMjyN9hzu60gjOlgJKYbAhpDnPqX9IJO7LgMBkZCLMXZYHrNCK+BMxoRFQNimSw6FnciRG/13GxDr0mi8ToctQCQW6A6E/kcKOKWRdBGLnyAGkt6BvqY1JIN09qrvPMGIQKIme44CqMMTEv0vAN0KRh/O5UVjK7m70q7u/bkAslKKEQHV/yo8ZGeHqWyIKtmmO7IRWIRKi/aWhO9GwI610w0sh/uu8Ina+nxPFUr4e6rndaF0lUQ/ZESP9rONh56kVRL7WzbNaTNhQ+Mlc/dC4kxLkN5F6HduRgJh18lS9qKgLqgatiWKUqvH4kAaqzdtwIoKakBjKAqKMsMDhPOIHJsOz8FDS0dE8mBW3g2PemiI7IlG03gC8gFADATI8AI9wsS2fJMEsJwJBM1cbqL3mPzaUuZtowD/VUvCCpRFLVC4cvJQN7QXeYGuyjFkDIZHABzeIKWDr1S9e+yFnDIqXAuDZNE2YqLBEUsfmSufGhY9VhVrdhWMaFkBWvFDXLU+wz8VSsZihlXIETTd1UApZGRlOtebmY/cMfZKrsXe1lI1ZGgg4TpoM691ZDhiOcT3Q/T62W/EgzypCtY+U5pXl4OiGE4k3uZe3JQtf7v9e5UgseO0eQWcIha/vY7+6En5zd6iS/q3bGagWi4b0t4YrsGHldcAoqBmrJL7oilx7tG1AtkTIVAj7kouNVZNrV5H9tcir7Y3CjQj1J9aoTc1IWc7Xek1VO5aOM2gli/SqTGbrZ7zaWyLVBB6YLuuuc2G69tAwdhoTX9TEjwNH7ZEmuB9l+BKZzwZ2ff5EwE3rRdMlhqxWUFGKj2ftzSFsGMKShAqXA82HhzmVcHuO/KHBlHejPpuJ0iEuP8iMVTqiaUhtsBEV+SZmqi0VCMk5foFME56PZfTAZWrV8OqG8BLFOtgqAbyACRI/8JAR3T8P4KwhxnEV/+MSEMEUkHJLTq3QaYGRReRymPZht2as71OJFmLuTqwA9YN/7+o8IgZI9CMhRLXuifd6sRnJTbBCbU0NFFqZjK5DvkUOAtrQMSIffo4/d+z256+SICFg+zrnIP7xi5Hr6ID47pdBfO9gTeHVcP+fK1CgbVaudtonLh0dz8IuAfvT68DCehAGtYLWcu2sE+aGB/u6mMU+QHrALs578ODIe9vJH17cPUGHWV1nQYlqHz8wjHvALr7+9XPjIl7whhao+IhcRwLOwQ00z/t2OYst4F2ZxnaisbFQcRE65VAI4AH6EHwIP+CH6AOmltzPZj/EWINDIOQEps0+cBpxh/P+T7Q/F7iL+J+7/mcOkMrZB4GMIZH1lEmyEMmgp0w0hYiHbvkqhRu9ZRh3VhcZpWQUeAgc1ZxhOqquM73GVRckXndW09JwBDjgvCoqLt1SLbK1f4v3RUOz7EShai5z13Rv7bqU1rFovWfLWB0oJO0ZdeHW/aNwCEPbhL29NZCzyR5pcZNuSK0rKqzOOKdQFG3pRSD7/gAvYxdQEHrjpXmzg2cqEIf4esTykv9kOsyVfNTD5idIXdOIOmmwqJGicvqkqoYEZrBMzkhQ4XFkSBhFOnGEqxSFNAUPnNZ4GzbBuB5owZILs3vqKqcEzeSgHosaA4rOLx5J3WVF/WpyfRZ3nXfTnpGEvGsyolOF8tg5PXG4CHVjV1A0qS1HU1hGTGzq++4MS9Gu+BIxgQNd2yHu0kGpIOeNHrv1gaRZTM8syh0zQHvcABntYeUV1TsUb2mSACMP6sUf8+iwyET4q58+0ngu+Nd6RjxrtC7xYKhs8IeSqaJUqYA/6LiB19Te6Dk0vYylYsbMg5vhWIDZHCvE30E/BRKpEsi5MVTJ8+t5NJsVDO5ZlNFtt1k+4XecV5eo7B6NzNv+l3hWyUUnI7UcFfvzoKxqdGFUQraN9CsZZ9WF4D2iVTQdmqrQpStqfpXDqW4Elvus3qASIYdYNErp+uBo9R3av+CYLc+immzJTd+NOLKYBe8Ps3Ed2wa54wSVsBqzfuChXM0A5t7syR88fmpuw6Z8p4kn6kJlcBqOg5Dp753r5u27KDZTXwJP62TQtNe/8GX96fmSNWdQQFtfQRdLwrG7kruCa3ul2pAR0mV4Tj3yLQg91rSqKRgDLnUMQ8pIn6FkmdXBjdoyogwmAQR133KGFjsuDhQDjb1f5/R2uVcubwr+hcPTtKaDaKFQR6qwehwqy1MK2FQKkV0WpZ6Iu/QStAWPtudOhTo7u2Byq2vH0EYpw0+/z7HEI5fUYF57RJ8tSZ5gXdpZ3LgSQC7CjGPbC2h9y79McPVCMj8YZeHk3NydqYF44p40Lk9wAUMGBiiHgzLybEVVMLTriV6jYRkO8jZiqc+1U4eI6OHn/dnabyHcLlPY+6kTlmDqGGvko/3MjNDj4xJNfRGdvlaST/Gk4ywcMR32bWcCT+baxOSde0IDyifkd9a9gE3M4K0eqzJc9g5iwqOwTRrPcMglWSr3t1WlyHzqyfKL5zSAJ7MI/L5VUWgf/hO5zPEcn5RkTCci4xja7kmbTTOdS2xFikQkrBD8kfLjO/+R98RMMNaZfFD4rRbZ/hJeJnaxIb6/3rZQsgvIAajTRb14y/YkbGoSZ1ukbj02d1Qfs4cWilQbRE8nFRbh/rR31NJVO2+nllYWBzGX0bqN5BVv7kYlMeU0+VJ1UknOHj2Q2uJf2iO9kzIrmGgaLLPxLu3bZ9cCYqf0aKF2TeG8To2jnamO/9HuaGjRDoK5RUAZk36ZFyKaZn4U1IueUE9NOHrWtw8kR01hlVFPIncExtUE3UoPyooe9ebwVhtjMlzNgStwAMcA6Km0DEhLhuB6TfT21kM7oabpvdekz6gS2YOtXiJErTl/aD/mQNRKmhMAE7/wH7PclYQGe/k1XnT/yjiT/Q8bwjPIjwl0EwgrX6j+6Mwodb0KPuLFcy4T5Hl7WaSkyIooZ+vgp7FWTHWQ30ee4sTfVBSgkqy/uOxORUNxaVrYJByuMB3mgUwEOV4+0X7VDnQNkQJAH5/RgkFRL2o4xFLRU4adRXkP+ySPta9sY6NPsLxcZDCLuus55S80yDrEhkdOy24qt4zO5vRUxx5L0JYWPeBzd5Zk/y7Ac5JASRXN4V8BSTFlhAWH3AqhiFFpw1qxfpCSbmxpcQUR9llbY2yBmw6kAa6U5jvQ1h3bxrYG9QNbRW0LevzqzT+CaXVA63J4UGuHOsC08eqrz9g9WCc1Fyp/5Uz3Jh/PbTpjfm7tKZHG95zyM+bnlp8xP7f8jPm55WfOz6k4Y3FuxRmLcyvOWJxbcYbiZRxUjAdSAKK8XtNBxdzwmfJzKgSBRHamZ6ZHGfbSK2G2WudElf8Sr2KTfTjPJMyVL3lsG/nPcpmhJBslXsWrOtowbyVf1ebjdpKPT7+cT/708nyijx9ljuH55E/n55M/nZ9P/nR+HvnJ4nyKp4vzKZ4uzqd4ukh/L74rCw5zTN7kyfw88pMFugq3LHjUwyMY+UHZSc0n6qkBV7Go/a0oDMF8NYc5a2dSlop+D2qN1Ox4v72y+zSDmNgRqDyzJf/gx3esvur5LsMfylKuhKDw+PjRjpeTCU2TH1cZ+VZCWr2VfYOmfgx9lSIdlQ2S1uPzxk/TdN0J6A50xyhpIXuOBJjohMiSkObtfVjCl+phsPUOhWIhsEPqiZbVBXYCAtjFUT3tf3gA2I3a/CZ6TlgxRXatEmIJjbYuxRS7+u4zQnl986PjHvX/PQOaenT1ZAOB3F3ccqrdb0mFBVEJPPwJS5cbRb686/QqJXuuApHKfdKakvCLvuoYkI5+CKaC8hTnEcy8wRjONfizr3aFVxUd8949KKTBMcFBVCIcfSSAtFWPW27Y3yqJgK4t1fzgl9MH6ENQdYEtk43nFF+QM2AxyM8Ap9MyyRYRxaU5K0sWr1C1N6TCHF+mToIcezVfGBKFFesFoNOuizz36vjZyVfuIO4oPZF5oEEihySXODu5RtH++BZc/D5bamz9dc4hIi0xp6sWOTZ5cXKZFYxbUhUr04wLQmAo2S7ag1ZWlybTflFpA6URKlpko23dHO4ZnUfSEXkunQQSA6dV83YSOmE6y+CvjrHbYU3SNuWtuViS/9WCHKcPHPz7Xvg4nGryMIwLuDb+yFKPpxagiVLKuepLvsYCsC4UiCKsxfR7vQ+qbOqdFkqopYMjrD3j1LiVB99kSbm4fblr0BrEQ/qWGUPbj5J1myic55zryB1KJOlo01lMUDINhQ2F6VsGEEhx4/IyDS0lEUXjdc1SfwM1AHGwAPCH9pfoeKLQmKeOWaBmmqHo0e6k3RPe5xUhGkBBkA+yL0kT/7GzcYNE8Q73y8Bg8BNEiE+6FWaBwjzFzZLMI7fHqSU5L0FKT3JiYjRVcb9TpHXEqDmCtXh+P1LlcVKvZc7nCgJolH+T/1TscqnqfShVJLTvIWu9b+pelTJZo4MhhU4TfX3h7rj0pBOpTwSRwgHSaO8YCu69kl/rHcm2lU8bI61ifS/cJkCdz7A6oh6U//mKVGvClYyyA1mwdUU6Q2OiarlpC3KNlJ9ydj0swtRztivFb96F889wjnrIIY0yttVTBfnZQAdZ358Jp10XmwJlaSMw9kv9UmBXBBN03JHK56q8Rmegp37yfg8Bbp4LhuzoPOfeC+DLyh3FzjhfnDtqg8LdHwaRtWspQqAajNYh0EVWBsVl4VbtgolmSdBqwV/tudqMi3EyKKTDZWRbB0sWHtdBknEyaDrsR0p2sGThlh0kGSfbSMPXvCSMtMOC3EZd/ANWp4htIYVCdog2gi5/eBDDFxWI6uwgxQR4GhTh6DTYtoM9+pjdDvLoudnK4++GjZR/v783Uv79btlI+Wfx0d0CTqM5NFqjNyhmiobyQ6tRcqyBB8HFPAyLxEIXPBwaozQMIqOyEVQPq27jXfrDXRuo1dK7tJWCevjL9dVdvqrLSR3/3y4PpAgDe7BArCc2okkdWCC0HXgUd+nfGahRCs3hwQq9TWD+/bdeqSjiSA3z6MgMLGJJSIsHlK6E0ECClRAZFBLvRBhqYsMOUtgNC294l9FZoA+K5C9/94qO++xDLFQ6gxSL0hgUz+/XJzX+FDKDLEJi0EVu/eUU6LTBnhylQZ4clWGl2SEF9L59kZNVBxbFx1fRdBsMSYzK2xCQAj/DYht8XPFQdTCxkRFEEuHr4YTwhOQw0nhCbiuAh5X5YMS0B5GGIF8DT7VrOLSH66GM0iBF6HgZtuQ4ivjf8xR09oxNVIxYj++e0I4hsyzSHabzmHidmK5oa8o7l5jIX+5iDzRH2XGOWR2m7iNaR7arcFZkpp3mH3IhJEdeVk4GTb8FcUweYZNgNdrOMolTw32nBkNuzk7G32l4j6ATup/eV7gxv993io5TxAZiqtIiHx9J4jtE8R9/qwmUf2H4XXSJZxSZcXonUdbbNcRxcu8uUVVDvhPud3+BSjiswB2jMz4bh/WtBRF5o5E/6SC3CXz+RGJnnaCMUA+czutg3X3RKWupeXydFucca657RT1OgXP4Zx4MossJaXXS5wQxvm1V6iw4Xq1GKtPIZOdRVZaEGHmcgnO/oJep522zkzeziO/76z7wETq04P1eslvGGUCIEEp0bEbSNTZxTzbyyVwdeqbgSXuaBYypcxLb3w5IeIS1ptSZnTV8OVVY7GmPdz+1yy5luAXtS8tTfiPnsbk+cBbSeXY/CEPtQu4D+co2qJP6c7flOKjVINVcrfuSN7U20Ou1Eu5UD4sFzVU9Ya2kOBorp3tHhc+rriAaWsQWmy5/OccI75SVO4js0xna/6We4tHWg8BSbye3Cwsi5M6Uhbcw7984n8oH3xEL8/z52Ged3k+6LUiQswYyOCQxFV08IU0hKwOfJfsgueFTGe/Me/2DLgM5YyDAGdQs2/HNSDpvaUCMvJ98yvQXKb2R9miYP7qKaxZwvmEnOWQPZBTpcA8Splbn6M1a7g1vPy3noOVmOCaLuwsZuli1p34590oHMbs3k0ijf/iwlF5XcOsmfkSVMighAZF/ZmoxtLdu6Z1FpwySn+E6gUQoEklGUaWcnEkSZ0OGkjROl7jtrC1IXjdhepUN58EJLyV08znr+zUvlSFufTV/mTWNDdDgtCFL3GgEj0mT3Dpy17E/Oe6Q3m3i5U5vkNUP4jqsvFG+Rufvtq/9wPrhZ+/kR7c4zeQw+dM/l04QvtXVVDPSWGing9SCPcZ+uD7cg6fVeZB6la0yPgjDyH+wYp5xqIEt5t9zH3YxwP/KDtCZdRn9Duis7/mr3lOrorLinZd5EB8gmwCMDk/hjXULQ0enoyunVAfp37E+vgTaQNMFGhncZFkXdCnEya5CzmRNV5IfNndnV5lF2xSHKjI7nE0D7pKbsPwM4DDzUPY1Kk3PC3Mf09XlW3HS4M6lUyK14xgOnQHxmApXLkvoKyq5D+7hygK3H5snPo3e3KY38zyv6UCAJlHkvoHefFdYawCtaPAHg/HT7Aifk8nVPQEKBOgsC+JiIaV8wZb84fOhXPzZIZgTQ/Ebvxa89vwYVCCoxwqKDBQi/70UCyJitoSnMk3T3pb8HKrNNQ7uTQcMVoTMmOd8/ljaxwurFiWAAWlBZwMBP8CrWXCVMB/VeM2wH5WEaJ8I2zT1tRNC2R5PxHYcm5l64dzQ/c5aBg4vym1CrRcTJY7XKUMINKd0ZdSzwmLgO8GPnNqCYbpxg5oJXi9Cm2qcmjLn34Hp98fDlBbk5enMLJxEmiBjuUG5ytkQgtixkAknFjEReEQ/47WOsyNFUDBdTGO19uydx8OEZmQxAkEx8m+fSUF1J+t8fDvYlrhsyxeC19hTGcMuL7jJ1876OeH2UZjlvnTi6hzmaWcbOP3azetNgpsCe3ypSJp1UFefoxSpOQbLn6B8UjJj75zIFEAnSe/dXgWGkH681R2vqDrkAFYv/ZWkv9yi/M65WuuwcgLmynWU82AnXeM1GhIu+kPoGd9X5feigo4+EZNcPetYjG1KxXk/8d69zUNSmo4/DKoIrTTxftO6v6DdfLsbKBI41PrbMTx+Mkxz2A7BI2t1/L986eYR32gdKdG4Rsevszl0pU2qdshwSioa21kfTfBkpRXMd4WBZ4ogWQm0G7yu/B6efxtZrkyN6r1JwlUOE2fGyT/LBQOwdO0RYo+uNz+IfdfchUvYsmUPBvRjqMgP8Yd8AWS5g7lgzxRhk5LoCfeRsSOKEuGdJsWPlmgkP0ZKrjzzNCLtt2PFJniHUxm9g3rZbd8aIun5jqlAP1ZGC8pRUTmQrH+0o120hPabF+zy1bdZJctpeIn59qZvP8H3Yq1tZuIp9YRSxA96dOIdZx6MjTskl2nc8/0h66jnm4u3ob6bRZ0ouhRl1R8lgmrESsSj4VLbnWrWnv6WxZh2MReQ3XSuRjACdjLTeXiUyc8RL6oGdGH4Ykc+2Aszw6zRxh74kMl8jOpA40wCgU63cxbs3WziRhbA1LWTRtAEWIBnCmh2rheissv/nXSOvqLQhV6NDy7jtEfKldHx+dwVLdkQzPM0ukf5TM1iZbS0kRdTfBaWULvefneVL66XUk1w/9AY+BbRkkEbbxuKgw708oI3GmOwYPYhUNmIgbJno1R1STY1YlUfkexcWgaVC8+1xTeTbwNqSslsNe87xWhSx58z8p1C7Ut1QLiMcuvSVb0Tkb4DNDdn3E1Wt8FAl952QZn4unoZ8anVM5CQ4qaTwZACfz/GsUMQOuo0b76tJQgAxXdoUBy4AVELdnIR/Y87EU+NUMPxdSdjbc3bTV5sONolH830MN6wBbbuuuBjaAiekPLH9nTsNSSpyMjxVDS/Hflav3QTxi7oIZcXahL0S5MWOWmn6b2g/jpReAncy8K6QNtrBbFXi2uXi15c9t2ceB2Dt6WkeMiLIkopu2CrOVpZIEifZ0Pjb9FpNyDqoZ6xET/TiGwjmPgdOCXnI1tSJiHncN/Npl7fOZinBN14sKrKHhCcZWkKa1nKB9cDhBrOqag3Jm70FJo6qd/SFsBDOFHoOHEQavvZ721HvUmvfJPHZdRtg84S7g7E5lwl0wcSYVfwtKre0ajZ1xp4uZulZOQd5YAbRqCX962B0iIPfo36KGR6iDtKUpgzyVdqDZS0Z06skhc99CmiAEijXsqnBHBUayrvmaNI2PUBmu7CCUB86KTo732h8no06gM66/aRw2nCKRdHB7o2l2xWGIHsdLQc096G4PnZ4A0g3iqC7oH3+YBmZRCrFS5Y1TvF9VmT6dix9rU9LUZl7ITSA0g8r5pv+OWSL553LY7gKYBXE42UV9SCPLC5JmwBvO2n3pSi1t8crYfH4ROsjH4DjBRBQf3gPdml2TSUFq1WhnK3a02hgrg6dUR/V+VrXklPWmGTD8owgI0gnt/UVcmWapybMaexSgoiJneEdZS5zxksX/rAyZULsnYHP+CsTCXEyb/7KIZ6arQH1FsIC7hNRSoWfGSLVHlVRb1R5zyfq0zKm6p4mF24sqqr9O0dAa5H3X27qBI8VHd63ucPqkS1GcBhmfc6zRkowI0HduyZmiPFyX114bunnHgyOBNktSZHVy9YcJJEANZpF32jWsKw67729n1V+98B7T+nVwrfxGMBjwtQ9y9VZ0/qAm/myUhroYeFQSCUQ7aXhQDMBZN1fsEXw4KYstpWdooOp9YkEsrWU8EH66Or8t+JmCAumRjwLEI5q2F0w7ZKgDcLYCnhnzgHrTD/vAAfrUotU/NoLRbXZLozd4qD+ucYp/af9qlQHS3aJmINRTJ2xEr/gCNI5lx7v8PbdhJA10+AtGZNS/RdL6DJuinKUtFM15kpVhjgkQWfeytuQbKQU08erUhz3yk7LeTAff1BnNynk+Kp3lcBjYMC8/edd8/8lFCMZAqmc83UDD/OwHL7pVT+M/hwd5BmF9UV6Hsgu9a1U24/2QM8YeN+77pnF0j3BI0c+cOz94PpSFIaKhVabDgl3pF9vZR517xMsV7t7o+JAIT05RxotqUkKyY/ykPRwrPivvLbFDLNUF+kX7/mQZHwII8+rRG3oseUJ5u3UWRb6UPqSkKVd7fH2HFnNC3WgHwcDtV85QMkhPQkdlJ/5kmQeHdF1i4xxKI7UMpmThffBg9XM1whx3g7cWa5qB4adUdWgGWjkzK8UJRGdspnoNbo6TvDo3esaWgL39N8U9gZBB/oSgdyY+qnncbkZpe0H5CKtIp6f/NQ95QyJBTRofrSY7VyNeJobjgo9Jdn1QnfOte7RC0y81AogTyM3s1gNncchAGbe2oaoyoY+wYZMRmhoY93mxdLdINYSRSKkp6uISJLPftoLxhmPhNHerntVNPnwv+iBVJpGQY3Ol+OBwRIsUSkbpi8WDKC1YC/DN+FNTJshXQiShuYqdMtqCaRuG7vZxIJAolVHavphADhTnFk1igQO2ySRBFUwHgufeweJ9pdVdmCutgVb1JnTPLPV4mDHy7irFx00emY/0z8eQP0JrBYmdHs81OFL6rmMVgg+Wmu6e3pGD8s6Q6t3Ai19ig1FivEB+yXAsmBTlW3YzkNwRFNX/gXHUDeq7bka+KjFmSMd5S9eN8x+p/5VKR4I9w1TMJrCHNSKaEUKIvH9Gl1fzxH7S2Rl2+t0oJz/9MugNx0ozR41OWPunfT5KlcJWSDoEPlD2uyA0xdCTlfajPaDeZgH2NHIOFoBI2AfUS3B7dRS2eNGApjl7xpOv7ltRpCgLZHUyWMrz+UzU147Aj6ZDwuIDYu2PBGCTuJtoNuwoHAA/+Q6dl1k1TGk+KwlNeMsjJ8zFjJPc5+d1Tx4+3Fg/1JFKavZQMNwKNOsaOjn/yku4Uuv//UfQjABxLVqXIffnwY/k8F818f6Q/ODxZdNtw0FU744/52BOCf17tm59aT9UQQln7BP5uUt5QlQxCJ4YKyo2Vk44jcox/NcxUO2SOe9V7v4HEJkjFbt6g4+YXhocqqZXKFxu6gpIpgVycyxT40Wb051pvUQ2+Ov5orDJ2ZgHRB8O1TZVEw8EIW5Orz76/5fJUcvW88ilQkCQ2ZCpedEh2D9E7jXrIz/mcunxQkX5zKYv+SfGhfYcH4UlrNkCaJ8430e+Doh2ggL6NNDss5bg8755rAqfoKm5PvQObsbvOie3FRVN0agvdy5TkQiSXmnNYVaAKlhBSmG/UWd4D2w7zWbyiThvwEjJaCObSfOxqLP8J4kFSsjzpNC7GKRAZDb3vPO6Tlw4blwILi5o1a2FWv6k+NBL5ENo9T8t2VahBeNu9+FRl5xXcnSGlQ+SAYfmhgxdoEE6IjYIu8rWSCiNqnW4ORJdfDAlxA2lALpypE0gERByYgU1y6XoCONOFTljb54r5ukwKUp4B+FQ0d2trgNgz+XRzJlmEqwDykaixlFIkrpYAAo+6S+w89Dpz13u5NUq3Iv5EG6gfvlprSR15cePH8xb+P7vrrO9vebryBDLGlq7M8buK2VHtzBR0hsaJUNc60bUHydcbo9rUhlSGtR2D7sWf6+gH/7Uy+fbPy5O4xf+qNWnx0x7AzxkPT7Q3ZBNj9He56hnVjcdVg7NXaPr/bWlcBjNc8mpzn8Zr2dzsi9MTd873NEDAxZh5UPCNfACPJB4i+6QycQvIbJF6hSuQxotQADihZ/Nay/TgKnQa2inoe2nzYxz346WgkoB2OxRp1ESQqL5mJM4O6B/qrCIPIVlv4ZCvAV30Nsb5qzadbXttAlLrmtwOVE0PQlUwqjrni5U7Z9bNwZaYroHTI4fy/47WCfzldV1ToxElFe8gNV4h1+D+lcooijmQ1GTBzOzmroK821XfLuqOg9LDGTWsz/VPLbbs+5+IRQIxLrxQHHzBIakM0NEQ84R/K4p/U5/byVWo04DiGCigVsNA9Akl5zzcynmTll8sd9WW/Gsp1SQcBhWauPzVuUxp8TY5sYPge03rDjtohEdhHfT/2qWqPhZFRVVju9/4337PWxphgqLj41pnJLGhhBkHtlPusmWx67b4WSXgvtan7LjjK3NIHuLm4cnQOP1ftUK9H7Y06SmLW5a3iBujI0ZzVXJE4vGx7N25fQfPqNC+nkftLnZ2y7YmCTE8D20tbTF7y9+fbzq1O7WhUIHREtLa0C7HnmrPoeY7X0iMX2k+W4jvs5eVnBPxef8mKVDTvhsJDlBPcgm/LqnxfOz8sYirAlKaJECDg8z1y6zln8JVi442ez8kiBsCOu6tnqKEGH/5HMKE9y42L9H424KEh9wkuyKsOoVnRtxT1pKp8DblP9KZ/d1Cs0Y9++czkLemo/DY105KwOCmf6sKRctOJTKLpc8lrNwfUYI6FsVw6f7UAbr3hQWKaputpoBnx54xqlch90G8vmInD4gqldPpKQ9zxri0Kx/ZUb/qvAUn3dIfWjZdDqxwUC2F2j09iMwR3ZE8HUokrG/OM2l6lu8MdMokKPm6Gg4jzgb+rJLCic+6So5MCPQm7ugcHgAxuNVREwTGZcGz40bmYUG7d29Z2yAERqwe9t4mRGms5mqjBsIHKKpYNprzQOWqaOLkmfNN2vUn+sqVoqa9jTy2P/yMvg14tMMvfEtkHv5oeqSBqhWXyH90mTm68YGcQMvP9RYFrgiZpYtg7Vd1aBjIJ3nUFo5EM9RILkkPROKrm9YQhvGlPGIIUv6xjkCNW+HozcPEal42zbS+u9/r+hNCWjADziJj0jHP/529a3E31/rmOipN5jPpbROzx/s9fv8Sbav9zrUtUjmR85oH5P38tnm9q8Z9tyLMxZwJTK7w1InMR3oewswFjigk361ba2E+BEjdz8grZRrMT/gtWw8vLA4f756jBs4/JrUXV4tq0hCB67YCINNf39i7sCGWHoPYbjWLLtQQrz14hHe83w1BFqnfgA3EtEjbctQNARe7uu5zD9qZqdFu0rPq6u3sM+L73m7DrlRnAEV4IE7siXYMOcgAv/S5u7zjLvv46us1rErKwlkZnx3q+nsjh63+0f97IgNodT4+Mh9OPhphLxv8bD6p9HaSW6NPRGp4+o9dSlXkeuK6dqwjGV9b4Fo2MQfkuQ1Is725BOLV8pxJA9z+vL832Lr00W6XJMuWPv0Dnv6Q9M/ahL689KyeEtTelmidOkY8OmpInnVMmJRDMyo82k/993uSWnrt2QqjzPjsTwgxuDZTWjl3HfWab2TcKha4zOP2CM1sTAnW4sQpt98G80ZX4Pu8Xfu4mLFmFtvW683W/0Mz2Mmju46BOfOnxfgS+gercP4VPfdaFT7fv+7RzZ3mw7ZmhKr4RnQFJpSLuBy3fOJ5Dezfn/f5leeDOni2d1zuP7ZNIU6UdXsQQfNfX5/4Nvzv1TIaflw8xUgZ+GidHGtj+QPIGHXAdbIk7AcIO49n5uQXUiZzbphjTllC9/yt4ukKga6tMG8Kq7IFjCZA5jYMlN7P8BWuRHda6/PPvK65gl/iaNtrb4eHy4ge3auzcS+DgY2l83n8WNcUqgT65UQ156cx32Vn8QimN0/uGoTaYw9nSUnRkeMmJWr4WwkE/tUv9bfhSVNyBWwpY2uvvrUv8506X+PcOef7nLuT57601PKFWvQjlC8maKe3pqGG4zOPS/o5KhssrLa2RekbMG2xSQbUUH2tWSw+GD9futyD2+TKFmgtX7XvJa1014NX22o2lWTsWcs5xcE9tqvvuXijJ/ymvt1/+tvUvVb7z6b2s/aRDl+YYuymc+hEc507cOL7op1AcSP1TiY8W/cksHY3W2mKqBBUb1QorCldzSi48L2FnNC2+kaCLjtmR8ZcqjYYYXehMimAPz8xbSKWaNCCnlKz7QiICxtT+IlO+FjLf9tC7jJvp3VAyr4hIzG9oNpZfGv47zb8G+9OG21TadYek+Pvpovss/r692oOk+fXDzLocT/qMt7/vwOs/LOI47Tb2oPy2bDS1/q6BDs1f9LQ81IEGjcrTif7MH6KPNgDtGbZVWRPyDTCK7ECCRttrPs+tNC+TotEjb6x4svTCR0KChvWPaMEoOd2xegRjagVaul9rcnBHXXBRXsO1GoWon/JZKosaFs977Gv4m644d9+xzdyAIyyn8+rk+m5PuHmqT8LAU0t989SwGVAnffo2gbjnJDUqOa8oUJkq0Cmunwqwuj3uidq2mLTD3b1MqT79eBsFm5+7jiGY0KGBHqAnJR21RJFr1XWlrgWKOl27ddFMaCfxrYOrrlb5I0yR+LGgvoG8Dxody5zMjKMkozRrfYHOhojLSCQTicQ8BHsQLKWIbfyuPncf4i0TPlLroLkHGitXm2wvkhc95/VU0xbzvPfZIhVwFamaDfoTy25v1Ct0H1S/fwnXxFnFsr5ifil+WfKK1Zy6NDxFVmT0s75+7xaFh0ixGPxrHkdsqV+s3tly9MvjT6mXhcfLu1qPma5yT6isVasBv2Ol2jkYrs84Q+HOc6YkurfqK7+Bgc7dhkVQProC9p6mQX6NWoSeI4XyJ0wZSaiyz+OV1gfrBAerT+X6ZUhjEa+BNUUaPz4Ui6nffy5op0gDKJXql1Hqh+aqNl0gINAUXoL2mKwYq1eIalqzr96GGFhPoulqdIC4o5h/8TFILlxVI0191UD3x3WbfCHxvEfZfo+ouEpIUlI2AUZNhb+Rzqjqcqs0xb8gQp7/RqpsTVxQ5ddbcLkyBkiI91LrfxiiIUQzv7+ALjXVoCgxR8FdOsGgF0xjx3rsik4s1ex4BE/lhDpcPt1JSuzPRUewvk06NJWnlYpHZTed1P5yioUkruwQVcfp0EJVoyjpK+EbomKXBrVbleDEnACiL6fkL/2i4gBcNE8GZzCqxgfc/vjEWAV+5Ytez4gnd/s2BghazQn48UpQ0IvqT+dLPTqzuuObZv5W2w9Q01oCzXcSKcZjM5yuF9gH11GYeXdcaxuN1oL+IaZ9aE9PJQWLpE0IHZgV1IghNIbCv+Y96pQSeWlNvTtkDpPvakz6AA1S8JNZYv29xNGK28qufFHVe5Y+AGTW3TMf5tuOV3GIvcxd43Z028Iu8OFkax8SFMRcX5mJ/gB5J2srnYkUg2FuXMBxUBt0smmLLZDxe28KPOvMDLFSXtljsCOVTsDrZPAKmF8NblwLaZr/h29ONc1c2vvGXs04hLn+ZsS4zVSx7edO/YpGb/AzCTMLl8KTN9kRqt/XelBF851Fx/7aEQylpqiGknm+rWCkgOPfjKIP/nAac1b3d9SYDzmAMKiSO2LZIzr+7kPsuprj2BlpoYzvW1XQ6P4xM5NtMy8RwPKVoiIKR933rUGTebfqv4NtiRwvtRzQBluKkt1EfnnqFar50WSVNADqwJbSjWFggY+F/rVC6nQNNlR5v5oS3EolflPJqxsW+sdbZyYS7N/+bfw2Zkp8iQgxmyFmSNCIrZxtrs6e/B/gBCshCtEQA6PkY79Ko+4iuzADJf8z+OuxTKsQVdfpqf0lprX4dzb15X072m/7awHH/HX22rMQRVfJPn+vy37iId9OwrHNeN/7tbBuweyIBZXj0/clUnC9jyIWv0GX3zT1L2UA3uQMDUhNMebnl/dJ6FW974SuL2V5aj/EzKCVnSD8vkrz7/0Q4JhbATXeN0b5vR8CrHtqahbiQDEPsaeYhThQzEPsKWYhDhQLEGe+UjNue+DscJzppZMMRDcoKtHq+JzjdaKffRNgaS5L6G2iHrfGAtd+P+b99r4P5vXJT58Xb//eqQe8ECu3gqddHhF1YP1X43lK6tGtfSmaB9Xevj0V8U+/VUdlw57VxscOHPu1hFgWcvxtFDYgiPr+2VGK3wqCS9VM58ej+CTviTiEePvMpIy5EAMMyY/cE04VAOKZubU5vd0ygPYSjRudm3xu+RcTGU1vRW2KBzsbdmIbmE5ziXfEAAXcKwOwk+Wf0Y2b0wBvZ4LNjCrAp3/3ifA74tNvDr3lUoELi32gXDS1mS92mkYewDXgvmRw2wioG/CbUtAtCGMEfmo49Lkluf1UaZYD0IZlAlzaz0aVZv6SO+W3a+iJJZxNxy+UiuNz68Cxg7OgO2uT/2ViHim1+iZLf3KdRD92ynTtsxyh35VR590a+NKsCp2gBdiPtP+uGgUShITkp8x3b9cL6632n8CVNYXmEdXl3X6lqk/0SvV0aEqqan5dqVIG7P+slL0/GqictEZV29L9R26K0b6EnU1wh/TQ90mXoOE1yVrG+g60DNQZiGy0Dnoqqn/zWz//JWYT718ivzISsGO5w/cgxkFr/6qwA3P/QoMzVgLphIV8F+6smXWHcYEh7UGjoYXQkMmIH8+qsYdOrdWDNXCZOS4jq9WiagLE35TI9usvBNn+grAjvSt+OwPghyT2jLufKMlvmiJdTqgPan4ukXbSp2AAABDF+wNBTzT6AQJtqofPLUHfgy8BrvVF7aL7omdTHX0EBeTEDsu7ONgeO5lX0LENgjZwyZv0EgAW3/rdOsHhW1mmBYPbXWOXa63HRdxpfZWN/1Bq8bAuB/CUp/2PXeCV/fzWe9pv3+qmvtE0Yv1o6H48lFp0t3pm5ez/p4VE1Yz+L//0KhnlzXRe5ExUjX0dQEJ09lDeAiRmWHUAzIPTQ66ifhZwsvAeJuijSr/nyoGHstb6A4Zz5waFCLedq4UEz5UDzu8HIU41U9Zkj8SSzMiY6wiGTuKMdXz1ERxRdgQdUfnXWw7oBxmYhPLS1GFiean9AeOyZbskIz1hROUO7UcxzvnD1fMyNJLl/Rcuo+0cmfzpy0KxyDeAZXu3CRP/f/HHbaLkNLI3/EMZxHkP8T/Xoqb+BoieHf1+FIV1X/3fD2UQ3S38TBkd/Moa+xug6Ln/wgxcA+Z/KIPobuV/WiN5V/5QEJg/BAo9G7guQvWHA/EwKP40R/Lh20NB2P4QKDLbvwYA8XAGMYT9rz8J7mgA6S4s+gyTZNaWq3CrxaV3CW/aGQxO1l86Pjm7d7nnn57MX8Anx/Csd9tttIP3+Z1fB4lg1lIsRdFP3eVd9cjm4UA8FIqj+4mgRJx8cvXM/zBc0HMu/ibcjMef3PTWK3vO8BdGL9+AjkwDa/bmNxCQHVA3al5iKsp3N7MgAnW47W7Rgfi74Kr/nwXK3r2za0ZQ6m96rVXRy2P/f5lg7I9mKdlxK8ZVf2P7qnx9g7M3I0zGp6o74jAR6kkAdABEqeWQ/IA+8ZhgXxXg/AGwHsxrceKfDjFvmPwQXX69KttNOXffJTvZGVyyilH+8lUxPSER9v9kPMTO7sGhJSbu4PebkWRCsTsMnI76A8LXyG2Hxk89/4r4uME0e3bMMJ522RyQd3xVYEJIoQNkMhBeae+s4mMpYrVUoAsrqEFbXMf3ygQKjP86QJY5I9H2zl1TnFpp4ztVyO1n7v+ecUOj1TOg7WdAu2xXoFoZwCUolnBnIHh84St1K3QE1uupfwTh0aGHK5S+OtCAh6Zl0oOVRzeBAyCBp9AZe8y/H+aVpN9VJnCWAUsjYJIH7qi23epE8jv3fRbOr+sMnHWWVRkvL1FzLVwKfe2A5S8UXP+CxfYsiEKMyKBGJZYwJG1/fvmW6qLgqUQpteyoSvZVv28yRfeFa17nywnl83Kp90AD6WZl92f7lujft8UzFEr39U92IyVfz0l+P50av8MFVFKZWw7/9K3MvUsRDwCvKOlH5h3im6uK9Ae0Vh4xBhKjvFTiwPIhAatOk6nykbTNQFUXKpT2wrGtsWS5n9Nt31qnpj8wXXrR6tpEuG7AnwtA+/l1/e51CuqMMQAwHd2e/u7yjhrncHE0cnFRFqjeiLPs5NERNKtL7jAfhibZHob8NVXb2hAUewOwM+yW/g1QAZunOwiFgEn7TXIJcfYo8nJlLOwhriN7Nf35tz7SBbTfuqH3RU6sGYV1UwULBXA1mHxXN5jxbHyZACI19d2OjzHW/98MSHEvL71veP9/iXstyYYKA90N8rIuk/fDc/VRTa5oQtAX1bwBvJ2SaoPhVgj5tSEjEKcFBoB7rBYe4rNe0StLYrVjuSKLP61uzBOJlWoHyhyBRO/a/ba3mhmkfZUQ3u1/ppLezV64a9T3J0o/YgvET2HiZIOM3O2PfRnrpJ/DBGGyQaVX9m6m36I3IVpscLtDwe7OIvtj8pA/vzFrzO6Ug7tncggP53wCJTs22B8fDt+xRV186Y5TKBCP0c0o1QJkButxUjrY2O8v8xiE0G1b7Ls9J4Ql98/Ahf+HD77qXMaydxBFYjhqnzMDNnFMPlxfJ9pC50VcVVL7QqTZxoPcbSLkeCxUYwDrQPCBb2w1akny40gSW2Zu8bZWNvHdRmkOBf5V+IaUXvw5e4yEzqRXpbQ5gyxzysO5vZ5ms0eWN8jHq4I74riONcbGsxN9Al+joa9LNewpvfTHL0nWIiCtrsYODXwx7KGG7g5J5qie7XloxG555O3Xu3+vUO3jcAz4hpWsnjQs0lQJ/3nJh9g3G6+ACxY/HTu/UCEinTI3rdUdoKGchoDLs/cbBTzVybchS2bJ1SUeV2fIfRMxJN7xnMFcnSuOqGPyra+OLsvVJk02pPIt3WgSDTbfanA/8BbAcoJotRwvb0WcQsyWs7frHEJ9dSnQ4H4QNZ0tWVtx737lEUxy8V2AN963o8sX7ePESlhpIN0x1rXp/861f+iU64vlpkWArL8kRAImYharf/XKUK7hLViWBPAe1ZXF06zkfDHPdwJzg8F5u5TT1muQhzmjysa8bE8k/ETrv8rlkka25Gwj6fJ7O7rQoRTVOMb7WwHDHKOA6wkBRakMNKcIw2xaYJLVNjD43b4g5DHUGn1siKIBGlzceEcdwlRLQHZITXXugV9I+NRJWWbVkHFveltwoEUcfQsSUzPBYPH29R91EdEu+X+Qhpsv79HTg5Gi+odYGHiq6w/fMxme2Fxj67v8N4fPV/pPMmeOF2dYXUZjYW4xuewDnZDz8cn8+A0BJDennmLA7q9aE0emDuL9SSp5S+RT+rbItt6yDKiM+KVkPv23Q8xdXWA8Y9b57yJ1/vx9NFQitGOMEmWjbVIy+XtXImKpRCRHCEeVv8I5Ud86uBkXG/FHYgZeK6teSrKV+3qu279vwbBg993OQ/Gnous3Jn6oNn7qYK5invlTecwJ8SeIHw5VHpgrf/FDgkOoS+Y2i9d+ifL1mQm15yAhrHbLEquUVim4fDC4t5M62BBw11yQEfoYEj3tcWCVYYxAMLIITR+N9YAY2vGK5W4Db+bBkDN8tvf5xFzVcUlGFfHDaug+eOTfKJ+gfK5auvJ1/cxN/ZluPrjuKekfOt7tkcaWKs56ykPfHmkEqJLpN+PryDRsomzq7OGx7uHxl3z4YfvQH374PHvYipBoMNwzGsOlZM6HA83/aq/UbPFAunsa+aNkkgbVPaNRMErm3NC6YxoeorZvGuPycxsV+kvblzLjGA06UDV1dnysPPrg3+Zp+9DG0+fZcStC2rnZnte6WZ5pQFwyVoR0pr21Wj/2+rFeP870Y7V+7PVjvX6c6cdq/djrxwn6sWudtbL9lCXz2FceI6jkSNNkH5yAeItm7+2b355w31AoDyJ8oBN2SmZtMOERnelSMk9jC5/RiR9Vc5+aX97MAw7v6byBmkk37PCEutjXKJKGId5T7/NaXFRhnfCTmtndP9Ll/pg1c3+mulIHxdJGo9ZOKccqeprHCz5Sz7Iaou0eMnC9p2rmaZ7xtZP/8WjBe+q2Ufp4M/UTq8cumzp7eKx+eHwqf9n5p4q1RYDjEYoXqZW1DUbSdhaAJSY6QcWtIylwbLBKlJ+8nnQlJR5seU/Vn1WCk1xJntU+/OjLS1RarUU2V4gi7svYK1XKtsGuJ9SdreY4qbK8wKj2soaYXVtKoGaaeijSWORjOoGh5mI/KPmUuvmXbOkY5VZYKQYQX83AYf4IlQl+Snq9Fp49MY3Ua+3PVOp8C9UA7eG/rgI7d0YMwZMeUT+LTmOOkZEbpHcwLphqKltfz2h20CAp0TsEtwEM66GIeGAP91kKX06NsQ6UJTXojjv4Eowuxh44fYCpRb8nnmPnF7SjN6Gl96c50y0Re1Ro/O8+vcMgZuU1JJxxSYxQ9Q05wGyeyQzzKY4B2PmB2KgP9309o5aBdb6vhKcXQHWa4j5j1mhthaKvGe/W5K9V4qnaM7hgzOE8Pum/XheC268lLFZSwO0ALY9Mo1Lpeeg66f3Hx5bw/533k6wnc9JNkq+Y2Av/3BX23Tw5PprruxmP/WY3t3A6omNbHdA5CPo5TAJ+j1jbiqN8eA0+TboPTX1aik2DL4NBkmKBehVmJh90HuSFTPu91b1HyaK4HJ0vZMSUgQ00+aZ7W/f3vY3101L9X0QLhcgGuIBwQcapuhDkmeTlO9LPWcmuboJAHjTklBV85vXe0qIe1U7TDhoulN6AVv8SH+GZwgRHCsbvdT7hRswzOBBFmiZB9ilUTLFmKqwHY/zKfdlTfxAkxAQfZ0EcnmlecyDgJPVQeiISpY5btT1x1I1V+sQAeYSDlVWKd/agoi0BMtzZKRUN1fZjFrr8kfvdeDdytu70AD0++pqYqWwPulrRSq4FDLpcJLr10GB5Xz4JK5m0sFIDupxJAD5L94kj2XBvZuBYPq3s0aeNiA3Z3acJPldl/UTvJX9OzaI+R7fQWz0vGEoSg9xuKUldnctQ3ZeCqnXzEoxGu0z847WJmHrQAkJNfBH4qslne0j1a/8M1scNe0IpsOZhOWRAfXgLiH48wp9zgrsedEVhMn0o4DTEQFGb8+ho+V047+ZrF6R37IpEXXYe5+b1lT7dW/KmRbGTitWiMYTvfFDxeMHsX1dHwamguM1OYvTH/mnYd4Bt6vtBXHJwum1/nNVcRDSoSaKB3Sw4BBXHkrl2/kfv6KaJHHOtc3mNM6Vuu/r+v+Oj2o3CDqplBYXU7+Xp4KkBwnm/Gi0Xr/sFnqeU5f5ynY9mia7zTwkbmwFy9ra44ERy64x9mwH0pg0tzPjqgB7guLgOSRwKUpDaLK+BfypyGpnVLDh5Gd92cLfuhNWL3cUWO1SFp8t8MEnLGKerpBEXSzeqE4PpVaN/L2nRr6WPdYm0WD4gRhmA1AKN/aoN4NEzZvnPMVN8AbHwbtZSNM6J/QAhLruq0LGKrmuApMRcWLn47Caa5qXutdF1MJjQZOE6rwHwQN8NEDvS+DXyG/a5ULp/B9k6KZH+cDSPsF38Q9/1KTkO2AKlZ7b1SSUfCXQ2POc7crYtd+nl0buCcJNfW/+KvU1Q9SvqeHICjvgwzyuBawOQaDfBvHWDdnWX0T5BO0CBZWV7VpMOWht8epNySF9stNb+wLIZq5YyhUMygdMSyXoeWnHNv93TQ7pvla20W8e8arsf3UkTB61HbGJnLOVqe3uiuwFD7pgOxxSav5ieti+9vFqMEm5e2FT8wyUvk50j4sU1sUz1dfqglXI/Cwp3EjOFrzRlDhcY7ui1O0R/uRFDb6RZLX+gdiD6MdZR15stbAcDNVkomH7NFUQ3TU0V81bk1Pc8UZHnCRiR2IIRN7VyCIv5y+GXpquWDB7kC9Zsx2EOzxdcZqVItyXoXgOiVvW01MDPoz7o7td8oJgsNx0Uw6a4Gt4+69/V0JsUQk6A9FptEHACRrpMIpcl/Ep0nXJ7KM6f9C+JqT9M7OcXwtELkBpISP4D+lkv+aiHd6+P1o8UTb3WgPvmLuJxncIuNyM6FaaFkQJRj4N2FC5Cc5D6SpOoz2+7K+DVVOm1AWNJk4yQDnEnFfO5cviRf7vKpMAlGB1Uee9opVQQDwTcEat/S3DAn8b041tlvUZG88d2yDaoU8rVoF4BkXHBo6hFKWgdgVf2B6pfUfKtF5cpuPTVdg9yrd8wPu0C8nDOWlUsNF7h8U3iwhvcK3J2U8vV9fJ7v4sMGsb9DAQxd/f0m4xruvnLaC7TnoBuB7ChfNjp4PV4K/HR+bjMQg9f7msv2uzh1KxSYWSwAiY83tTkB526bEHMCat4plAGTRjnIaUssSmhYOm7qzy6u5QEf9VWap66N7Ks/NmRKv95d0VbQtTamsTM1lZv8pZLW7NEFsa9QOPkD7VhKIrcKSdj5hx7z9qOwjHGuGM4hZLJ62ytCmbRSTV0HCxuI3e9Gby9VxKxywalOsrlAA++A4zQtqM9Rt6kNI5YOtCMRkrjPRfgN43H+sFDQ/5IT20d6B1wGWS9xkiT3DDFprJF6aw02A2VJJqi/f1VfnqKHAaPu0tY/HHMngs2ZEfTcEGiOvAR3c1Sd1hmEGCW1Hn8LBmTD35+m+kmmX0qomZnXAqEHUWJaVLb1iOUyyFnOCak3jYOFZbJuL9Eecg/ULA98yTBgaGsiZjdVRFfLnmshLYvd3D2DTS/cFzpdsGKtWzMGHwPBkKl9w0Cy+et+vDNi8glCGZevgBv+7tRrfDX8XWI3WNYAmZKuag92SZ74oSGD9pueRBtwBWmt3sLoTV2/jAE9p6FIHU92gltiBc+g1eISc3wnVIgx4v+MEt2bHDnKyKxt13726h5gylOguUBIHg6LCkxb9cGzTzgFXpsPDM5yxzfJ0sxHCj7zUKGIgG6CRac1RlupkmCWI6OWW984xesfW4bmzJ6apqcf4dRAJdM+sLHo4xKLSVb4nBqyKBnNdNpFuA5FYPzAo5YHTOOk8AFlij4vT+l/etAOGniMXplkVMy3E+BB3iIM5ukbi7CAJXRzev3AjLJ6U0f/AkodVMx2Vnx5fhGfRrImkdkIeokWcrXAN1TXHQHAx24zgqIXEhM/pTnyE56+P32CaH1QEB0ajlQ5J0r/JCgNondDyMG5BIVQTk+HyGyWaDMrGkG2oMr6qpDso4Tlzy6iarA7HfvrRpaIxCf0hXmErB2u/28hV4Y2ladfysyr08+qO8VYR8a9fJcNopg6oQb36vlpzsfzK+bxihNOYpujVSSRY6OKKsxMdNHHn1adxHLqcFrWSukPrqkeOaDDfdIcM3hKmtl1e+tjYS80zyuXtxYr7o2MGyd5b6yGbwz1KYb88Y4TyFWyP1VDXAc0pMCiZY4yEkfKSQg1+kf4wDEd6RbwmxWKClF2JsAdPRhiemnlHts+ATP8Gx+orvJh2+WMS5oG5tsXPlBR0NWVmn6WtQe+aVlS4vF+IGXJNRgWAmKpMVC4vQo9U8JOZj/b5jBopPLKaCKxUIOj3X9OfVr2mmh4G6tOusDYyowb3Na90/I4LVPPqmLDRTn3fX2p+plUgQzcBtwJgeVQOTdoWmInVtCNJhRzMXIlfGCf2UHDl3LV2TkrqL58x+lluzUhVdBh4j6ceRK99FBso+bbKtGnOfqATq3Bv5QfhZthILFMaOOgzpgr/RRF+iK/hRY/sTmtq8/bLmwTTDkt2T6rMQUHRAZ2Qyk+Du1EGjdKsMgvO/qG/3IpTxRXy9QdUnCB76n9aIuoHTMgz+k2agiv0uVuT6Z5CK8ng9pIuMq9l6OvhJpvXjE8KsedNr/WVoEzFzzCVTQybGS1jsPQH5TdorQnbmkc7SIACrp+jIlEXvkkMMvNzaTXoxf8Uvi1uuKUKgp3vmH5VtIInWEH+cOjBID6UUiwuYzzu+V+s+ngAog3AtrU3xMc+Qkik0cK5+C8ZI093AKW00lUkcKjElSu5zEfqn7QJJMjnOlga8tyPXoSu/HALo1tyz0UZp6W+i6S98if/cuvAm1eFsWL89LY3ORJp2odIceh/y4ZeiWCXApj5jisvMfV92iiovuuZ4DpNvmNxCKeDdK0OjdyEcttl9yAPTfqPmeLf2VZAfuPMBzqk8GnTNs0CsuubRG3CWKRbK5kQrB++jNKgI5DqJiLbUXktYjqgiE1rqTP3w8hJ1PGvrQVPqQYDTyAMDyh0+mgq7gACbPdXDORECHPWj6MIuQ4psDNhLn3a4RlA13NxBbCUkizC7jeHPXYj8JQc6MQcnZcldhCoPkj+gp3TGKFM7Clw/5dyjvYnYTTqLRT5SPfcevQHFZpqG9aZgFY81l3aJPnGNB2FsQHKUMhs2VjsdiT17HRF/B5rwsSlzUyIxDiURZmMt+85LoQknu5/9YpFfbnDyAs86DW84l9Qi7fFbMIeveMaVPPXfaBlDhXlyt8j+vIkGzJD5AFVOU2ilFYhu1Lz1jIYxRlbV/SBcfxWc0NQfSOOrR+/B3FEUkEuDfr8rFzWB13nb3/63rHyIFO4zMM/6zE1VK9c+zB3EeNjWYWcd67UMCEMxnBmpBLUezyaeMG63bxpWmstvrjBQHCuaIEu4wooiiWfMZQxVfopiEKz5vq88ubgFrwio/D1wS5HaStbJtuYUAeGI3rpbX8NK9dehXKKFesdmizUR69oB66ktC0sMNDjCVDeMqQczBWscVDvqptAYPr2oYHRBdaAvaRitw4+6qK45pap69+g2gkUIKEgsZgK+GLziCFRFxJDBf6ASiD4US6CgcsDbWRsNT9G1PeXkxGXsMHpXllcA9fSJ1cevcmQ9pHU0Xr9T1b8zdZwikbi+WOg6dIV6RNPE8XXoI52/QWzlLmN+5/3OvwxGIGClMiFWgf7cYQ6UYGQAklGWIo7UeKFuBEtnxAEDO1tZ1azdLVS1+y8v2PfynDmG7/3ZdMfqa3j0tXFtzNufajxQ7G737cf2BnI0b+nIpmH79uQ4zgob6JjidvovlwFzeHj36YQ3a66rqke5ZrsYZde0ZKMH36ib/b0PI9dkT61ro62Sxq+WkezCRVEhhNUDqp8J3RmLcqS899nAl9Sc1te4iIe5t3KmH/Y7rzr0G5E7jEr/x1rA5bUFdSAw1ARs5vowm0GrJnoXXSTYwYR8OT8R92O0VRSTDYymOxJ6SJLNGkXNO9zhK5KDwYcksl1vmViVXUy+zP5oxr70llEROgAEa/bt8LGenxUeVgaTuA7yFbPfKKUrG2UmV+cw8kIO6DKz2C8BRCrt3Hos+tfrrIZZiw9JMZedEbSaEogWi468gdJwGncorMNZcjZyLrrtGLeXRJY9HZcs+DhUjmPk6DrOLZS6E28RS0eq4YveC8MYa4UDnf+8GXM6xWJMXdsYpGoIO2Yjl+3JbyoCcV8yJUklKjuKqHxm6WEhFxEOsmqqaWYPLCLkBLmBbC6BM8WlxAIRjlKBwEonwQe/txp5J89F160/qIM7yIvQvnkmUj8ZggGlUeRCJq+isZlaTCZmw0ux6As+EA6zmx0xdCKNQ0B6ipU9m3W3tyJs1UgkOHjvxZ9rVN3Hq0mwtflkjQi+2yT6p6eNICUbaGIyZEqypSeOYottilXDWYm2qHElEe3s8XwCEKEv7d4hqQAf1jRTI85ogRz9Et6Y5ACaljkweLhEEiKfdmodDNcJWGpdqB+VD1yvApiUrZsd6Lxfly03Eav984LpB/b3wQJ4XFwaCGxKdUHvBTXWVS7trRwlZlAQgtrcJsmI/rlrb1q84GzcwIr2KebdPEtyZcwFE6azTQ3oW8JqcJP1zgj9Bet6Sq3L4kkMV7l9ig+46O7qgXC/eg2Ew5h3QiFJgiUeBQI5AVa3+hp/87j7LJKYIZweU/mLodCb6n5RznbN6fK8e8fmvhjPOUbmFj1rePiFfN3HW5ixPYVA3bP86jmZi71i6zJrYK9lCXGguWf1ialYCw39+YRzaZxEc0wzYv6QWIiFLfEGjWaC3zlJUUykoEmfKRNaMEH/MLTJRhj+nse0HI2FGTiJZ8TRGpTArgiW+QaIW3uY5b6a7ENy/u9TDViCnIvk1f/yF+bQmevqJ7Ssz6+7p4r8sOKqSQjPT3+qitj+Mwrl2MvuChgVpXpU/fgc+rX9hP/w1f5ZidZDvvrhY3trDK5L5fW4Z3o0bVnfw5JFNkf2rfP2c0jxZ4mHWwXfakQAykFlp1SuwpGMmrNflOgK1oKPAmNlJajpdV4gDDWvVdc5/mNsV6wJo3UbLdRhpai85mpFsZq3erTIoviyQ979gWGyu1Rgy7iR34RcA8nKrgmbecpwXmlPM1cpG0EQdGpwQJoKE92Dlb7OX+7WnzJba3tTM2ljo7Z69GBBrQWt7rCC554bYJlqiylC7+H1GJG8vg8BTp7QPIqA0Q3StiMAsqBRi5lMVLa1D5DvS85FzWCaIAGIf+wHO3e1ys6lKS0TgRxN9FYCPl6T2p4QZXOcZhvIUc6t0zBAzLqXwx6XeDbAfiH+b1sSWpd3xoFicN3P/Zu+8VP4Td3c4N8Gy9eIOs2Q//baqETkVbe6eL4TT1SxIocHa4eWs+72tOozzrHFOMOs6NiTvr5aBFCIwqLlx8awdjlhxyRzm4R2EJskReQ/0VJam0LHuYWi3BpSeF82+sBR8+k3/TxJgc+10m5V1c7zeYoRYDlwj7gIYRAsU6BftyyuFguB12BjmR+RraBnLip7N5QarZMbdTmD7CO1mhvyxTw2nm0O1GF9fpc13LuSGOEsFsYp2BxokBWEJM0QBabH1nT7BHfSVc7/zLtN3uOMvhKd1KcQ1o4c4v+dHKBxNAw9OavIvCR9M0WHJqnOhyfAiCDDRllvuew2aml1lOxU4No5nFoBMgEIR616sqbYnkSHU1tpTAXULjyzfSZV96OZ92G8goTAzf70iHzji4iOHD/scLpO7U7MM5t9jbfsdUIXcQ4d+f3l0cJPOR9P5Fs5cFU/afPQUQ+c/Ez8aMLo2q1lDombpyOMZpS61tRgkwfnxjAK5BDudvXEX9j41Vu5jZvA0ph6i8aGMV1HvLDE53YUYXuCyPcI+cClfpCTWce29VANbi5eRrja+KMR0iezR+306tfxMpwgFpasnW+S44fpzxxrnNocSKaW38tqzQjFgFJy3YDZq3rYP1HhFYUBiSUpA+44WIxiMltXLrpCeYtgOqHQvrEf6oxPuw/yglccdcQAYJSVG2yoKp6xXDqUwW2N8x2icOgc8Vk/P8QEJH7Rj1zj+RdlbKawFwu4XkKlkQRxyMTYoKReb9iVe4E5rauUH/GoGXaGCBLnq7TvzeNO00fnhFhogKgUSoYRGhHn4LiMlTg+T6FSayUWf0smHT6yJopCUxV3tfGVzy6hQTWlwaPsEjBOaK4CLAYNW47YNLpIh1AW+bVZmiqlQF1RjR7AJ8Y0PdwmQu4/7e1FUrTV0lIUj18rS0xPwQuxhJkf2Est/fuk/ahaWQjsOrQ0d08N/ZG/c+4Jo9SJS0ud6opNLUsTgnvWyDG1PionSpCZ9DfQvOln2RUgy8SF6JGDFuuc7tH+z08LdYjBfSpoA2MvmH6TDd0WgHtfo45IoGmP7nHJ8mDEdb/AM03o8E6zEdDMu8DyTvgzulyM9DVRiioekBke3BN1UO9xOvCnBIgJQgXcvpX3McVgbimavHySRmP8tZquq8ES4m0k9oQ6Oa0NAdO2m0csre+LdTck23v7ZQgDoacYQDB+DEvfEpK0hJwccatP3laAF82D+T26o5gDYJezlJpeA3+lopAbA7BXHCAXOCL/defaaPwvMCOfbEAoARCFyHDoMkZqOJ3o1QyBpDwr8AytlDS3W1Kx9Wij/jrFho9rGeP5mVbPWjiXGE0KbaifLhqZXDXdluv7vGPAaIV+CeucGoctBWMzzTy6lN5GXUHKtEMZCKY+83gG+fcOw07r+O8R+V/Q4bqQFMubzGeSXGhjQ76GLoedvszwVTetp7uLpzxNIoVI1W0LslkYDsqbVH/OnMDZwMLndzJ5OL57SRK0ppzOmEwCKxKiRSD459XBKAIoNgwyc2R51oF/y6V6UUyndd+OseL0NMqp0J6GKpfKWWBQwNIBgKZHKraTWHeeL7o6kBwHLUnXYw37BHCAM7P9FZ/TlRfz7yHztEn7I0ys44kA0+TABJzSP9cCInGngVDsNA6Ark0oL2VigmO36twlq4XPvAwDTAEcnkC/pGcOkhivXFD1ciQuWknrJeXqf9+NloArVh8siZ6SMZMD212TVWCvi8BdbxQr0wQZsoTdXZ2RCigUAI1msBektaOBHX3guzc5Spa/pw9B+dnFDOhHchrVUEJLJPQKWxwCkAEtmv4eHEWMwSLWPzAzYp4A4qAAvcoTv8C5rYDYCWrIWFB7wy297hE4fycpO/9cdJQg1CrzbJp8S7pKV8pYOTy5TegNtXpzAQLB6ngP8GCLslehbMWLFpkueBf95pWP0X61XEEV1TQorNaJWwFbbyH2JL8CPHlsvj2/ReHLHvP+RaNILMUcK004rBGQ2DOEExzvWB+bmAbQNZTU/P1l4aV8aDUHBOYKxmHZCnh8H6Z7yEzWERcvo0QAmnwaW04YOUUh7YJJTstPM6RzWPuhhW4TYnKXrQ3F882lkseaabdPEklKrUGqxVBKljy41kzq9NwhkHg11kJW6bBFNL2ZrzmbohQhWYCSI803tl+NZRquQjCLY4rRQ7I08xHCQpHEAk2EkncbcVUu/RQ2Yn3TsUFXAf3aonG/pDdWAR8vUen4cFjTLdiX49cB9xvr8tMJrCit75lNEgIdC52fRzKrtsXKFS9bZ4HtSkXxPWSAQJYZGjyYahnGvZ168P/UBScZEpZBhv32mTsMDkBqjeUHQeJwiFdRlq7xjxXymcQGcxNLuP18R+NPA9AejFnE5T2W5UalK90EYpJLjhrgxhh2ZkPF2DbXQSG1QgIlMk4frKLD/oAoLZOFYaYGv71eDkanerRZDnLW5JLDBmgx3KxW2+FOd6XMPtXJ8mDuUYjUHsgvBn828mD4WpSpg30dA1Rb3SWGtcnWFOY4P/bUxGxsuLdZfDOgAA36pNBAQH3OvNmJLzYX0v9MKEHGF8UCwsBpIwni3h0TAvAPjRVV7n0XYw1/M/QwA0puHwIe9qA/sX22PXgBKKWl7lgBStpvADRS/ERSUhXwICaHTMDyUpIIGGGU6VK4vgUexS+GZCuQYjzrpaBPSpRegl7reQX8g5Trl7NEaxTzdlifu9FKsalAkHTGMy74RrMuu1nMKKhxl/5QYan2DYMdO8bUs05t/VoCZGQw4Fl15avmpp6N7IeMsOBHI0jrIdEfIyiapX0QbdDBMdPG2/ouNWSty0fBUbqhSt8pTBgkM6Xs/k6q9LWv4EsnSVf1Zoo70bORT18vaWcy2XlfvuhcG73ecFjzn5KwAZr/yYRHyAdBOrGmtpAGhpbFiphSjK0lCmOGzmBiozVBC1afKz19bmu0TRYq+c36ey64jWHYVvKl0oSLFgyUqTR8X0ngsBm1cMKcL5yjgZ7P10QmgLyFITioGUpxXuIBGS/L2c2OEJ90JG+5XEDZN5EYcK6mLdGbPSGfVBTwCE64Dwz/JWmvjsq5n8VLfr/xt+RrazpUx/RtDzgE6omF0m82SBWIGkI2kr3ygQ7BW7MVMk2XldkaWWWWWfDFgpzgHRadgRAf4WeORKlBMTBBUIHtPCCu+F59ogS+gWjSWyG1U3VzAWCQlACOmBMgDU5Lagjh28EzmwBAj3L7Cqkhhf1mocTdbz8BrqmDSv8owkglhP2XM2dzpXG2IRMXMizmLzoHsFndRkAsP0dIX81ftZOSvpbU4kDstCO9gf/BX1L0MuLLNVipE7HTydqhJl3O7lUMx1A2i8owF2BA55dlv7R58y9o7AlYQcCwznTQK4JHoqS5FiKduMXokuopdVpHRJSo5MwOQwDlBssIhjbhvJhFPxP6Bl31BIp6oJBXjHHYVNws0MPqsXHfH7jeXy0QpQLQN3JhZLEsgLrdivxyjU6es8CGjCfL5pfEPTb0PsDh5bWYYS7C9AT77PghNEVRnnQyAfP64wLAH8Qi/C0Q2sFEjUKLCgOyEmjJpxnV8XQ58lRQlnmIR8ZYjHlBkMRW/DMi1CWOu09sT555HPRj8U1UNmcDHYlRLYDqzqzL/lU1CocPec+w/X6MzHZipgkAUBjlEpLXC8/k65cGS04iLVkEmdR7JHa8XRYT0Ur6JAZGIixWS5Y0ZXblYzWOtqV9FoIPxZqNA7BygIGbaktztaG2IsEKEMRu3cBKQstteVens+7of3WAT93viRS+ay1MnQpIZX6O/0u3ZBAvlez69VXEUR+0NJAgtC3Pk7uWHinOOhGcqIUUpLTWAIuObyoN0aXEefYod4Sn58d+Q1Ztoo5iF3+FopJip2QlHgzOUIp4G6TV/S2rO55ZeR7XdhC1sIDyFlZjzqLRi+rtR84qyita7g6/7Pvs+GI3bdxLVdumlluv/zX6Ke8K4i0cNERSTUD9DgT7NpuLtPAW9gMSaQaIreiVL6KCr3ruQQ8Kp24kjmnw1NWfnaRiiR7gnmkzmQ0hqllpfipU3zGE+H55qMosPbZs47BGeqr1nNka1rhFOPGDuqmdJNigM57umOfKXSIvxuac4xhq6r0p6+OfPabtKXDpqOAQSi0gBzZ4OgaT8PHUWtuIsnp47SZAKQA9KHmnqp81RqtWpXxbvmqy1FEzYi/EKfe8VwdxR7c2Qw2rXwxJjlg1l5JCx3Zo03HkKSX+napBuq6jgPONi7XEo7LGCh2+4W85+ryYrCTM6RLwM0FobxT3rYMTU2BPVeNuDsqV5opWgqzLkk/XmuxVgiM203q4JtUWGyiQIGt0jtyx9Jo5U0ZVAgCQzEQg5V4aD801TaDusxNaQxSVw0a1tMJbuTnlkhWst5c0/ssQ1N/K/UBJQwPkDExylBnyJ90QYKBh2nMx91DmIrSF8LF04wUUezY17CqciQFaZP6AsaBgb6XeQp5HXW2+DANIU+xwgwEc837EVpy++oQSrpecBBqKwJ022+Kie2/BxwI9v/GYuKtrOLA8UtFfNks8iI+AbPyzalp/u2nkCN00XqaEcSgfHjSbcsI5Diqh4MHNZjlxTXGdzlgNARn5TI17dh//feswFLYHmTmCNi/NsZ0A8O6rLadHi+c9T74KaZmM4nOgg/AtuDQqxIXHI3PiUQwJ0kwT23lJ0jZcdIkyKE95JlYLzi1nenQrhZmXhpxLvK82Uo7I1KKDws4j946bRB+E65RqbVhVTtUBAmG0NX0AJesX5KoccPAhvcfrwaQ6qHzNAxp2JgZ+FCFbBp1ZhGzfFk5wzrnga+6fCUAOj+daW8hxvalaSLcCchTQ+lw7tIjUTML/4IYDuaQ75FQyW5iDV6ttOS/aIl6G5cMJCbDJ+ABuErpM7jD9xwSIK8L7zbh+siUVC0PBpFvJnPHuX2PdJBCpXahGkeb0aS4u80lU85zfIT9XEU2SpVDT5OnR8nkkvU9brok5qSTNUfk6S+vgZ5FK6VDrnAzCpv0z6i9J/j2Ze3RUMiHQeO/s6JZv6XVB+EWznVcAzIWUzxhfqpEuQTKWpDuQd6ZHnC3dmzEnOIS8jSfx+FDA2l2yg5CA2ecmFrDXPjnKBaVOMTyQjrUf7Hs5RKn35MzxCn+Y9km0VGvbEwnhWRnTpvhYNQ+7VAVJAlxa0TWHuRk+w7LG3R7StoWw7PyyPLi6mDGE9Tqlc5aRUB221znke3vS7Y3UHd4BrrJquzkwvJq0gxdOKilOy6o30ulNm1RrpQaF27lc8KVLEyUtVEsiO0n9T8m2bMgGdCivms+A3uvV4LQFpoIqeC4IYBtahlUJlzTirprFCyofhgDO+XMDVujA+YwhRykwZ8khVXqAyHt0ZUup0aYWzj0n+HPcDRikkfEPFfJD2D3Q4VSqa7ZRu/sc/vB4ryaIA8TMaRZ113BjtvbR5zjvvbuJP/mh4JV/iiz5zs4onEv3p5hU/VvwOFv1J9Xneu++e99/vvnHCgwD/UzytSW+ZBJxSBFc2TkxwftaisZhkneqtGp/VMqfGLCG2X0vwId1S/LC+nHLGkYSRh2eBwcntoQjZEIsips9kD9csrmhqXyp9HQ4cIqxGRepn+XAm/vTDtwxu8+It0cBGAqDOwyOpqWw6/gcIAWilYD+vwAPDslyC3WgiRvUtPzgJXj9CHhOpGMwKbelNeTr4w3E7a/JsZAR3kmMdpzU2ZYbvliBLo/FbIne7DGaSD5F3OJavRTTuJ+W5y8MtgjcuB38GeBI9BNsUTLwzxmcK7UuDMD7VrJuiQbbGT3NL2/rszw7i9MlXq4fmD3lLqMCX+YpS2b26coUtawWaz5omu3uulMoNR/E3UrffqUvLtqlcq7p5U4gFJ8o/xzueh4UHJPxOe6+0Tvf3Q3vOoL/3SS3T5lwnit9jmUKqz1MSyRYny+HibRTOgdjjVRTpdjSMDHrw6iTuVhKIXmeV2Jdeif4en2XpWIo/Rvurv34S1MRw6hmcxkXuScbmS1rV8jVZ92hInxl4lDVLm5ZVxxdn5c5Q2YqmV5/hXR2vzheXaLWhSkN1HudWsYosr4iaMbydZ0bPF8NQrKgbvaar87x+vabP3dPl1pt0eEw2aXCvxy/I9oZXfQ6PyXA/u/wNsESKJx3De3FLbMt2SlLxAbT+q3D9JLLEOX2LKXuv3111bdEr1tUgnsv8FJj+d0sywUmGOys59taR1XKaKnMyIsZtslq4a51vWyXsVah3pX/g6O+cY9I7wyIu3TNLyajiWRY8PndY/oXUQZGG1APZW+mioEOW28VLKfLfRhngefU6yjZDhaiwxDmI6Z+uPoJOyBdk4RlSwVRZEilyf3sMCp12s2wEk/s/XMbQ/yHLUZMh6ePE4BhGRWM0drNrhFv7Hy4RHSJwDgB0d/TQPKcYwmuzDGvTts/KNQO4gcujB/0lQMIQv0BJsJt9rHjZ3xIMRUWDx71Eg5QDVqaUd49G1w54mDLdDjZz0zE2ZuRy05qwlb3oBkr5yajqTCjPmhY+pWu8MR76pOO3h7e+uO5vSvu9b4ZA29kNzHZhPMhPnRiEl83i9t6clOm8DeeEmbKCniwa+Joqd4UA1bw/tU4+2TZ2FhiNKhjFP5ObaMten5oeBMbElb/wnf1MDoAC1gPhZAbDTc3B6W0vhAzhwtbYBdlUH12msbfpjmd4aKE31NlA5Y2WL6VBBQJrSpgmn/sARuXV8jXf448zWpYVocp/lYMyjiIqr+ZUrPuKm4sBT3ezxNUcQuVXi8a7IL8zByRSCAVfv8hXUl6/B9fd7Yt5U3ogTU1Qa/V+xw1sGTWn+HDdTqnEyDUYItdWlRojxzXjCHv1GUPgjKpynTzGTBrZlstnZtp/PaJsq2iWHABhZjK9goxuIR41aO6ROdQEU2WRqYvTTMFer0ZSfDN8JC2vVUjp226V9xKIMOjW7DMecXtQYvCfuruNmv38lsPrTDgXWpDPb4uWHInf1/uQbriIeNjceUNMb8i6Pkr6mwdTsVk6kRpOT5Kx0zBUc7bt4tIGFcjm1yLsc+K5pHB9Qe4MCCF9q3xB4LW6Lo4+DrCpo6+TPDMclg7dUr/2MWE66Sy1wSGwJmNo9pRWgg/s7JxwVSUNDadsena3YOnvur0kyzFBPtELtdZG5oHRRfrHheHHchy/onAzwnyK/Zo7azopfNYmozBJLfbV9Se6Gd/cq47+aLMqVWu8c1JviAzzykVwIIIhvmBQXMr82AhcyAKgjv10G51WgpdJowooObGeqfCdKNPprZTje0Jsc8r/gr12K6gs4qj3GzrZUcDIbKqhDUHP6qINDBY9Y0EUelVPUYb2Bz44UXTnkiaVQG2q/97W+ukzgp2nk6vtr/Lo1zA/u7e7BPvkp6/PjUpFSV6YQWRvXSXsPCLRsBSGVql9a0ct768YrosvWodSE+2QvbfdYq+k0ypbQf3qVCszRPRiBdKYVY54MIglaiCfjVU5a1eBWONWGtzGKWL2aTSt+cgf/h0Gh/tCHllk2msf7g7LMoI9Bc6fGsZUEGhTiX34yq/3M5+GW/ZqPrJkyLjRKkroxbOv1nMrKIFoD0aQxFWkkdfFc2XKwSPw1lt+BqI3QtxLg2aKc2gIYjBlmtibGcjui7MAtyegLfMUmXh6PpJhasJMFid19AGI/AG0NbZeHcyVa5xUsuft2RYg70DlNEufpIpkV4gaewz3kWKkJdextXRIcjUCkhwbWy7vefzAs8tYoj/vAUPDD7Pyz7czYl9ga78GvLH2ftLjP9l3thRWwhp+Nrl87eYvRMGFFuxMdC+jjRu9QXDH2Lv/o4KoDPmLPE4YMH2vc8C1nWgvbCWWah6WPlmBl2XYSbOPDlThO1346FBY7AvCCp9qO67MEowrk1Lk2C7jJoDxlT2MB36e97OGi5vbvL3zGOGPt3PE2wlj5CHNenlEM1tghb+KDMGISuRlZc+PemFyIgSgCxrYoNjEBTMx/YKMMIq8OtunfDfRe0OPVgKVOF5FHBt+Ls2YuiL0hhvuKoamz9c30e3QZHdmcjCnLgIP7QZEX/bc9zCLuPR935ZVjD/qV4yFdhGY4Kxub8LWvJA8GA7DfTlueiXDEfhNn4s29c3T1clbIxoxd6VeWam+wx5+f+wWKwpqxaigvPm+xYeEfAurxCN+3rwM1L20+iU9zWtz4aW6E3vLfwP1/5la8YOK0lz5HJzeXAxDYr+GJ6Sju8my96anEZExa/St+WpvEjV1OKYsP+22X0hmBNhgNPk7/oH03N3qzxiIOP/rSvmG6whAn49kb9Izda4URa/woxjEkgmrxRseG24Te92/B4adgovBhh/C/1e3IUF7Ac7Molw8/CKi+JWq43LMUh9wlktncOn2671oWfpbde2aVNfUgti2uIMgTYFZtVw2Oh9tHR3l8RO3SxmLNi6ZsTlB3R79gq5bXBFmNS+mCHWvaGEY9FYqrUYL82rNyB/nlyjaBdV1Mjz4KpXWXY7HGSn/KiQ6+y/R1l4S6W8t2VDulr2fo3tRoiM1P7FXnlZyNr9Ofngy1WKD5iqMaLFwC9MRLXyIMPH2Tyv217H6Gq+8PzRqcOurb33nTRX3YW2a/qz/zj0qmfkux/KDDdDkZ91d8zHYu/vqVUE/sWv74ePH2TAnRAAgVodqur9d0tN6KIjjhBlJDvr4O698/fROE1suqID9/MQxqSJL04ACAtXv92lacQRd6e1nDK6vg7lHp/16kckVwW5M+Wj73ff3KVnWK+JUsY6eEGBou320mONuv0GhYcN9VEUGQ1ul09qbmoax+kudLBbibtESqxQHEkjMe/2kA3nXoxXtDXRCRtDxqSBtuJCT9RHF/s6ZEErAvmJbdhh3jNl9tdwDYXuO/vvFyZHfhoQchUntmeDSj3fsf5JImYy3xM6y21XzhfMGgx5jxLs6VSTs6Ft2Y31trfsTgP8fiD3Ilh3Hj6oSSpHlfZxb9s2bqUa852N6uh/TOGuOjbZve87jd/8FTfvLj3Vc9dJxsXjw0yUYz94P2fw9cXrrwgQqdgDR86nf8nFD2fJuxicxOIynJVqfpUcxquZjqWCUNKZ6MYXZofZAu1d/XD6o/e1I0P1U6b0V1YvNP/Sf3jqLRuRCO12/JWwyTBws+CYQSyvF3I9C+PsCJMBtVuMppyzUHjXqToEgXsIHQUB3eP6J9Fv6Z8NZDEkdJgT5n6bkGiuD1PIHsPaGeRQcMjxPwqJw/iLLAEPrUbGHSJHzWEFHLcRKgnzuov+/djlZ6+1ip6xR/RwokOkWRT5PQ7Y7T1FGPE1Nfj3o73fupACizti9bA9u3RpoNPdMvIgBeQKY/gqfEdc6mmrB5FXDg8c7gAYLEwKeOoULnXJUw4ZWr434iO+9b8NE8GBKpTWLJzxYh8LYjz59sFLKq/vB+f0fRezmV9IJyF5fFeiHzozvYM5GgGFrQ+VTCiQBCHAVolkPKjuHhXAWJMFK5a3LZHt2z9Cz5FC4g7sGsbrhr2d8buo92ufAlALfoJdKNRiFbIs+vV2IJR29XcVWn5tmvk/wt+b32NVxVo0TSxxwF7CxgvBQvPCGoVPP2a9YY00G982XgJp2oSGL6VUPwl6XJmBZOqWdKiXf3/Ivi3PRoCPPMHh6jwPQEr/Hkncei7L7tY7G6UMSVl+v9MUQaJE4MQO9q6IgnGPlURFK72ffHOgSDozSYfbzxb/CQIax1I8t9Qb/3u/qgdPRQ10j0AViJ6Wkz+z4ab7GiReWlt7kFGJGV18Zu4sdSTL+Xufd8Engg+H1trdDzSLe/0W4ke2xvYxXVojJby25nR7U2iaJyEhqMmO6bapkrPzaZojTociRk0OFY6A5Y7nsDoE9HCEtDiFGfJa4UKaOO0yW4pnZVT+agYIjQzt59WWZVMXG5TlKnZ8Vmohy5czMpHLp2qgnTkocGCa6m271EwXr1h8GAYKJIqCb3lLWHIOX/NQxon3A6B64mwMt7LaHZCX9QJ/D4yPxp9VdDYjcgMRUJEMwuYTyTdhMnOkEjaXoDGR+zX8Nh8xl02Bv3/Dk/vCPs2hXue9FRd86dGd36nHgDiI+xdmk10B/eNje3sf2kgIDjoI4Wqwl/wyv0wtIfO1bi44T0nJXmdk7qGj+Bl1xygfUp302PL9ezgbFdKQVqdc5rvhEPN7+9TnfBV24y1fSOCg1NLvz6+jVFvb4l4M/IRzJL0dGwrfvr2Z4yz3Vp7o6qd8ehD68/Q2Ixc8ONTodyTPB07TH3y3ifbFaTXo05hMllYMdvdhBtI6fRDIa3UhxrEiBGHzuFaczG0T3DonGTeJ2+OZDFEYGltWluTO1PG+Mfbc5Fd8nYCL7aL/el/KHv7Lzv2F9HA3VXY5w8oMMtMa7+XchrfVuDATwqAjzgkouwHBRQeCbuhXRWUwbVB7KanLgAwLgtuTMiZlCa/zMubAY/2v1WPDlG9/PYYuJeL+wa8u8Wlogkg1GgA/krytdXVmacCFl2PfDo4kEHmNuBPX2Jt/HwmceIwKJ6gI7uEo1i1iPPCItPpj3JPqh3kGPBrXoWU1Ht655DEXcz1pV/UHvj4pOpPJfq+rRxLoxDxpUAePqE86NjHQD0dw/6KJ55zgdZY5YQBjTkoOM7/v//4HcHUbbPUcvkHyV/HzkjL177mkm88b/20cJbW+BsM1Qy4aSQbSlOrznNBMl8bwGMQF/QZn5BYnDRBTFJBgBCSqLNxwrFQqv0Mq3jOT8zJQxp5n98AQJ2BwQBHyTAyOz/wmWzNKZJQKEXqrl400reX2CzEskA42zTmi/Hx4XvU3roctVIkjGbX4cdONJu11m6SC3rL8EGWQFiXrEpGV6XZFsFL4HMoupd68H494n507uU+qW3HoeFWk/0Ns0NlgCOirbQKYQXDsAThvSVQSwUvfwATptKTlsNkJtRtoOsCe4+YDMFKcEsjnhQB5ohvM04YGrxWEGeI1GRwGfWbLf/A/srRw7itiMo8aOLvvFT9REz3laqCZk+ENrqGjI61w0q1+xMgzMZTkfXUOvCnty15r90KtaVMVyEtyJWj2GcV0Zgzi4wxOPUpxyMpy4v7corcnSN7Edo6VFew9X5mjMSnT4yideL4FJDx93GGNSL8BBrXiqt7CddI4qnDMhzlgN2qay400gMTJ1kglvbxmEcvzLdnNLyJ7g/ET5HebcH+7VL6sKwS/M+N/nwP1tdN/IBTyY8/JzsZujE2sMuJmq4BdLILxaYSxgitzDh8rzy775u63/Ze6VIOJgg4DEujZ4KZQzzx+y/F3tQQeDbmbtHy/T72BNQn6ikDim7sGxayxY6CqY9YYqcyVmvtAwttrEqjc+CiW8jW17utSbeOyagap8v2PXxGzeOq6EGrzu43T8xXTqalyPFe6TtVukHo9GON8WuUHxOuU9dgyEV6ekstmOfQRN7FJ15KgUs9GrYKdFuSUjJcTNq7KItaCyPz5XIk0ta592nS8ihrEGn+b04rqd6ITVUc9RtdwpolQ+iC6cXpg/+rJcCowG4qdHbl/JWtl+o9/e6A9LQqiAXM1lV/2UrpERgqsWMLUfZ9ISMTw4CoqRCabeVp0W0FccrQD+OT5mDlHmJy73XfsZlCMirwyeibEIPeEOsQ4KS4gahm8eo8okeTZrdjcDkWg4Uj1qRMzxhbBFEcBk2fRZkoNbOMlaBzeAlAS7T47KTs4DF2e7A9vJKxPGTNt7G3I0S9KznTh5758zv7lKzcKH4MaIZyO3UxH7g5cx+E1P3xfQHS03bro+EZvTUKyKBp8FY8RbNrAAUaxPL9RvoaE5V5+I2FxoIwuiODxYDUiok+Lp+8dtpSCgbt7PXVRfBXAKCIBff/LsXS2gm4O4KDkvKRHQ2kSlIp4gmRmp87fI1MgnO3X4yEdEj1mlEorWyJdNqbXDX+HuTQHdfGuV+e9BVMEj+vX1yQYQfHSQt0B4hTSe9vXZWU53E8nyY5Ylc5CYJj59YQ45eU9UiZ9g1pYEYDkVID/z6iyAUTdwsu/vociBc3nv5dRXYaxjruw9pj5OJdftGrJ3R8wKDdXS71LvAwp6xIny6PCu+/niF0770FusZ5rxT2b4WD8GWf+F91WK7T+bmpM/E38QB2Jb3gn9dWevD2ij7RoMY8lvBxXwWReGUoX5l9PCR2SlCpyOapZnCGU6nvMZZJuIFX4s56DT8Y4WbxrWgAmW45asnuII+9XQpHmPpPqfYAh5U6agrPwS2LStHMgcceAzXedqkoGgPSc+kE36XvAfPj72K9lI9dECwz41U/2yzS00esCKSfWOKKHIaDh/U5aD/4E+YeZ/ix6YuACQ87qnpcU1ukGXOFxrnMMj0Q+Y4NhtgmeKSAQ4XRqZgN8ZiDGnqI7txgjVWEN9oW56WgpngAlRsuMRsiF9OFyhou1cR1pR89DUhl2JUZVuwbtIzrUa5gw0ZwOgzqTVwNVchy9qH4TskVOM738TZP7PvpyeDdOnw7BhpIJPFo5Ia3lutCM6SK3/eAylevl8u1HgoygFaaasjnODStHlYifTvwQ5ESoJC3484Oa4BwqoHUX18jjJBHLdgGmvLxr5eLvGVyZRxBEAQ7jS8NGMW/Bghg5LvV1IzEAEY4RYj5KhOPIxxoZrrxEKO92k7pLfgqFbXcd2crVLQPWOFsCc+xC9QdnDKX8Ua5rh8UQy6yIedAkocryTc7eB35v+XTEPGSyhrKjd0s1V9M4fiHAXHfV6fFT7RzJEQodhrmNyxX1zAczTytxmu3187nJSZKYmOdmyibbREmiAy6b5NmQIlAmFKpym28zDCjSlM9FhD+eE7O2zb+bpqyPi8LXVDancRjdwypOlPPc9/mSK3eV9kSGpa0ZAgB9Bi2ZPamyof5CSopGjTnk/mynWUiNk96+4awAEUkqX5ERAC+/qNFdENAo79DCQTK1SXRNEJtLLM6LIM6ACQFu7jPVCoCqWGy5pSSyveyzQ+gk2pzdZ674t602K0wuhnHtAjZ3Ze6U+9nIC0QlRBDyKvAiMWDtjTpIt4tMDFNdgsOMppaYvJaSv0Gd+z22ymipWZxe6TH2fJRV5wy+j0be9vYdCAgyz7VAGsn00mSaa+T55XxESQmw656nR7Zs6hfRozm85lyutO5shTAxIN2jHjZvWZaa1XqzOtixB3S2mWu21rYX7Z+e2l7dH2i03YPlmXFa3yumAVmDK2VA2oVPGVD2qEM1JEGqORa1BfeQ+CEop0zCMrQYHxZAg0fNG7ssDeNSlgf1ox/e6gmuBAWEfNPAW3b6BXgavTbQAqAEGPJ29uvxpPP3V6BOppZpA8onm/rVpNrGDzgao829N2z0Xrfcsi3kv2yGDzx6oQebhXF26KRm8tUB4vVoNb9l2fz/x2wFOPf6N+2YsBoSBqx3zlwUbK+xNDhAskDxmkS/MAuQTq6vcZen83xyskT/YZd0O5ObvDFNH0c3FdOas4onRWREQNpmemnrdSecOGhHHjkIbYyf7CPfYvO3HLvb4eFdlv7SzUgGo2IXtJhUItQcPGoS2BMKpStCi7AwHLkTOPamaMcMIQQQix6GIHOFmqNaZ+hY+jzNxdaDF7/3EG/7o7/CMgo7C4qNfZoN9Ia569YIkqFHxEjHYUzviJyiyXPkZu5GKuISet+G4qDoOqROWfbVz6cM/DNXrsasj++29VeR5Dw2Uu81k9YSW0nA8yPck4lR7NjIJSHj9FPqDmFOtOLizFqWgBxxIwQpoMQFVr8DVU2tLpVkZexLVK5EGHTDAHrf42Ks1arFU/SBFQ/VIqhPWaB84DtGsKMKLB9WVKXsdBbY/iN2TiDHj2hnkfx9oKQD4vRVv7UV3nc/9ZyEQYZVdIBoMHjcaWPSrVqZrdh4tzWOx0D+gGt7P9V9wZe5mi+GZIQdyOLX3OrilW7EKt/k7em6QZYH6IuFuKNcVBi2cPh/z2z0pS/pgKhEBR56uGN3PZjt7TCeJ9MdmTCyDUhdrglAz9df1S19XCEs5o3UN1sOADvlmyaOLPXrjMV6eTS+HG/3odN23+mpYHG70dbmD/0jTskQ/CLOAMN+08dGJzRzUdcIp7Q4DcAsqlQw0uVfnKL88sQNNZA7wJj+HNF8z+yNzOmbpg6WUuAfKraRaFIQjcDvdlxzHYg2TJFDfLrWGfCUYPwfBJRc/F7bfekkbXfRc0LJiCXusrWXYu0FVgagZmric6AQmVvpVwneBHG5AJ7Xue0aVSdz/jP4+DpQxi8f3mxupcacmjZ5ftZVP5J9i/rmQeriT9mNKAAFEDfKl3N1T01GyUh8kmfQIWwECCN6Smupaxfly65s3+pCWLQ7gp9SIaR9vQ27rDvvR3QBFJx7ZEuDyzl/5gu7a6WI+xPjkVJKz7gJ0w3w/PPeq+AZa5N7aRaiwleX9sVE1VQ4tpaoT31aCjZ4RbIkOkJysFpdL9qp85h4Jg6qrFKKZSAZdLNpgn2reCJNLkYiOoCZpltsGOEaqmd4cmKgu7gnsb2aU+cJdCSm7cjo3YHfx4htElY5SqDj35cUuo108K0u3j68rBFEG9HF53jpHkbT5s2zAeoNSzvwy54UIkDBNVGdmCEljxKWbhJRusSd8qEUgLyLiQKdj5jVT0rkVEnn4AEU6rXK+7lHIZQDYM+K+reIKGGC0y12JmEXjOjfoksC1FAvTFAODunumKOXm3bqlx2cBQxIQS1EdaONcNUPnnBrUGoPOYH0LrFuJjKlfhJk8SGPhFMJ1G+WZuHpr+Pg0XOuc4pi2UNUCpvKliww8DuvDJvhDX1AZwX1chG0gf5m/tfWmk+8DwC3ce6OXg02FFmUJ+QmIRI+RrKZRHvdWzljo4yHoLMriSh1X2+5+98cXErcddhsQQqe+d5j/08WoYYs5UE/ApPYVwscJId0g/3kVUZufCL+tinNOEznoscnT/iNyZb+0jEr3euJxhDOyFt2W55N2X75qJEjWlXVy8cI3yeLwpCJS7jekWQzZIBK/H/FKzhAR96nmW7IJXUyuURvRXuPUWahaDewxDMAWcoCr0iXL9lyvfzHIrQjKldELYkphJW/RCpr+mvsIBlOD151zALIAq6NTFcjefoJ0no8tB7IxBZXxKl2/Uqc+NV96V75uxW4Zk5N7dMSDdLpGTtktQvGzJuxjzQyF/lj2VpVa/2XGA1j177iz5S0uCMzaRjyDEmJXM4wrbY1gJanw2+TMaHPC7L4Xf2NTMJbiOtVD+9adyVnqHSm0sZOSIOsibc3g9TJrU+sTbkutHtGowb0dO9Psivbz7eMWC/GK6h9eP9j1VEkcOErPLqFIJ0gDSU00nVndrLQzEVG4aVLrNhl97XZ4YSmo01HE1qfca8bHOZkfQmI6Cc99LjJXlXe6C1pUFoqxvMQg0HjgvphnlZQ6kmUA3sbYruSeUq+jC/2UrqDcmhy0xQYDBWfr0Zyzq+5fh08gzydDSE5SL59YPnqJvnzZcRpFNerYjxslOW1TQCR3q/AHYkAhoL6Z3KMBpBmlXoJnzRvY3MVihu3W2kNVLC9TYdY85K1aq+9V60BO2wbKYgV5pOLDXGstLJc47O3EYzWYZGPXF8MRUQHL0XmADn2BAnQoegGIlOO58CtQdJ991vlb46BMJVFDnEH6iaGm6hrsH4ZCXFIRN1NQqhgfIp15oIycdeIaVgHJQy155LwoSE5ns1cf98C26b107rQq44lAAJ2egtVA04jglx5ftAacl6xYsYBo7ZXcq/4WibqoYQ9KiXvb3LRf/dcrFwdNqp1OGdUMZLk0C7XmNcsNySH7I2JELoVplTOsGq44ZpilORAHrDg12t9174E9LbnlCj12qLv28BZE8xdj5aibSp1EpQEvMtdsNA1tDNqZbBD0ycz9Ca/juXde67BoqePpgEYVpD1570KY8Rkcm2IlHO1Xngf4KFcM+ngCwqHnOqjI/nDbKI1LJgD9nOJ6J7nMd1h3EGbRcRotdZ6MB4xulPpXZgv/VUQEJYsVpblZqVsxnh9FEvGyIYej3ViFOjxSI6XYkRknYn7xgbPRVd4UuNcqnm9Dt5K3BAU5oIkDd7h3aUPbe1hQTL4OJOyiBNZ4HlSuxLhJxLc2n8DmenNq4MVKy5P04v5Nx/R9gyZYhiHKJT0G+lNcmsDrwhpZMOHCniObpABkybMUE0Fqxsgn9D0A1W/sb41DtJ558QB3MNrHbe+8mfeT2fHh/gSZOYY4RiUJublwP+zNl3rOE0dWcPI4c9/d/7+HLtGsMmy2jshpSzk3QqeptL3mrzcbdXSUX4OX29oZt6L+KJkemQk3wsEJNMi2q7LMDeGpebXHEPZrBTASKO1NVCeJW8i6qUxjVnfvgYej3ylZtiuDabYiagY/Z6FGzxAsNUqmnWYhAjkCT21uKwEnrkKRHea54rUpwAl4KNrm/sK7OD9GdhY8TeOWjmX2eNyjBYV17Lq+ikYFkKWWF40su1drsPgAQ3oqsFCB4cxylhfQSkwM5x6KuhaEa2GMCxWuyDIopE2CG77j7oUh8tdhXOCpupECFG6r7ua9peZjzexRAU/9t1S9py+2kGN6pgml+oMUIDwt7ihbMMLG0zmpNid61fMocsLMPbqmr/sz26x8Wzj8wEr3gvPdrOUzcopU0f54na6qZWksk/toRlphEUVSXxxXlejBodp6MsDPrwDp3zpUvdH2EmDDbfpar7Z5/OefDqnNDqKrj81mcwivC7mlbAXaA++2Z3k/wE21rBfgVp/HYcJ5HpK7gcojRc9v9zqNnk2Y+XT0CaerZ3eH5OcZjh1MWIRcBVPOZpDcH1N5Zzjdt+S3BH23e2w60zRwZcJIRgOy7q7serxcpw7etYSdonOWDApiTJ/2pY+4BN50dZ3gtyo1a82VUG6PGsxcQNi0snMfFL1SemAS5Xc3ZKXF+qmwgGUVl8X8S5sM+asyiYBWWAzknkYk5WIe7oYziZxmOCrkRVgRcrIfAUt6uu37Y0rS/JMyrTW9kx0EPujptg+1RuOXvFD7AqMPRJmhACc/v3nURni2ZBFjer7ti1ETXPyj7SLvN3jnlGa72EG3fKEutXlgAyhiB4sMYOdIItxUItJEYsdU6vmc1MiJ1JQZqdQzUvlmpJI0FSUBAfbaumUMeBz9a65NKhfvwIojnv8vTqE5nqdBgapKAjDEByJ8olrA245w/svj80ObqBe9r2hIkgAJMr/Ia9woGRJCPJs9ZAWxrfoN8aslONMD5C6qDxB/CFBTjSkrfJs8tqngFSzC3iQ8FQGtgL87S0V6YGvVVZ7AKxYiL8gpOq09ZRJL8W7hIwG1G1g9IOoBA/K9ZU29FT1oae9QQ8T2dt+kOD5gbE24De70dqqOoeDaDRVxXqrsoTKuYpVV7kcG4E73wDSfPNqsb1DYt0vgEx+FbCYqKz+MZh2FqRoHV3dY/wVH26mSOs8QdZ1me6uU9rhPpLYIEVdAaSKjNGZlhIWYbX6ocRPqePFjHzqbZqKXF0poXDjF1KkazJ4VF9Fy9dE7FwAfJ6FAHTwGeM4SKc/BM+hKu9wDfNiZ2xjYGYE6XgB8XKrYpMQvctZjVNQjgM90htnrPBbDYrNuiJ3FCn7aI+DUFwFhnuDS2Jh3CcmPw70vVz4ZHsCIXtYxoPNFwCe66hBw9CZgMmYQ3RuiNypCRB9gmiCtIsUXEonoIszFKyhOq7kseuhh5bkg+x6Kw2ULjmpEXZqZh+nO6fcwZKwCce9sneugCGl6qzY2YCmwH+ptjWmJ4f9NgXtSFDJyzkPjKL6jmO1i55iaxuMdBWGhvSngDxxUxyqylfB6LUEn2DfFJVUyWPJR9c9WUnaCL1pmD9OTgCSvhxoSASlMiYoEq/QcqDLSYpJRA4R1AE4Kkun3VjNdaibSEbINxoc/BZSrskdyZ7KfzfFe9qWpxGjYfm1oCgPpBuqbXykiRRaqwS1sRTBnOpQHru4i5rbpWcdtrUY0DEXbKjSJOZMk3l1C/+e56i1xZFvqNXFb1sYsEKlvZztBSrkbXaHDvhkb40xJV5VAZRJnQyJlpHlmFewknSRcCDTr1vntc6VZR3YqW9QxYg2K+KP6WkHWwywkXki9a9K2oEnJROboHSy7qIn0bJpvOIZ4Y2q6O8im3EbNJh+hJTuHZl5QWCWwybOfyTwoJKN8FTgOoNUSrpqr/nNF1ri87Zs8uvPnQtdH8Ip2CjjgfJtW9O/euzBrsG3a+DD4l/TWyj53NlqvlujqsdrdOTGzCzKcrsp4/SUCZbvyRah7OwrcWPXTKAxiwHerSLOyaeUjfPDnidK9caQXISMSa9HjTZqJX3S0XRTwDPgVKjjxKBzE4GzyxeoJBzM9Wv/TTKjeLs2tGW5R/zOfjnJjjnHmpfZyW4dTOmltuJuONqAvB9WRO8l76OV44LbZbX/edIMc/9DkkgB9X5Dp24iOu6K9BYhmm0STpakUOoSZSIKmex2woxFQZ+8zjYYTxE1rihaUQzl0Stpf7ghIX6vNGUqbILT3sxT+VQ0dVxXkoobYovL1vsx4s7Etc0ZChWyIA77FaoW8gnoxrpj7E9tqVNXwi/nWVdZKoPbVMtOuUE2cdp+qpHlW++ssakSP3gLKk5/rEIgwDpn3kYKXcbfqQerhXJjxKlRQtTGHgZKzTeTtOQtOhCuMsESchirHgp+ClIoCz2rmWVE1hrjgS2ktP+54Wb8adEhx7a7q+35m/a8gImdPCyU3DPFCZDJ/O/8j7CjbnC5fvy2sFIGmArIBcRPk4ARU73hxrhiGpnrKVTgxoZ7gu6qb4j/X+PHxt0xOj/3W1I6Xsnzh9+f3u82zazKoGMTjFowBddvnpp0aLXGqgwZTKIj+Q/aABTojbO4c75fQGazkoM0V9ckYrIuxW5S+Tq2/FJeojapJa3MYQW8TIRU03CPdsSFwGC/34nVg6mMI01c/16b3Lmkx5KfcrOElUcZC8AE1KlZ759+9GNfN+8RlhC5/iR1bzke9Sc1iYdaIrNO9tQvUAufVNYhyNTTsNrEr5Ta3lzRtr9DrCuQJlzL1Fc6huFW9C5V9n+UCg8n9If+IexbIfvCG0lTaJkfbluoXf4K6mIsI4ruDEoS7kJyxOiT3yxtShVq0ZzzMDhI6pUfxHnH86hWNxfo2zkdktvKxT6BGiQUuVQlioioFLmfEiw4nxf0/dgo4+xCpJa9/YfJOfrXBFwoeC5OgI974WcwNLBGMivHPcxYDAFmpxKiHJtxmsh0nsuLqGrRw8yP+6YjEC5kAiLMg5VgOOxCeovkE7P74OzlKBCvvlRqboEJizP243eyde5K5HLWwJ9nJi4Y5h5fYyoYIfNchCeNUvzeXEwzNmfgBpxlxWW5YT4E8h4PwIkSDPag3tpVD05tBQG2JCzd6Bi2l5uzWxn96e2sZOOdSMbtgW51rbpipX/sKe4UciD5JfiFtaKlTheuYeadPfHX6rKUXpY03EYkAjdcT32nDTWf5y7Ic1IR32AJjn6S1uz7WaI1ejNOCel5Efu8dlUpl/CKl8xhUuf8MgwMjmFX64SviR103cBCIoV6mUSkqlAHw2QLCPdEzy7yofd3sUWZW7uMrdIn9nckWajxz7TTF/guEXVRMMhKV8i5zAKE5gRtVr1DhrVOihRXDKnPZHqmc9UkMhW6cVWIDeVPNFA0DHCB1wHClU5fYbJT/Nmn9N5omAeO0vaFiIyJCFGZuf6v5D57trcvXlqppdEf2EKGANUP179VySwCoUreStz1ic1JyeVMSzitjgEsKRRTkpfxHCeYyH2mH+qrEquteBdXqF2uTnhGM+7UhPMG08/W6fGFudjNhwYl105AJWqoHIDsx3Y5sgvwQC2z5UjauH8Qdb5W4QRS6cyT1atfr6OTqRkM9Tm5e1Ns4qN0r+EAOMsoARcg40KrUCWvzvWkMtnQAZqX2bD0XI+tRcmV4SXWIcMSsF/LE8V30qkD9ME3s4QU3eic0uirjlkxY26EciRAf5PaCoaOVTliJdk32Hiypgg+eHEbgzC+cjNKla1r/Rs5OU5ZC1iYovlOSh9jZvjkj0vDTifxfOwGU1vUJgQ53hetyxhhQYuZOL0MPtKwvikr3sEA3nhtX0DW6xvCAHw5Yry5I0bfqA7V5T63waJuSBUw69GdWqMOxG7QoSU+AgjseW0RmMKzWfd6FWsLn6t5nsqP5kUaqQMI/fiIH70z/29YSMRzn+WwyRGdlU0jVWK2uz6OnfRzOkloPBhxDYtQcboer6kiZqan7V3I1j7dnbbnEAVOiZrHOI7Y0++kCtKY9TSd6bVnEo/gcu+33cJdMsU1+sB2MzA5wBx7JPCWPHf03KzuuCHRanu2HeDDbsqt9I8Xfg8NVMPABUHJgaLYXvJ+Oac4bQCChZ5yuf+5hkgjPeCiEh49oYx5w8IiAZXoN548P2LJ14CQeVe1bNk3ykQnH6ntTtejPp1FHNUVhJU6UYTL3slqkMVSs3VVZekSCSH4HLW9XvMc3MOBK1+c4K51/EYEL3OMO/uyuTnT25xp2mueIRgGE1MZXHeXok3cuVTqmN3Mgru+OEUgbQRyIwr5r1JmTZGDU3wDvXKQjich47oOrDGI7e+OCv3JLQ9VeAb8VXx72FCta7FWNr8DYcmPVDB0EN2AMcSiaXRYiJjeXD5BM6NC4O8IkGf5FNIMQPwsi25549aAWGga2vX7ENeXbG03mam7TQK+3Np/IX9XYPtbp5xhbS7RrprHS0r5zoMf1u0mWjdolgQ4kuisqCc9NxiHeMyBSTiYY0iiYfhsoQIwfbq0KToBT0JSF4psFZfTMl1O6P3Fw47VTpmoahZowtIompPWP4UEYlKBxABcFQakzP65uRCd5IrbMKVMrOLVt8t+2ptRq8ch/5PEBTtTkXdn7MxNpl7cx389EXt6/j+VeHjyC8MtPw1xhip3H6CEF84ijO/Oq8fLIxN0xQ7DvAWY3wikkF/PsA3jfwvV/xyEpqAbSXXN//IDBxwjjJm9BPbQNRja+UlN1rB8OhcDq+inbQWev3tDawXzjzVtxUnb2C8gTHZrTxXQWEXYTN7GLDSrVZUBk1xAmJHNKuflyNLLG9RA7XF4NuofbgenEV1yDWbL1h8k1W8EfV1M6M2v2VaMJuiH5hO9r796g+GbtiY4YNN/obQO681HSvAYJuMrbtA1k/vYKa30qAqouKfVVNCUOV8nNClpP6pvLPJl4BJe4OPUuKvX1j/LBfZ88+x2uPx9OzWsTAtjD3iR/gVaJJx79RQ/XxA1kbq0e0Sm9X9xE5DDXxEdFowkzql+r5fkSqMMuU0lZ4YG/gY/AJ6BnOxvr/5SZUvltgQehMpHGNClGjXPP46qxdMnhDVBYkJtA4XcXuvcL5VSAXVpfZPDjZYw9PFoEkHQBQCI1r3d/r3S1QdYhv/xwQqgXsoOtkbhDsBSfiNNzx/Z5g7XfbHYHpr8SW4Th0u989zrwZAjkGBSQTDdaGMBYghfKYw9fTDjJ7Oh2x1ybafqwTwtkVVXuV0kPyi9QtpQa9Jj49/aTOPenTr26kEP4fP0ioeQ0oc0xe8NBsRhF8c/iM+KdDhPR/4dSQX5KpmRW+Xy1WDZtaot7sM+2pOxMe3zQh1FW7rcr12Yw2/dlFi9Kj/EyyyQHXDbiXy/KPhv1Q3DGILf6ISnZnR58kFqNKLD1NVzj4162fBnpy05fRnbeGYHjRchpK2bAaxTVNb6VjlyfPwufL6P3svOX0T+hp8KXsfyy+y9j5WUvPYG82wnL25nExrlLiv1RMf4tPz9CSF0lGDsrHE7hyVdm5F1jDtXNmG/GcsO1/OfLnPg/sM9yeny1WfDiSSqhLMxNFmJh36KP1+sQRmvyB5aCbDY5UMR72FTBpqWKu4guJYewa68g0Zhur0a2BGgVkxuAtx7tLGHMd7AU2XT2kNcJL2slkBMUP3u8rmr1CNIEOGgR0pp/uPrSZlQCxxWIcSwXurZrBpLsMdHIksw+OLQUo57k0gInKe0hBpcUe+AjhcxWB9kX7JKxwOeM1Egh7qXT+57JJAALNLNZpTyCBTvWlsiZX4NdSdENtkt14UEnkR8jNyMaVk3muWzznTU4p9yezjRZols+BZ9SMcOf5UFb09I/V/3KPIjjq2JugzzDJ7x2gfZwOQ+mXBItmDMmBizdfyqEamSe61SrNXqD60CGDeW0++f7mkLLi8D40PFFjobVdSKUkyZ0XQr9uuFxXaOtC0KGluq+ttTLqpNVGzrPrm8i9Il3XS3IshjQ4dWEKjXPSkNUS6iopS/B0aXRRmfGUVw+5dA2sfqZeyYW/MjTW/4XjLkhRr15zDybzP1KDpXnvW5+xmLM+MPQpthCvQ//VSVGlG/KzBBurDCTBwXq3L038YbmyH3ZYoJZdz7//ezwjI9SS55WF9L3e5SasWkfzsVZTWbUBIM7QAvmvs5iilTz5ti4AnFydz/HUEILjbHVKA+bPncb2/JmCEcksTiiE96NoFbNqmFJubYiR932sfbQ0FqwRp7oFDZIc2IdlE4p/4K+os26kKBmXi9igoH/ISxi4ukZi5iEcxGTOsQxeZN55EAv13hrZSDeqjwVtVDONWmDTR4suibSnk86EV1cWmoV2VNtWAz+R1BnopoPC5ySFbWam8fH3YI2s1U75TsjdWg8fLQj9cKPe0pwKIstqR8QJ3Dm3+O74Z1UDBydAnY0J5Xi0JiOrDCLQlQJ/tuv7QfsZBSm9GzcFXXYWZQM4tsfPmUfYHlh9cgaxoYO+1MGRqdkT85UY/9eqUC/mivUjfZSF6jH8MqBDvpB5ZQaNnSOi3Yw7eVM9ZYZXXU3e/+I6I0lgbPNPHBkglrGyCGgm7xPfFhnsRYc+tdv2RiUZu2Zi6+azwYBDtBo3gSdxqPV6noQluSnUgLv3/zGvgFa7xW+8SOD4HfwdXGHGtBwHqHFWS2I/Mhr+zGtReuoru/JRukj/XplSlnK0QpyMC7PyRzexpKePCf0ozBfWG6hwm3tMN7Pyz3a9LntlQIf38SrSp/TtpZGWxoJJc0l6bvGBJbKyFCc0bsiiEi8TfMmPKJV3U7OV6p7J3gxZaG5C8ceFcadqgqV0ATtKM+cCh9sBIPOrh3Q3O3EBnoqkPNfCWtc0W5tIi/EK76dnXmFB3fIIu1KZ5/TCQfAePTgSxtSZwDrcM0ARMdJG1DRliyZkC3G9LWzh76Qa7J2cTnY4GQVbEcLLZGElQxQkqO9YcwCC76AGRqUPsgU4zeDYo2PQZqhwSZFr3PkrK5TXetInDR72btMLcEC8Gd4NXze1DEm6EQJTyyFcO0YlrUdRpETkUjuHbdDXNroWOyT4/+AzkF8Rw6vXwrRYpddSARggZvYcZmP0/T0zflDN3pLmnaie1ZawSN3XJEkGoNXbA4XYAl7CrHjIT7G09bWY7R1ODz0BKyrfGbaWz2I3ARt3v9+Nlu6TYr6RdmCw/ffSbA8tNHOiXHH1IHKZSkxIqdgapS7xsIw07VdzW5dAGEbsBuxgNsgvAPevv3hbGXorVuBEYzHO3W4GPDOiPQEI5uq43Onrv8H3hpFN3Fww503l6QfJgBWUX147hyseWnpz0MK9+2L5HOH4Cjd4po7yUroxV/pck8Q2Uea46Jusw8ygO4f79KHv5NPnrXko59eZwerOasEov9STI4S6+nU3RybqjzZlTpvkdjXy6+KR9+VTtw1eBOn++1p2fH+ITQe+LJaNfUJ9sKG+ThlzVvd2RletJOiBPul7ypgPUgiBEns73moFVZuNDZ1DIAN50F4CNL8JolbTyNPfel3mVov9Gpa2qI+qjSLPTPU2CrT8bs/95zUNLXNp54leyY6Zx9aNFWzeiv2JvdeNov3/uyRjvuBqbyHlTna0ilU7iRpU1Q23/X+XCIfc18Fzv3ryoaizdTZtvHJ1Otix9J7o1yFNE2OxnuCWqRkM7qbgvp6Laal0HBnFGWph6VF5PPtFOladqmEY6o7axK3hXiq+4vc5/SBL8eUT+NkSKcG2e1WBjb6uiYp1ja/X3mzZda1vIUd63gUZlREnOzUhKnvPviY/vz87xhQV0ZK+UFmqsJ2+DSvAT9Fd4+ulmHWInvx5VTdeRfauestL4MtDA6jBRnHYhjcXBG6F8NvAPK23FSVfJYXgSNUchD0FhQjgN3w9suQD6gjulyyAI7164C21M8utLck997bvllWB//pjgVfrCZ6gRgPR/m5ObdzmOpIBMKBXzmq/wsbdETWD9vF94cw//48zL+fa/792PBPzfK7JXRl8sxW4NwlZxRrVyInKKYjhyauHsVuCG7kAk5mITxhRek9/lfUbn4UeaeXWKfLQQLv9SB0R+i05uCFB4hMUXC6VA/osZeqP0KyeCbSU4OTmse4NhyGikwwDRcJEtIvAv7d18hFpFtZTXb1uaolQUWNguqwLROjEQWtLCFvPwIvxnfAjCt/jQLw1f7eBF4WYy6x2o8QnA/VIfyzbErsjnb74KMcB1lVkUuTqTz8K3tpJtOu5nPp1Mo0gsa/FS33UvauduB3KklC1wTfW7OQ7Lhh2J8Ave65oKKnJzME/v9uVg4cvTnWKGfpzyGDCodhlI7kms90tdlmDPv1Ns4eTNSorY2XOkPcGEut0vyABtqTzwYuMhC7DeJBdiUYbBSrsSF4qoNz3bJRxjnFOPTgoDieo2zT8AED80kD28XjiNTRhxtG+qjAu1o8D0ZEdTCACmdLCaCI8w14CzsQXbMptDAqZDUH4k1h8aK8GpuPAxxo84Cu8a2JLyUxpmW7kRwxMGdycLtsUCV9NkAeJYhXizvhmO0extDIn+oo3xZnKrKkJuS3PKGNHGiLovi2te2l34akhIG2AOm63SaKiJYtS3sp24vskIF5kwfcZcNq6bMhKqnBvFr8KUkq62IYFQ4FSKGM+Q3wqCbS62IGLWwKW9CBNsvZAKQdblMcK5WMvYlOrxwqrkygbIlTKXIaloqK++Sz1+8ZaJlY62lpJAZfeFdYAeZOo6gxkMJbj0ABuyNW5XTZK/ZLjzulsBoksz+XPkHDqxXDxq3nGYW4VBZlbW95tE98blICzU3FnaPRRMBUZ/+IHdN8MFA9e/fFVm7IwsKkGbOcnp7jBINJGSM3EypxEm97cmBXLJ4v5k1opk3IuZFdqFHgIPw6ECZRFKUil0xdcBcNuxqPVzOSppi6wMMyZt45j93G+HcxZi7PSdjv829Nwcf0TbExuDfT4OEtu3SoFhvnc9M0RQf53RHTNuXYqgPYMUqOju/zZWpxxYYrnJI98ESRA0NsqWOUmsuxlGPxiyPiYJxS7vo4W/EYRkWLwoNk/KSoGgCXke5RNb8yXmQuHCL2hCJ+rICW1rgxsx4Za2BSPpstzrh53992P29DL2E+VKDuFmCiZiagohWeAv+0tsGmBS0kF6JOpbitMawJrKBtemW70Pv4MMJRMjqmM4ydPYGe4WZ6x7Gx/z2WaXk4x4XvrOInKrx/96CO/UPe3PAmgJ9cEV5G7A2AMVkEhCCcCMpVFPBLrg3WcSQNK1jpqK/ZIvWWKxccPFjiHbKNIFSLo+BinKTZiUMUSBHgdHqK8/bk5/VDzULKBhitXzI03y9igryPFjEZRkJl5VTEiljQmO2TUwpsxHxN7v5Sa1PnQaHA7mflo2+3mguzRQ3FY+zlnk/2duuzcKynVF4GuYQDaq9QSsNgZqEQu5bbANGXqs6ZgHu3rU9gDtf6n7kq0r79+J4UEt7RbFdrUXGxsgWxzl0f1WGOXnFU8SWpHN2j3IKCvccwN8bRsI9c/OmPQnWvYJbQv99EDcVIULmqVHzEg7eImTQH+SY+x+LFvuhS1sAGJ4pbA5+Lcs1iofYVsOpU0c3sLuwWnbTqvuZuVfcjF6qg/KKUZVcevwbPrvleAFH/j9ZkggnDiiGUJ4RdZ9ONxSKcjy4HAQ8aBlOJOGSyBY5iuwGrAm3P81vSnD+ZlQggbMqHdfsTY9u5ktdlVodbsH+tgJRC6okOkkBO+ZlR96a/aGHLHfirIujZw+pA1Ha/wPS3CZHbkgDVT9GAKUWFh6iFOwA2j1966KX2tkJXKSwabzX+XD/OujEor2Rc+buJxk8zNn7Pq3dCoz2wUyV9vUauCR6G0LTtJnVcTad7RxqRcKeUplKCPycQ0xWvdwECuH/0lGlDHTsMjWVJqnNBD+AXQLsJWNqF7v9at8KWrnK5lMWKNzflokD3a6IaVXzdemvqehrer1M/S8EDBMoTDcevj8e0atDmZNUoRs4xePut9DzupOExyms+QUqOj4Kg5dza/kYqY7hFcnBHJoQ7ILAKdwE0J8V2rFoDjHLj7ELCcFehDRHTaZZd6B4FY4NokjwloEOsE4r4LzROJYvLSKMxYtGxQbdpyi8f2Sr6k1NhodZJ708HKUHdBGJDRsOrgswsDyI/yo8OT8Dhr9CPcqTsVabSWt6EDph1Pt0rhNVapPFV/qJgzxBf4lhfUfIsRPlfZayOH12jIZCLwmRXCMns/l0VbKYXd/Ohu61yhkCk+vQ6YDCNMrZbV8slsbh2G0/kyXRSU95PLazv0Ye4Wqb9mJ23ZaBrfdUADtd+ftvO12fY1w1Wa0ERGxAuEtSw6+uufRwUsWT+9pjOeCJTUENMbDKF2FfYvVeKkwHr+8Xou35/9ClSXd8r4SdKTq7Sk5KCRbZ7g5b7NncHHZAfNSE+FqbshJiSmTKcguDgMEiPDDjG27p5vTlpxWg4ZfhkKg1eZl9ml4Ebyf8MHuZzl66a/L1QJ5mHlR5NH01P8Nn/Ry9PZxfzqbvDTdpyDnB/LXXx8XX5KvrUmr7kc1iDafduddFSmzepSWfos78Oi34dqJXUk0Jb9aNJF/rZX/ltoiTafmWKXwxqKwwg5gHl/fiB71SALZKwtfycVCytO18h7nkInGhBEx2YYss1hPBZKjMiQ9CrMi6urLA70983JAn0sOcBilpojps6Ulov9nZR3HiMROVt6+BgtlTZ8rI0cklYvbtQZrQzcaBCgKxYr59BHJpdFpaRITH3NjuQ6htVohbeG5O5HjFCXT5D3knaNw+datPu87T727ddUdP++cjhYbo4yuqPfTCSRnvsmonr8ra93/OxLM+dTTf7pYQ2Us/DWlEgJL2fUfEXNC3KEvC+ghWhXrPpWhlw1YqvkDmrDZ7acXBqq4KAXtrVC7pVKa1yWfIJ9R7Lf1WsTIrRh1dwFnGKrj79vLy3eLnD7UZFDLCNdjoSW5XKXo23lOTVotRCV2W2TJhJipkcEK7KK3oQkDb6kjskgJ5Ev9O9oaNr72CrlFAFL/42qleo1cuNSH71xuuWwoBKdqLR8yABj/fGDjtsebRnX6qGlcv/f4aJIYk4jIR89WKslFcfRWZUXH87ISyfU99bLoz33HX1pBtfnUHMDfdOG7IaKPQ22ADDeO3oAZcUrdt93ENvoSfTUr9rCKXR0sKNLhRt0jO3v/F74wnG1TNQCcc6TeZINS4amcRbg8df/WLhO2LlRgr3fvif7vjn10pKneo2dtPdBK6DvC0Ge7tqM7sbY0t34CWMXQHSaMgbS61XEhSRnt4DrbOrbDx8RPRAiMpcR6E8amiwDzsTyfJS8orh2aLFiIf3pBc3mSqG/eI7BAPmdJ0gS1fRU64BOw7VXrK8HA+KxcNaW/1/KISePzIUphdkD+kelLTNzmnvMBcMM3BIAtAjyaxrwJev5CUTIpdHB79VNilGqlc5GjPtL4ABjJ85u6Yl1xBMnNEdxbO2P8rbcV2Mz11I+qrMMJhCnl5f2qXBhOAFJemXaUAOuYMFJTk31/6UM4+qtMI6/08DoI0ZWGZLnEjIbvqDlkbw4f1MNwDfm2N18s6UbmFd/LTgqVmaD+vfEEJJNc4jqccJBXFMqF1rXykZkO2v8yImPjCGRVbaqmf4I1grqwlI4+MfgZeeqlb8a1TL2nh98CpBtQFyA5l5AagZjDvQ7wN/9CK1NhaR0xy+PViRCO2q6mRx8g5kFmpMC/PB0QJE4BpdOyRowSQHs2qKgWME/pITDJ8pp7HhOU/pktggOYMyEBhuBkvuuGcTXOM3X3Qs/xOKOlldY0kr2/Vufsxgl1kiUJ2Sv1nxrOh5aIX7Wj9YoMkkY/iRMqrIGCK4bwsFOwV21TA+981MIGl4fhSCt8qtf9G97iq/b7C3d+xKGl65Tuq71JYxq3CsGtBQAqxYl9TtXhagxgCRuaTSOD8yuzWldEFT7sfbI/3/ZEg6dUtckzTTzi8vqDsaJHsOnZDgXdNizBOAfFFi45sFTpD6EdOUvu/S6BqZPx01FYiS/kjtUinR1bnvgPCh8pJtohFr0tyh4Sda88gnPPAEOyBDQsEDo+vhf45NgfEhc9of3ONt0/6hvR4ywGO+wAySxOWO9asSJZHtywkNDJ2SvbkvenWIbrEZx08/LtffrnqQqOY8VUc4HcdK7Q1hf2poP05m1diKAf6gQsKvpgNl8Zze+QhDcpO2tkMWOO99UICcSUZeWBfv+HY4WuRrQtq56cVG1qiREXLvvnIhSJA6Utly7rUTOZ9jqMzYAKMLHpU645MzWVt3iPLn2/4SAMwRRdsTWL6CL4SP0QjDHzRas6zmuD/7d2gzQP1x3Sxqtanh4SG7Vh4WKHm/vc5dj0NyvmAZk4FeLEEBdzXDz8j0S8dVPOzEfNMv3NGMFzs+2tS5Qq3Pr7rikh7YJdodxUV2jr7hc7ZHvfPuQ3xKFIdocfsaK0QX94nZsGs/v0OpeYKK0uQKDxUjZmNIxxBq40ZXSBgKc5ozZ+PAJAvhRZC859HU0zR/YY3VyUiRdBP3/njcPE+z7Bg6/+p0Hq6CTLpZ58XbtLj95bF97hxbpuuBEfHdPQIv9rUpDgXOskkl9i9VPM9adv4oKBUI+uA69KBYqkSXyL5I6llv7TXCK1pJGHeS6K35y7TTTunoDJs15va8qg0bRToPtZFql4BoTlRC6Tov8k4Z06mTTZotAqkmN/uo3/UFg6ta3xxSgTquOSu+DautUrN0Nu8g7RnKJT64YYJoN41VLnGBQFEgmqcbcTjA2O80891O6BWZ49wa0f+qS4agJKnaim5I/mkd9nhKPRApdJVCmHa00ijfTw+8QnVP9n2nkAxTCpbvYWb8P+kB9V4DoSeP8Wdb9jhB4mrJXV1BZ+e3koa2t/G8e6CHc9NNkgUyXngd8sAspkx1MQQrS/lG35xMOt3Drwx7QEt9d51PXPCItZ7ieJ038I0qv+jOJ37kVXY32tmZI0RfD5Y3IwBMuGDMaIQuZSSC5BVQSwf3clJe35JIVlfkHTqLSwgyDX02V9Dst+R9vQjWZ7sHTf0J8Qoht5I/s1kmxDtqsX98/AGRtICZDlLRNOIjj+M/SQp4BIxcgbAL3hcVOFvz3kavTshrIrhMvMTleY6DdJrIIm4DCelwtNLeZBE5NP5OyDKyDVBGxyKFtkIRHF5poYsPCxs7h/MaK6c09CTk+waOXACWpJOK1rCbGBzDCjzAsDRhD2HnHinbgq9zAlQKqBkDaVBmJdhh8dGlFjF0zuq5WTc7j2r9uzFHe/dxxXvTHDDehCSb2uPw50rmEEs/pvu99MEmAMjN3PfTk0v2w8Am1t3j2bwR2++yqdd1CxuBbNhst/J2vC/ycsPb1fEfNxhg5m9qNMjlI5/2sAaagS4Z7Apr94IBZ8+uBs1o+DC7/d7I2jFDPsdDsvA8RBfURkPTc7S2gSNkVEc70pTYrKkxCyrXcaKW3eRHLePpPlqriuGnYYme87X3CvmKSlwuE5NK+oHzqC2/w4+K5RKpTb2K/hliCX1nSCdnxgnlbMe9GnjRIlb0IAa6wblcC5seLuR86h2HtWKwuPAPv8vvzrQgKu0bJ1iJVTSfVxOtKJ/A16052Rao0gTfACYfCnKA0jsBdUEkfhcPybwvPOySzwhvA5vufiKDjrNObfElPrwTt2aisoQjjwkN/kiaA4dXEYIuWXydLVGVdcs17i2MtwGoCBUxc/oELFHVDyvZJNbD/bsMiPz2ERCf+1ZfZoxlEWvJofkh4E390JaiiRHLb2Z2hissOFbM+rcFzXgtW5WLn0Rlr8Kxi6kBJO3+3r2NVBZ7r1lVZwLRhp99J1MNvc0a7VybWjyR2d/KfaeIX9M00Gc25+xnCWmH9hVrZUdKfPP0smZNYQ+37DgcRlexaIw58CeJ2zNRtGHByfPukGi6VZOTDBgTKCLmB6p5umGXWFm4T/ITDbmSiTqQt8H+wvGSCcHasXW4bwd6OWgBjIPEth0RjDjtNEc+FvnoA0+88pJ5biCD0dRXQK/e2PK8LHzsm90diTCz6A8x06oGygZjZKA1psbdnTnRlbHEoWuAeRjVr8nuabr30wj7iwSj0NtAhocfphDrcV1M5O4ljyy83ymtOxKxEwtaqBbz5uUA/cVDIIgcJyxG7brFAONGEL7YofuR101uAWhTG1o92rVbWdRY6K9yzlvnEeEfLeQVQPLbUP3LOnf9Ukc7Z63s9sXMCrQ52hJDvxUXbJFkQSxSZdW80+GlWHEnB0YU82pCCZvEl5h3vEZhWUtVpEr+ENjroX4O27jXc2D1aaCrcoD1NL/E3O4GMs9Fx3cRuaG7JRK8vRZfvjR8VkMf4XALKdGxJDnfi6LFSh9CtVFM2Nu3GBZtfug4XQB7su858vGdPOZkdbUUsaloW2+JpIOMNfSmCUYJPQYUgQi5e++D17RlwlDaIxFTN4qf+RHW4UrQi+n3vVq/c4YQHQY7aTOt9/aMipW0Vt+Cz+g4ZFM9CMAIH/WrLenCCKGZreyPW/XQ7U3kUyDVSDFo9Dg5/Z9OuNpTGEqsdHX6/GWTDicKiZw4g+auOIA0EdQmLW9P1YeYT57FnItqLw9Bgol0QUs/sgBPFoPCJ+tCD3FVtG8XvXae+HBgYZLiMqNI+3UdIWfqBiyBV2HGCKUv7IeCSnOuCNKrdtbSxn8VmSggYfxhP/bZgxsd4EyWpWaep4cqHRIQdDl6GeDXUL+G+hUzxqB9Fkr65TZo6lQ1EqtUXDvsWLWXJU0M126osdKpLoZ7kemqnvvQQUPngJ+pjL/W/eOr0ptqfDS5+R5VP8v3oxr5IFe3L9UD7QWm7GdHr6GDcxN1nnrk6tgNFYDrkis92xMxF2s0VdqPTqBTrikzz2e819UiUK6HWKNzNL53TUX15o5lux28E5GelYmTqyJK1l2fFjRUie7lwyHIlat2towM41mTSLkeek2gXIPsbBp7AmMGI6nr/2dF0yVXGB/AOCzmRWqVys261pCSEpvvxnmoqa0nwCSvNYLpelamDK5+PRhDp8z/piIAUXr9aV2zIkgvAwsaLJGcVKkqgWzfcylErcKD632rY2fOKu6h8W4DZ74pUGx5zMy3OkLmLuLlGqTAr0CGziUkeH4TA8fBZOEeOWWE+zVBYXwyAJ8sbKZCc1caj5IS6xVE4pSoQOQUQSrEA2acurVOKKg1iEolaRzVuddMXbIB7sx1XVRwRjcT2W9UEr0GogayCltIZs/e7ND0FeaEJXfkbyoUuzK3mRPofxU+trQZ9oMcugzbgocYYhlOA1mHFXs3WPXFoOdVgtBvJqHysH4Jb1q62NhwQiIT6/lU9JOFdt59ziPFJEIwzcAU8j6a19qDusO6UHZYJ6Mr0C+JYSldrgGCnuNxNs/SfVRYMDg0prkCcNRORdcZ8mcJe9OAVg92gD5ZkwdjONcNQ49izo46HAK3zVjG0W+cXgR7RjrqbTMLSL54njjZkVkpHX2ZXBQNZuoMDj5ItIetAqWRmZQNTDfdC2EK1EXmVTHoLRu5C6Sn5ZqwyHqmOshF7pT6wCDYfAi7wWQI2yYqMgUTlz1JX8bvBoc2Zlu3M+PRVtVeU6oDA5O38rW8zA1ci8kQBgTFJn7nJ8XMFN2nTafFPKxisS/XT8q+gvfQcA/NRux/qFyifLJcQYcvgHwG+GY2BDL7UBm2QBthRf/3mYIN6wu7vXrmBQ3s9cTAWe4GakXPVY4aEq/aTUaSRIYKf8tpIGWzarb0/QxHS4f+qjV7l9S1ReMYlg/uv7rSoOd6LZaWInFsLyYcf5co9KLD8b2F2xNrOs9uhbXlR/9ylxl46f9upahc9cqRN3qpvRdu78u4Kzjk/IaMe59Nt/IudAsYV/l+703eATrtVf3CLZlR9comh+GPwynnPX/fjbm/vck7uh3/Qty1Y/ytWQT98Mf3vPhLRe4vSz4t+nt3Tmt8i2uz5eM1LmB8QM6q702rclEreKiUxjdsimNjO7HfNPqhH7UpimmRkbR1ha1EXsSTjQrfVK1SuxTHY61x/IsPzJ6e/JcrSQe7lZE2krtW4DjbzE/7L3Q+Tc5fM7t/fqVN7VmMkcj9hbuZ1oOgCLJU7rmJCchdMUWQN8wu+buyOkfw2kWKaWTKXIUakeHz5VCiIRpOlERqoOGgvloQf2dH/ftlVRP+XZv4IDAYuOUSqsUVhxaqveI02f6SLDhqsudXVMGh2hqihRI9HUqh7jSkcdfwAafR7jWKJa+ha75v9Qm2WdWOFrfrWak2ObUfi6TerawxKToQwgHksvwMwnYoPA23Nw1XqtWqVOSFnCA45uhdFdSpqr4fkO900JozgPYHtJKDN1Wi5V6o9cZrpGPZetAWaHU3eS1aaLsIr4iQR9vVHezsrDqD+M+0elwcEbyiYp/lGJHqDqRax/OiYVOqIvKw40nLIfU1iGrk6BFPph9fleaRgx2xFR34cCdQUFJy9KS647G/fD/dMFiSCF1Y/hYA4VTYhKduFAMSpUooEhAAsk+q1E5BxADlJUiEYs8F+vBnu7fS7OmJkwTz3LfRbwgqs6+DJU6JWgl4FcZ1VVX8uc2yyh5egFPYRkIQtQBS7RkshRaFaqYjRzNQHBqkqvk38UtOP924t1/pYLbl4IlJVnimUKOt0YtFQC8NujS8hgrgxNdWnzH4fL09U3OjHR64kX7QrW+4JjmwuG6fEASD2sbNhm5MssIzNX/aGt1ZBHSvh2nwlt0g9fTYyYZ09SkDO2s614cdO9laLq4gzJvoDNrSKRbXbfqLAve8/TqO9LLv5aZ+9c4aU3GKrXkoFO9NoQwAlfcAmUb4TZjEEeXW1jHTSw0RCR1qJbMJNbOiUjhU2JZbokgrswF9lRbqFPisHasTA5S6i7ujqJpjGYjdvq5aPoHB7qNOWsmfwUB3dgx+3vWmupA6vPKg7GxVGOfgKbtRUwWVYZ8TVrTzr7O26UksbOd/6tIWC91LQxyL/BcPRKsuspO7L3Q+/7rm5sLSRWe36O0LoVghTXzQ11T2COASapJWEo6ayTC98Xdv6M2LmOyJgABEEbbu3eC6xygWG62t9isHMUVXpGoLfeOnnLPr/f/N94IaPjZpT6laAvZLj+tFFyb0kIyyOQ8zsTuN082ZccV/DuOdBucvkAVn3dUpFlm8cKIg3J2zVu1wlX3vOwk3eoHAI7rIkKVJ7KcEqQvns98DDQ4GXrv6iGAZ9MlyxEsJWesDeNW+zKrf18lFNwk3vtasGQ6hMSW1h4YPOFigdXP4lhlpNPNoiYA0HpwIbnSRbPE+c+WPSwilAILj/VrbwQBY4hxoJgvrPJVnnKtJBa/ffxVyK/iQkUBXP//wk0gFqzJEHLQYjltgFTwXZEbeePY/69U5l3R6+2c7z2WO+URWcWuIoDMlOhX02s8WBQgL+paaW0njja29QM6o9dbHGa8MQULjLOMhvQw0tF1vygXxrTbhAmmu0w1yDsH45yFBHjvdjs+y91F9ENdLbrgJHa3fIN8qw5rr9Ey4F58rOOETyBUwU0eCXYInXBXKiFIG+DBPRvsSzNMKXeRKTi+6PB8evf/+N/2GXOfo65ZRTT90RX2Y9QhKR8E6Jelzp2VPgMwXyOZhins1oXLEdwWfdjcPoMbiUCe1msVi3XZumN9rKd+EPF5V4GJatzrpD7ZqKk3SFZ1XsYiKUjFxfTSfyh8g2ymtWwCWfZ0rcIbDojJk6syW6aVqRSbhAvhgaaTgXIQCUGUDyWabc24At1wvW9iLGQHWridKw5byuGeRFujSvJJqIwQDzsZmgXgCF9yE/J1HYwIY7xwuR+TL8wGaGBFfbMVGNlmGF2Qia/oNcsBIFmUICBp/8XgZYwFctdEYrNQghleL6/09cwO5omvS2oWSpTv7C0jWl2diBg6ZUUnKIVql0jUV2BdLNAR/xQqrtaVaSB73/DX0mZEK9cskn9H2VD0z9EN4YYeLNsdNV8z9aRg7dJBVE22SEAlbpf2MFtZ5foWs54QRcmkDdOI5tsQDl0aLRAVNIoP501ztwO6FmRHF1jzG84ztj/PaEFqQcJG449TjBnozIeuNeiGuc2zpBvI7TOIYWxjqmiduWbRHtSqfWd2RuXHvQLs/cmLI+gK75wB9tOkRJ/TMjCnzj/fQLMHHe5rWhibBugSrFn0SCychu2FiJ9rCRhsL6qHjl5EP26ZNAvnQ49rVfxDFKD5sHAB/BqH6Gpd69rUJHI+db5/+m6PXL5fCUYffRVom5P8HwT4Sw0hjMHcd9YF4tirHf+/e6K6LxqSLY8x/n8PBW+zN23XZeKneJoNxYlDLhKC/z3m32b4t1mpa/U68Ate/BjEH4xotDhrxca+gOQMgCDna0QXKQsM7U6qNUDGX6RstRkGiUOS5H79YhybQaT3/YtJ5FzIiTXmkXmKyQPaRlrN/J/klJaa1kyyOJQxeqhRYlbv6DI6UyJGoZD9iMRJXjoqyEgdGOPFgJ9dJKI4nlK85hfIO/+ZA0hvtrSfCsIEEfqnt273D7BZZ5wxejoWd5T00Wm34CLUT3k3dmOCV5Ot8MYlFdO7ubCc6j+sE1TlvZDOu5yuaG2CX46Wn4G9az7/EIc/O5u7rjV/X3JYe5LomANs/8bNjchn88u1kGzF7yy/b+HOG9pwCpsxbvh+KNr3wCFi+Bme7iv+E1TyM0flzyzj/6IFp7fKBQnacr4sz+37VEfZ1dGmz4Z3ivtqbzo2RD0U7syLFdHHUfeBzo8eXuT3Um795sN7sE+wxUVD6tw1NJjLouYT9rd3tutmPW8d6C1E+cB+3rscJyGUDTjZ+d2rG9Rba/CbKvXV2YRz1S+K4j4LI9YUjmdRe7G5JPLK3/NGnZWxFRZpTI+FeD+fhhwFDrTjLW2zFSLdIHrURg7zk6kviH39AzMG4X2dFEQd9eKG6YaFR3EZLvI5WUBPqB8G7fROwGLujYvXd75P4LXE8cb+3paieRH+1x4wYvbhwVA+qV0mqzukW7wOhUoz8OVcxnHLle1t8g2HVRHYbPkOCLBETr3nqaWf+xwXVw77jNisfreZMONYkee+8CKGycz7415YT1wIIMEV89RDMHLHjlcxyAx8UV4HknLTtR62DR/jwcDb36V5dA9xVIN59jTCohsJoMnpgxVDbHDt3uGP1K36T7m6m5YnpKW7yvQfrTalvu5Ei9VDo/H3HFNBP6fXo781s8lc8uvC+K0qfxB+Wjyw8/8YHi3zYZVVUOohBC31esrOTXJR8+TuLY6OsvdVctMUnsFYvdp7ycy4NVCKrCKDLzJ00CDsKS0I5ocmhg7INpFZeO9BQNC251Ea8x6XyLpjax1SC2aPFQqZY1YIeoD+s+hbE6V3cQWMhB6GzK/y1FshJVwUPuSTjYCOTtb5aSkgaHY25tkOs9HOlsMOrqKijSnoW0L8092QyLTFK6rVmaohBNtQKkuU86bHIqMZSTKsFTmDdl4qZKOVuRrmlcV70QnbgJ0Il+7ZLiacJJJyGcT1N44bsaHhDRcimsxK+1FXg6cjSc6Uy4Mj8hjk7tI5LWYHK8XFiSBEvmid4PjeNASuXoOasEO0bznZWHfsSmCsWqrhMFAxnmeq2wfZl27AUZ6dxDc5ssQeoxZdh6wNROFZ2qf+n0vbWg5rUvBkmiPLszzwuPVuY2MPDSdfk11awbOlknb/40oOv20mXEfbvoxyD+xyweazMMlDYlYorSQEUF7LUpxPOnfz1CzlqFiYT8HYrD6rN+HOHFCOUnhqTyEb7G1yPGy1kEjK1nw59DRcH2Ot4ilH7werJdBVkFjQy7/rhyZRV7Su5UAlYo8fkdJfef9arFnf9EQwhv50gYmngsfmCUKgWsZ04MQPPfXSUM4PsImh47CyXTRe8v6lu2rQ9iQb3XuueVSGqnLGmjonKw+wMG/pFXz50HWTgor8t9Osw4vKP9CXNg/ffoEY2laz4UqAjVgmedKd2icjSE50w5uVR+iLAvb785xBff8qfhM/DhjrfMeEbcea7qdvveC0RH/5cp58EAwqKlne/DkjQQqj1wgTtOPh7lQquKSOEdwGXAQxTupm45MgMjDphNZdpxbiJdN8Vp0cuwKCIr7l+cfnxQQgHzlzbQVxKyFYGnRF9ypH7p0gmcuLu+PUwj9zCr/3S3FtidgUfyc2daTXkJgJS4pvd+k3RDeLfruYYwKCH/f4l4j2c7ITzKKMWfJ9LvyLgITD9T3j4tAPi0RaLogGOsA3DxCOc7HREGbUcfUxsBDzEH38F3Tyq4sJfeMOOKaAz29hjWBUZuLpjwrgpiFzULb99yMSXP9fpFeTa8OryK0Ad4sUX+JaDBiu6glEzswaWchYm9viZwSMZWUY7OR4FdhoZN9xcTodZFFz4NpEcml9UNUXIrYnfAGz/CeJnLFJmvOjvoTuNXMH2yQPRJ8rLC+XG0qhRafPJYADFahZCQa9cRjP+fNMcjAuazaMdtMyjXmHXLAuso8H8K25nsOGnMerkGhnY3QEOexlNPMLJTkeUUctxUx4BD4rHqQINNYw+xEnYIIFVw/H5T0ILBcluC+sPexJ+cOhvdGGTfbSmt1MrSsbffO5DcMDX1m3zd/5FQJ+zaYVAKvjaYdOoUc8bOk3EjBXzgAg9chx+Ihm3+pwW1HallvjkeI97UA2IPISzvb5VQaXYP1BVt4iDQh9MDxpcAbBWIw0R/YU7eE2C2QFsfiLBbcR6MfQUzd42XFRE1CUfB7hem9KqI/8kZhF18on77SzErQAHpEyFs62IZE12PGlL1EkTaLweV7jvxu2c/8hOxn3ZHn+x4PZrjcWW2oLPv2zx7SsqChB11zQnSpMNCFL0+4upuayIR6aONBFV2ZJDOafI2SypYQhL/N9elyq+KEc61I3C/1a5sI/g8OPh9u3G+I45AbwvxaKAQcNeXrX6BXFXOKOYc5AlmQ9I8tWKml5UccLGoCynEyE5gkuj1gF1MDRVyQ7xQ/MzkeLfhf7ej2kNJRrAzQ9QisEWKNYmAmwQ9iCoKlspW9aJhkBS1Uq4VHInyiaSvyLgIY7ZS8Lt6YyGVuh/ou/x3qia3b1AqjynjrpacmwhvDi/3TR1Orno0ja66Nl3xOiTm/Rz7la1/tLYgrLRpsDjSDdpFlVTVH6oAWSIj/+W3+OEsnY1NYJfgUbLY+TtRIaWUFPHkPMJDN3nDpFeFgLDdpmvZ+vVmhWsBSE+NUGYXgL6m0oxIvoXYDJ+VtabqevXiAQejd2hZy7PlrNRxrgrBeT5oiO85xRk9qVfFBkuHS0Cg7r4J2GmknFDL82tK71clqLn8dHp42k6Oa+mjDjP1ZnU2ochImajYD4ymTXvxCWKtqV0cSWAixbx1HPmqABm6rICPndYujg+JK5vMx060UtL1Rpj8+mLTKeSY5JCUeXt5IPSFzA/S6MfYVp7hqy7tM71a6v3MoN2C2DFcfP8GygLRMEwLx9bogk3reFpzNiHkWkwqV2Aqsz8tkqGCm3TPWVeLsQA+Fq4LYEg4mTGNYbSdKY7S6gm4RBEX6eE/CrzS+TUr4nm93O66fuU+ZuNw5vID+RJTnwylAMVb0DHV9FWu6Wc+RsFu/+lq8lS/rK5Hf1tW830f6n44gXJF3szmMuv8k2R1DAwRmY/zYmrY+ic36p+EuE1tEOyfEzTCnnLJEQv2HEoqFSYST7FBGbHqBauMk+0ofLMctunD7+8ofwChNDfZa+OOQzuOI/t3QKuBVQWgCqpUKqsvimWMnkuucYExXsL9hD+SmoPFRqw9zo0Azurl0oF06rABsGElHzohgTzmremueeyWzCcqocLR/RTmvorQ/6poOgH5+LGMw9vpfvn8d8aXVXFlsv27vaS8NswWBIYhA4tvTZYQ8j393IaCBdWYK+fVZAg3hCvqu4edtAHOiyQPICy8DVifTUaB5pJy0MBvYcW6fvsToih8RWK3vXH3ICOR4wyYIz9AbbmWJmqQX0MWGCuP6r2Ifo17A/hZnCTpBHCwbHjyq92+wkxwK15rZOtEAMpNMZbO6pRj2E03qCQ9Yhl+B5aYQ76rPtWZdy4XMtu3xZLH+YwKHTEQnkPLT7OZbW+ZFUPXy6x2yG1J/X4ViJJFKKPPMqyB4QUX3DrE9Xeliui01ZaaFytBwDUzYmBXFqpyG0gtGkhjG7frtMYw0RHFAohJxYu+gZZZY7ejdu8qGdVD1+8agC8DOz2JClaAFWxtMBwPtL8LdjBsfcw0oFyeBzQVibFhrZi61DXQIbHWXeLGHr5EBC1C9t/0vC+eKkjD918Kkkf7RnjP+LsRBgm0EZrzRYFa614EXprwPjwybvFaKuebECFvzrjZmA3BiQvO8xEt2XA+FK/5W2rCVODuCkuZXygSPPLeXM+k9U3q6rCUOSpm+J8CvvWQu/ntFjAt6rqC3kPw3z8Ln4iKbJ1Jd/q8EOZJsOo1TIuNqzic9kvxVU91O0LiXRTfcjQGcYhC4DoExW7GTBwdF77ldiqijBQ2KgfntpNRf1bofC3L3MzYAx2cIM0hOHZopMwoDvQ1e77nEM3Myg3prJznqxOV2XfKs0/HUbpjcG7c/dg2XlOlqmrHmrunvhItckhRe7qPXSDKfd6aE2mzTSqfWBEikr1mdT91LirKeLZP43ErarzPGaLx1bTj8tn76fczx1X4nUOn9eWraaXlRLo9yLF8KBYI9VuRpntq81W06dMuuO+hlYeCN5WlTialdEb3MRqwcBuquUJEZ+aWjr70Fm51P7WIY2rL1DSUTetXMnWPM7zmS0FWlXV+XIwPW3h7aZ91pqDBPtm7wrzUvBPzwyZqbdfghrOdFsR537BBzxX2QZDFBX/va+sWnET9sV7Hmsrmsv1Q5eg10OjkMfDoUQOuf/nVtM6Kv1Ick2NBIP6vYlefYFRE8yP0J2vUeoeTtyoevzlYbehoOyND048s7G0T3/BVQaCJrtq/RANPdfsiLuhZPAD8MFrGroNhs/3HX4QXobDSmv3fIHm4HfAEkfoww1djRMYGojeY+FCFZQtrOBrjfN+TAFhT5GtxRSZ9OB9tolkpY53rSM7uuYRC+k7cK2ncj8NpXAefTqi0hBA2v3HOgycvxNyEJKpO+3TzH1HqD7GoaGmQaJd0IX9+qdG5xC/RrLtdORkSk+P6YmDIa99E35ZEmfnGqaAVLLRTuojgDUuGZVpnIiuwPBQLg5juihUhSsjzjONZF240qMk1q/y0gXk17qfZ3o4Oc2iycJl1e8bUHlR8+uPzmm6KSQKe1E8aS+B2/BZ/uqNVTkakFv+RiiONhoN8pETIZrtfBCQIAWA9gZD/4JthFvI+yZrLAz3PIRm5PMBVoymAxCGSTwPTM1rAGnrYuleMKjXxmF5JXLUI2N/mEvD+QK7o/0Ry69a0MHx5Jax3DGmVSzTuMDkEYhqXdRkfBaAdMH6kb5orATf5Aa8YZY8MRjBss4GbgNFZH2rIBJmFTKv8x6KqIhu5sPr9taYl1lU2QmX6Il1umFR+ZrhuXD7svNknQ3qLeeAh8OhQ5Zm2fOjPRCM2wrTF2948CvJWRV0pJVQ3hgbvdgywz3q373wfARBlGKonG3HOzOyCkw0eFu6Nv3olWJ21y6vrfPCdwqaN4SWGa2o8oOPCmFP240pLCz97Kh56uR0/VLHa+gvvwU6PePpid9wFDp9k9cw9Df7LGVXD7cl8plMr8ZT372rlsWgwka0Djp0z5L6NYhrh0JrG9GzZ0e3bzQN1A6hSQ0Oob+1EZOS4hx3SOcC+IcoOObhzzn7ITl6/7i5WF1oySTjBxN4jx2B1R7TokfGYoxo8fB2yw8wQSsJtv+LiydeGdGL0gy98o0bFIjh6QsGivqt31Kw3vkhAf68TR5+W+Wr+UCEYd/YA6JEaxWOe1sMNoL/Q+N6Q4Tl1dAjogYuFkX/kTJqUmjJYDX1FaisQbk2jQZbUISqIjEN4MYSCWJLz3BbK4xJGg6+3IBKrMGK2HbrLlPMjl0ENkG6wMfaXSygFGYUW5CeiODEVCGRL2A2yOoDla5TSycwoca62+DH5VIpqLE2ABoCxnD9a6YqXvesPU+FI1k9ve7J26vL/w2V2PyYe9of2X5KeQSs5Toj5JKJGGkzp5IZhRu1/XAQq5f3m7Ict84H3pypVaUg9QopJd+ohTSySJQApqtAQ7MKkdBTcz9FH6oErCHJw+vRlSdgogjNNVSmdZd1q+0oQVJPimF0Rj6a0vPL9/lU1xz53zgrL7/d57Be+oZ+xXBvxYMDoWCRmbDJc9jl9p8l//IcxNTLDu2nXkiD/iRWSYnxn1ONPZEw8rwOIHAImUSmEfSg5txUCn1z7eP/01CSD0mWDkHtmcMkDXUTQUt3gIhRPX2Nx7yzAhAKJE7j6OZcz5wFSdKNtB6TAmEZky5Kl3KN/itZLSMedAaSx52YpAlND6kKUHaSkhxWIKKxeqDdmsyNVMHTS/52bVcvbT+oKXu2ap7MIJuALIyo+taiLhaBqKf480wArwZBXZEsXsEybngJnHsjimHA9Pl3CjM9d0fNj+wKfL708tmVzhKJXziqz42Ym4eHx6dMNDUgTgUCTugKLbI4E0Aw177zSGDkFJjoaFRX6IAtaJXnUYicVZ5jH3YAB/zAIHs9054uIfIojuFs/3KqFAiYe3kP9Lyz/OD7dEmy361J+bkbzEwB9Lg6BVhGBm5Jf6Djk5YKc0sgf1KdgTwMFmSIP7krnReC2kGp71DkqxjWD4PjySIdi9qhscgO8lwZOtSQXUHb3WIMaaxqQUAcQA9oYkCgF1mVqioDyvWW6xQc3Yx0S+jM1oJlARxnckIPVSZhTC9cpi+GE9CNbGekgrDXXKJB5ZnxZfI2S8mmt6FBSTPKanRr5gD8qES0oQT1rur+Ps+oU5rtN8OOjwpwr3d4kFSuIiYX5dhN3hipDSIklyrer6v+Px0zUPInqifKwG2egC8Tmg2WlS9n+EsSfPaUnBLVjN8OO0rHULKnKHDH6T3gHgI+G1RMMFRQiqXHY4Tcop4meCRir4WOg1XVgGZBVxepg6m21UfKKmL1c966FtfObCUo+jshvaSLB1RIHHBTD3dXNpe8LwAnSXH7tNEWSfN9wrpOKFoMBQkbccMdTkt4Fgj+jZYb6IhxvcduQn1UwglVP7ULAhyej28PAanlQFqzJod423ti5AOiz1CTrsDtVwI/6JlYBYM+hFXNy4yWRpdy5hl/YuMmdeAHteo0jxyaTIHORXgSBAouhdV08ZIWrNjPZU7JkasaTx7gmbOVUhyVGrgsnKR3VS/mRrZ37Q4kXrI9SXMeMaIS+pWJPTvxQbh60A2jLu/aPf9+RejGW5aA3GUtf3qinhmCGhZ8aANyTthJSOdwjn5BYmeRPtS6Z24kyaBJD1HLUAoa45qy7V4Jx8FfHKEp/Vjcp+/HeDdROlAlvwwPl3aPR0M+RawMqcy7yU+LxJLOeUTAGB5tVCi9bGU5ttfB8gnfbLJeDqfRBLlo/SwCuQmttu2T1rlTrLZNaa8c1U9RPAfxm5wRF/we1dsH0zslTqpV1RynYBZvKCvfmnn+yA3RuCf7xxzBUJfHQMGpQ+vCliFGWuz1W2ApijL4SZBy8S0CGcn5LAAtZnZayNLjCEGwNhAZmqLYArukjT27fWfd3ZJBfSRKfOH1Mz7mW4kIks1V0CZkta+VbuChZLZCewqj1cGFVcpZ4iDfvUfpCF84AbCuhCZREnEsMFJFSSJRztwa7zVznLP+kUWDbqFxHKqLe5pqaau3mKRgMuI3ZKpQeKLSk5ie277EQAvN1HaM5SbKTzmdahyQ637l566DsbRuRnyK5vUxDVBeVave0Yqbp9tu1xmEG1lC5l7EhypQ91sPG36lvQC0LCkCTtQ2VJshS+Vy+hb/2V52csOvoYlMf32VfZwWKPLBy7QoEXyoyrjH1yNxKSWGWn3MyiyQUCYA88oqyUPTCkU4UpVoPV86wpmqk6PVw8togJFcmgEHhWTsCNqPTYm2rh4MM0c6zv6iSTR5u4V+JgT3D7QOATCl4tQR0827MNT0QxokTyeDMCNRMoJxw8XUOo9GlKUtVaxFi7U5ZglLO+lZUMEczL2Chtc17kc4mermOakTopafXc2leYxIIubKUho15uxbsMhia466I3swwFGJtdu1OEaL2gqKROxZ4NuaPTE9t0EKdgLQwbvDCpJNQYumqgFg4Z7MeVDpalFea8VhELzokQgzDdY+ckMPi39Oo1MLSxxuC1B7voTpE/VllzI+Qaz1H16adHmG1+7bffTqE28okyBYgDlwZMAir4kGicq88ekMHJQrAIQQYglkjcqm1grSXOkUOU1WBB+T5C0jzEv1JXxGSszL2+cVsBljO0SzWAbJQWYpw2M2PQiOyL40MQBGFSt0eraUhTJ27vTIqFelOBGqwrNe4fwCp5ua39TlC/It7SSTX6YmtV403Rd4aTsFyMCBM/lXCfsUHB4QAHJd1CR3HxCGpfp0RtBoDDM4lpwQlYy26X3cPSv2wbuJXR3uvRpLup7RYkOJp1AE8i4+shJu/QB6y7IjzAulXVNq4dgTxS15Or7qr6rnM5xvmflcrRqFfTQiyxMQipkRiIRe2oKM94QNkqwUJgLOJGvRRHg16+uMUykimwqQeXZwdx6bM5mtBVhmeTkfttWzL/ypltpl3RYxc2Nv25xzHq/UDvsazWImWrZeP7pOaKvlkM4b4I4anNqPTqhCKlT3KxIII7gqCyriI4MF5/oQiuBcQX0EeSoBmuqvU9wpbxxVJPohOtPC0NyRGbeAsdB7U9ktI8Uco+c+u+FI0faJ9w3kqpgbCF2wdHBpLquxR0YPSQnOExJoyrFFT7xg0bygLmwoC2wEtP9FKuG9BKHtU62PCFoSqDV1dwzOQphqXEU+d/tNhfUNJQMURA+71Q0x7VYbT0+oFU77mgPs4oqS1yOLVhMMmRqifGkzDXsWg/EuomH4dVEMeja48QkTX70geKrQ9AujFAaJQiRs7nYiDC2VaFWzAl9idmDCHzJjMZSfCm2xJYbpppyF0xg+Gz7bYk1p0R1XyimUK4uJHoxGVV0Sf4Eg8lJNQYQYj0FWyRQnsZeZLOYS5fYCX5AyKURaMn08pYxf+BiOyG/jMZvUYHBjHlzsAcndGZSZbNmExyUD/xTE7d/VbfSOLuDpyeyzjNV14UpYRkyrdC7mj+1dIJgmjOM0UyGrNiMjs6cmEYtxZQvw4Uejv1p8Lh/Cs9p/oxMuYGASjZhrFQUzDhs4SSKSskBKSCWlzOcBfpTBimzavZv0LvSbmpPakk6oZC6Fl9qelk9hWvlkQZpfgPUcwG7DYPJM/u8Sw0HLbwIv3o8VeeLrcxs3QCDTOoE2fTtnM3gaA4+xIo30EVXFaExNCiTg6BGIXblirBwtmCk5sB75kSx2OwMQOcqzYFE1p6cS6Tkxx/5YDO/UF8Cxp8DuvkiniVdh7SH0Um9MvSvF5VbJoUJtn5yI88XFkBCt1dB/ilvfouksGtiHH7mWI9kh+Q/QfPo9jwkJb6Javi4t6/6khKVmlMObDLOb6MKarMGHICNvNJjb4ed6UbvLGz7lw4BHNRnsSwZgXNCZiKAyEQwiFBhR90/CO4R73O/vjdil0yNx3MUsjzc1Odhtec4KtDLJh3y/4HTg1tPB0o85YmOY7YXTES3rtMOiD6eKlPBj3oe3ORg55ZQUCYI0Jwq5CF3MpPUPrkjBmGtXFBe4nU534zo349xovk1n5lIWCNzRDzdpzdm0m4ii7jrMiASCLKKnSvdssfsuIiSqoVsa+VcSIEz978gSC+j4x5+5TO3cIaFFSXE0thUeJZu6wcZzSSv9EkK6JNAWUhbUXyskLwCW6dgyzu+TMUG61SRXcXBvJXsZyDxZggIMRmwZ0ZaaJTcbCL3BP6j13p9Tm8aGKAZVKNvg/A4U3AuD9YNTlPfvWChZI1sTvCRlOQEDQPB4Hl3mjDbxgP6+lup2q/3CKdGB/pFVmZJkBPSWKP2CFV5IOFTtjSq4LdlPFLIRxP3x54wSvfgNJpUkgA1ec+6pt7pgkQRisIfFP3oZi5z7sc2td7UAG5DIBQi7LUt+eKTpoWWtalKUxYCUFmqC9eN7WJQNlxb7kesxwh9RK6Mh8Fum4llm6i0svRc+62aHpvCCL0RQMjpnge+8ljY2OXptTOEpLuofCmgDXwhi09eLNu2mxfHoaGoSpYKvb8Ys73zSJa8AuVIdXjcaE6Be358cH6c8O36DDAFThM8ZuLB9o8hnthKFwRu7JPqI3YLMNghdnBQB3sjscxOcKPLlSP5ZNFX2nJApL9QALf5rR8sKtKJmfRyseuK84vTeItSvQczRtMJwBePnML1yVM3Sh5kyti2W/CwER6OlHwkpPkaJDaBZqgsk51LIWAgNvKp9ngBUxMDbVA3Arl6BKDH8IOwDek+HCEq+1QuT5md5rEozvHY5x49b1CQAbMmnA8YlZOPolGUpKxM7aecW+Ek7wr5BIgC8aulns88fAMMgMBLNUTwdkoMAvzRu4OGz9YmhrCSM4Y2GRRGdM22Ri3EVyLUIjOju8TROo0ijlUsh4Qt85klnDKV3SphaxLVTk4d0s2J3S2awK4SKaRwA1HOgh7H/ABMapr1jWxx+7W8NrMffzeikwyMa4tDkqEhGyJmhjibqpIu8QQtT2JmhhiY79wJ8UezHY9gnUENoUtib1O2WSC/04jeN6CNw7xJw4kShc68xOPGFduh/CXaCidWz6u7srtsaoyRBnqsqybz7TQnHbICkhoI+f3qSj+TBt79HynjQCmZhHZFhQkNmkdwIC/A+pbkiRGt3iefMuHgEAsbZWveEL+68zu6W6I8dmPwYIBxAdkTTcRCQjLzr66bmXCuR1/trVqWuWrjro33QKDe/0wptf411v3XpNAeuGMzrstMX8FcLno2/Qrh43099cfpgLWkoMR069gi1PS1udjZl5x6kUNDYI3IfI34WEFcjVPMEFTAHAVWGv3ugZXUnlMtCN0jaak/A+ZU+vqS80qeX1Fd673rTVsmwelt2mgfOeYz4A841chVV/34SGUp5lYpC5uxQTQFPe19EUz9J0Yc5vkK58dMr1Bvfv+QeQmISn8ZGy/Gk1HZz/U9LLh37Dmlvua8dt09krLUcgF6r4oGjzsGoSvkDqLvx49nO+fyyd+k0BxuYhtwYt+ygSVffwsmZvyubGLmqe8tCPfgFBRu6uwzwgP88Gzu/0lycP9mBFYzt3JE8TTXhX+j04y91rmpwR9nBMVypy+aZvVhe8vHF+pJPL95n7l3DKNeqmyjsxG31djfxSSxc7fjmva+f24exPRgxrI0f6vaEgLpe0+slFK2WLGK/w1z9gAO3g384N/1sU/ABXv/gLzzN/Aq/ry8ekiR9eqBKu5Jwj51VX5HTUK5yrA5qIJfZgxrEdYZgBg3cutosgFauFddX4MCv16o5xP8KROgMImYWVYmcYY0NilUS8Q7i1KhyseDVGzAeyIfBjPHchgs3lJ8jwYFKio0XVQqPBoIqcBKYigCRA1m6xiMAR59srZZzebjLFaPgtkfnVq/xwPh70Vy4vIP2QA9voEg4cgTiE+uk8kFxI3ghk6ebwZpbXDcwhnGAyY9LUjAfmY/f+rRJcCEYlgGVHdBSRGt+7D6OvJBjy5j6ODZSskLc818wi0JblgarmT9us4QriFWqOjKHSEIaZ7VrcOYx5SuxyjVJtVnnE7h6guIlMWzaKICoBH/c99KYlkyuzpfDdPWjXHUB8gg3allWAWD9C/gpUxxiCjtP636wH2UKz99rUPLBDNAQVcgfyGsFTch2DEaZdcsBBI+1BEdNfAGcd5wgJL2weBmc2knmIiaeCXIdxRoVy8ydodNFG5hb6nZ4v4s5QW9LfB+c3Am9KsopUfFiQeKEO3KpjacJC2X4EI79TVNBhHVihqTNNlBAM8gUFM50/jWpIOUkiMKSeWe5hBcdS2AXAwazyTEeKA8IOd6gbOPmA2SmsLZcFShdFEJw7hBQUD05dVr7UuKYb0Rxlcy9KNHzguyQajnNamQu4qIhqYOVE7224K1tS+PT7CIfQ54B56x/ZhH6O4ikWEpsV8eiikbEq1nIgodAVQainJjyXOh6PkPIIE+MfH2urKmLl64KBf2DmLoNP0ZTrE9U9XgFMX4QThH25zUNrlxAZN5+LSy6o5NdOthwlV3wsVunhZu/HM+Pr4PWIsw1ek6c1bvWrkWUAVeA4agQBuuhjP1+n8XykcFGdZl49nEboDYPF8TbjNpi+HAr+uPg+6/PJDajEhxQbYki9guwdo1uNgRcNepeC46m8fguIdduqSdpKLcUur2ygVbZXBYHCq0SkCGCqR6H2wir0igC/QAxkArVMrTaSHe8V5sWm3NMGoKxvx2VQrOuylKvc+uq7hELWc3sxW1UwwJ1RXR2Gclr6iumtS7MpoD7dLBxB8nEQKAKLz5Wi9llVwP4Yp+t2xghblMyJ0vUO3D3BAXjauU3KuPKRMKTm9oKqfPwdHaS+b9wISD9YXCyMZH3tk1CgYosbSdSpJLHRBP8DxxWb+3wvzItZmSx+VWYu3KtHzvtrkD96/awJ4wxQqjcj089koNGGd935IKEElcYGf8kQkkK+80iLlp3/e8xDbPS/cfAQxP88WvplWyoA67vRFp83UIonsXHwu46Ghj0Lix+BXu4+QBXXXovYP/X5A5Wu/2sN4nLSQa+KS9xj1N0wYfmBBssbWJtU29YdXWOYk5XVUq8im6VMCsrtcGDoSeDqhRX8TP26Ytkt50c7DMTXuF+7htFLP20NaBh7RJTejf6x6dSbcLV+7TZdhH6HLs3i1UYne4RS1Is1jU9rpAOB739+QOPgjEB4vppORLCY/+YrSggWj3HyCPDDQUSKp2eLsYxyy1wTzHHrdx+K+PHvymhxHW2k5ThJsU+ZVVrqSUbllbOvJLtz+6gVd//X5buahTp63cb3d0x+ubdRvd0jL41Jx9KWqDUMa11eCzL5egFYcVquxMZoyrTX7ur/UD3MezEuORb7tf7g8x2ia9DAEwTHl0fN4Ukj+BdNbZUjXkY5Ktle598VIQEekzAFfq50Op5WsL6c2EBL/p0XmPgQzxC5O7YGxy8QFvo8iOIN5kU9FR/vXHX+N3hcyj2Vm73jADWhAgC0nUJM5YxYBRriJeCBF1U/mQgBcMKGu7MoMRycWMI2gATWzqmsNBu6gxG9ovNeaCaOjx7w7Mq3Ofk7LVfCJfjbWYM1gNkRVcB9a/LRtc9B8qbDkmCk8g9Qw/d5Ne5mX8oRT6m6MvNWFh1jJ2s/6BHGehfPX5hlAOwyjrVBplHLI6dNEduHe7AZw5tHHEX5L1/E7lcFOASIC0qmNka1hzwEu688IupEXBV6ierJLc5j9iWCojVoTfkxAD089X5SCV3qBCjECOQ4ZcztpvnrT6AhyevPsEFEG2+6tfvv9k6MrLRMe2AAu80zQSzO19ZXTERqbVocDAHcisLziQJcQy7cLgtGqq6zRNy/f48A/L5936MPfFdACTKYe+qhQM30lUPJ9hblDzlGRVuIsOWOFunOQmCqSjWLDSerXmetrhf2VlWJ4s7G40VNOwDdKE5H04Idezt5AxTEnXutUhS2dhT+V9PGRqXoB3vAwWQvXsjVYj88v5X+AWU5H3Opl9OecwRbhaGQZFPp6zOBvUwG3vXszTjNj469fOVuRI/QeQD5pWMStI5SOxO8AWyEjdghkFtvqNKdP72m/qnM94KnE+1GrAgXZ1a/P6T8VX/pJmLn3/ecng1i2YyFBdJMtBvjcNKgm4zOoxrZLRKuLpUE0aEEKnmf2AIaOChNYjm+gNLsaykqRLLzul661dd+KKVsgt/jUeDkHTw/MJRrL4BKL1L+pjwbMFGyXlqUAn6278xk3yL72Ba68iikFSjTi+Sted1BzDZhGdQzyecOVhz6FL5a4SCbwavAUOj06f3cVjdbDnQ76f3dsee7A0jQOweMbcPdWH1SxwpOxzVfCToUZC0neDq0kF2hCV0WfXFr1jVtC63fRUYIBC7O/BcSJcJ4eLmYbPuz6VVSN6e3VZ+ZefGCKfUqD0tAwWUZ3NsLLQM1umNN7PerlvQQy5MjFi6sDAo0JABdq3HScnD/5+u17t9sLHAkosagmfqRvjfufkephGUL67uiTkxyBu8zwFk6B91+17uO1d3tFUrekqTE1T6HpkXWRZP7vSdieTNY31ZLx8CbQJ9dURkGvy/JYcvWf8B7va0VGIWYVdOFIA/gjt8vZ62LOofcesFOPips9S+HGikyt4Bhl+Hb3Mf+1OdT3YKGuwqo7cKSrawwxSo2se9m5GzT5AuPx6WqqP0KetcHN6OgU41+cnR/KHOxngxuby2tAAsTRvsxOsbANtCSQDy26uC9zewD2G4ehLZS9JY2bvOVwE+CPAkOeL32Oev/G+nTA4dWHw+CZC2LqWWDp9m5AgZ8QElsqEF+9zOuXfIzb4B5cOLxkaQ8zfHAQsdR5pw5Z1njflmRPKIUsULCVGNfEGKSWUd3ViGBXIjSthB3UBKZqNWjGH5BnttkXMjOrrjHKkq+UHCBJbnl+6lGPJYOhQWzHb0t5bNIu/1AHHmD6iHTNJ0FumCY628hDbDw8Kf6Fcf7t6rJEDMFENT6qOOA1PQ2sakEFEBlmE2fuwtFg8hk71l7m3rm6AXvsmDS76Z79lu02jkum363KiROJ7dLYPNCSejPhJ0uCXitWd1KOXKGXVN4NNmn3679a69peKRFTi1IQHcj6ioWNtZgK4B1cwfsyRg/UBJ73mFagpl3tJUjgfj2sEJ26DjBOi4izlnYKVV3vppe05vLJp7MbsUJnbM+NpMAFhzBAWmzBW3WGmLDpv5UW+lZn9kKMLD35QiVzYpALmd9SI5VtUZvM8LT3wLCKY+vaA2oZdkziKcMgTS+22ykrbRgw85wcrbRa2pqyvlKZSrSWm7mJXmnwjQWbq2etznQ8+Kejd3Yy6Fos33PGJsRqibbk6+EUqFpjIPqJr0IZCRGedCqlIfHuswv62VerBG69x+mS7nDCLDImrtNnhcn5Rbu1iU5Q0C6qemXHIvHBDocB8ON4QlXO9WwAKqhDOQD196pXoAzVJwNoK/YGGmw3RCgatjLQRvI3u74Vx5D+kdwxL2KTtnsFqdYBL83s1ILde4sx7EW5/UEHv6u2118vhW6VqP441lyKe2Kf4NpKNmn6Rl0zb0jDfDTnyR9R/0BNFvd7FO1rz7dxnMcCyeyMx3QmgronYM7Sabh0W4AAHk8m+rw4ttZdvn4eh+WLXXQjzSCmKO6HZR9jjihIwGjTMP4iTp6O3l717mdOMlNgnzYYeLZJBhB4BTMh4TPfn6IGjUi9adi233kDF6CGAnQiOAuto//QYkWcJvmdkxnhCoXhZnNwrWO+zOUrnBMgpzjJGGx48qIRXRuityxkOQBwInlY3SU5KWiVcU8XbyO+UH/DiRtw/3e2BQ6vxSb9rmwdYMR6CCCF10xbZiHw+mk32ofHaxBvZJf/Pe9fFXwEfaHNdR7rqWhKPEAXAtT0oA6PC1xE9TYvn1kjFVW/k1jatJCSHGkLEQaoWYkVkF4KgCZbQRM4vafkdujKj0GuLbEqiY4Y4/pd0E77cI/xhNQXCbZhhmKc9mQcfIqcmH2/p/j7QvO//N11JHFBeAUSklQKpHSWuOYGAHBSv3FST2oE9stRYZqqujlWaYBPJc3SSGRGvZfkUIu0mawNAtZcp2DuCcYfVmMm8WBNECX+IEFOHMh00BT2j+wm39CqS8BNVSLBngmc+A0wW2lkHscigPfqd4aMXh7o1KES0JFq5A3R9LPk7dn9Jv4OosJ13nEgmTTVu9qWsj0Sz7bHePUIqni7FSiUR8/ZiX6vsjbrIxm4rDM0IOXLY7XoPquBghjiTJVOZ8Q7wZcsPYzdBPxqdR5a1LiM1c6u4vvT6j2/GqJF6V5KuSunnpxA/r/iJIHV+vSuZVyX5DEP89Q9sRFAymbeIuEZb00A/7QPmk2jCf/DkNYY48x47I9g070bBO6JnSKzMmLP3Z0lNwbL5d/09lN0IgzY2LLE0dKPD7onmP4VSf6K3xNwmcCt+5PE07+IRJMBYLzNO+Mt+ZSN32gSkJ3dQya91Oekf5zC+bC0prCVLCDRzcLY2b/wHXLRFyzrOq6wfU5zO14s6/6NgTqzp+xvUBM7XL+QE/qX6UB5p5724HGup3gLfJ7lwBEF/jP9pd3J9U9UPyCkP/WrmNdPEWT6b3u3RQMOwmTlcoIaD2oPve5Vc1teAZJwZNXk4GX9pox3bFJXi81YGOb9h+mmmJ9Folz8I+6V7uDo2bHJACgdHyhF2XOHouRXPIv2cuA5UXx4+xSn9LBLwdStuy3O4vmnO5gTcvMp9HKmU9Cf2TgiuPUPuZeRHRidw9tas786FZOHWyncUgnMbvZ62/DYCQL0epr5rozOS+w9fU4uTBYATKYXJQw02oiP8gH9F+Wxg0XDubQzFFaZSXUr6JNNB8ovVfdDi4oEni6ePrMFnDGLBLS65cNMgc2DY8oNCa7bPsEn8ezZT4ZONMU1QLLIILEL10k5Bd3wtupwIhWmI6oeUoe4XPa1pmUqsFpbfHBgolTRfE0rB3fXW/I4mmNARw8WLgehArxp1cRFu4t3WfqB8wd/66TM2FQYubuDP/4YzJMB5iQcI3h+vCizIq3lsifzjU664Vdudn0+gEVBTfFD2L15hfOyp/5ejvJm2/u1PQmCLzW+f6pxbNrk6mYJ/hzggfXlyPw4ROnUCQ+4IMA+e9Fm0SYHxXPsXLPR+/VcDI+vXzV7g4DOAwH96PqMAruOCOOAJ1K0tgqeLbVKVHqXIsNF0b9uYoWp4TzMqGfZvbcFczezi9rmgOEi5dVOxFbB4jsKvfwt6I1pg+mhRJOqu06uuOXujkLG1+aocjFbzxYGwfTquytDz5DNzzMe2cDj3urS/R0HKRdZb2kOw428QogqC0awdedMP8peOL64/iIQd7AmDShl0vJvMWcBHN3aMFMnERZ51MHkPqZOaetEAXOaOx6uKFViRW3ZagsWx2ddUWSUqpN436VVWs1NgOMaBkJsW4pIwIIosXUMrb1jkvuMYfhzBlEGsdWIKDbgL8+i0C9haUMzYsoUd3Ns/rjNs+mVSCezQbZWvuDF7hXLi97QudbvPeHZjSPomttLNdRPX7DWdq6r2kcofWszEbEN7v7Ox6CL1dX1Vo0W4MssFyHpgkqedm35Yw374XLZfrZ6HIjHprhoFyDRfBvZj7vk140ur9wHD1nH24iUqRWX2ttPFrTLnolC9uadK8WxGnCooFjoeyp9EgKMHicSioOyEO348i6LNrTxPl875bExRiHLDUao5TYUvphA5VVd/yA5iITl9Q+fG9XIYqampkErEfu1r+StGL389lkBqYossNBp+yESNIe8NbTTJCxm4rvklqvPD7JsVppCV0f9k0drl8lS/QybKSo3m7O+VNXkyFHl3QnYzEHv78k0v/98PP+4v/QpdPflj59G5drEZY81qJV7Cp7K7XEfQZT2vT78FdS0TND1YG6dfhLxrVkVQ6UbfitzCSMp4a9EEzcBEXKRlhroXYDAHbKrWmKvNjLiqJVkzG9aRtZDLOojCBapUOVjbhjrQhuSNG6b3IN2Q1n6X8AOgUYmwj1xXZlqRSEyY/N5cEoprvReDMCZKWPhkVY50/xD0dv4BRhD+BRfhJDKhENIaeyIYMAxCg4vZUgsT648xYxGrIRIwZxnWo5GKZyb2L73wSJ7VasGnvJb9awUYm0chqYiAHCTu/mZuczSXvgiMt0mps0ssoXVExY/pyy+36SL3YIK5kyKC2Q9I8jO1bZgqD1ARi68FqZx4GD14DiJ0CF16tGvITm/naU5VnlV9M5amqHwav5lFe3jflqOBzbeUm9RhlfrXgASK46QhkdApGe4H4iOZ51VgeaEZo7eI96V/+K4cu3R8GAfb3f2goQTL5Lg808EuowaA6lU4Uv9H6l+Cbm5wJYE7vKSA8Z8fc3DqcNZaYywVpiX8BV/mEMvWHlJZOCB3hBJqAYLMG80gHM+DOzdZMWZaty+8leUEjJ/qkNZGzPXgVPJF0x2yaGbjPrlUHuJQy7tTR2ZMN49faCz5ahVDzWFovhGjKMyEOOTx5GdROxl0RIWlsQ27o3bht6Xy0ah5uvO4jjScy+viEuZhRqD5A7hmYNMLcOTQ5SmTRdFjljXO4JmJ8fSkWT2tUsWaN2VhlqdKko72VSCYLwvNuAULhtfGp6FDeVEdNvjQRzDw/jcLtcbSm+daH3BuRHhkmaG+NNlybOGX9skBJJ6DtqOSUUC765B8HBbgjGfNsgLUqkSRWm3Q/Kmg6W3w31t4L5cYmx2+1/RRgYKoyJJbx8PGzutVtV87ZIQa8MEVf9KIbHRTn+IxUqu5ewLH9LnJRN1qpWiY42itWI8efKgZeLyehZFci9w4DTKWP8KLfqooUkrYHK9MtOVqL029cBMwgb4/sn3FU+RTsD/AvYEtel4sWXep752u5BSctEz2xVxtgoXqlVn/BCKAViJNKA5AgZE08n2BwiJQrr0AhKsZXjitRKGTsvvsKk4/CpAjFEtRhVwnm+E3h4UQ1pPVBs2GAd+DABwLX1dhJl2pDwe9lpYrKIs/JVF6zdQbEG0WRhxW/Uqf+R5iu6PsPwidND+xGq/VcJBL9HT1l72IlzVdSQSEUkSaHPHLKK3Hb2dxdXH9/6VXEDOAdOEVNG//cPkwOXCfEcOnshpcZhBW+KHs47RWLu0EQJW6jCnmVL1dMvOw7iTAjv0pFj5WbWivhOMzXw1+UUX9cwDMlkLRb7sHMEsGIbAG4F+cwcSXbth6PV4hZp2IWpnjS4uJjRyztci79LEzMNTtyfDLd6BYM/lIe62pGLvI+k3toqvJigJIIuUazPQsxZmtoOukHV+yZSWF7mMoTeAqtccQJTpMJOMghQM7K7Kt3D0fYIWPJ6WzH+KBe4pIYTTxPcFOLoi8SVgKKs2JRXagc+NEnTH7HiSzCPaXWti38oj+nVYfadb6A4/LB5UoSllRevH4LZ6GxhA8UZBPhNDJ2eolhTA/IX1wYQXXbvl0UBckCK6oyEXKXvCis+qTIrb+Bdklk8Dsr4SHOEwEELcjwXFIOVaUYsg7XJp0LizUb4M+19yfmzpKFEkCuXUjryueLMniV9h9eHtXtv6qvtk9h9d1xRBCYxvs/yMCK3w/j2f1Xdx5e/rfLPGJ5uN4tbsOf+4RZRjTT2Exe6cP514zldeXoT+3IAX+Efw7530A4C2WTSPh5z3E35gYmMUDUgBvJ3IlQYSck4oP+nHbEn/2gGAg04iS39OFaIRkZWl41hs0nUroYpN6GqMEAdUZO1uGIomd0XEbQ2YA4qiStGOKn4bVC4cZPusxUpJ8MqInFcsTBN+ChMqBDMtUFqVFdNOyaFuLGc+kMdQgu5ypWOW2ITxna+XACF+JNqXGdd818oSsRUmJ8pUEeozlzoSfgCJbkbUinB0VQMgAuwOK1E2+ZWuYn4MIIdMazcKlR7Xw2DnLR+5Tc9DzV86+FE3NBch9NmNsPeXclij9zN6hB8rZuFsERw51kC2LKItkoTATY5QFQYpsanPyIJ5jdzBgtGi3HX4zOGaVfkbgTeEnYRc5vZQMFGQGeE3h7Tzkg3/Xiw9kAMs4ZMu7qdYFmveBMzBzrcpsPBltQnfy21jgnK4UWTFREm0qNoMmzzs7n1aEpDv/MlbxX8ls6nxGj6OO0RlySa+dJyptlTjkUiE6+EGcmAqm0KnQDlmDhHRWvkcPCHORCOQxsGAu+aLHv6d6vc/b/71/1uw6U/ffv2S3p+W6d64jYlJFq4xvrQf06BoJ8D4FR7ffn08R0dPz8myyK8LAcwgeaNolYT/7aC8fvbErbhScyRF2GfDnaGcrdzi5adKrg6AmeiM+p4fsNYLp6jt9HfvaUEbe/Nvc4YUO8M4Y8DewwvyKmIzEh/5x+OJ5vdQaK+hTFZ4O1wzqCDLS0D/gCOJTlXLjlvD0PVmj1zp1uRnk9Lpwqpbf6dD1aU7N/pGoZXoWmAlWtiqpOh9RirOGDN4G1SzN6pWAIRs5+r5it62LqJjo7Nrlqtr04w32MbYVN4tj1r1/Z7Odn+HxpGm+eXrI5dmV+mqx9Av5ZItE4y6RaZWUM4hmmRK979LNuJwWZ0RvFdeqlFT7NPI7y/R9//tYsSVO6Qi8IVgSXicsmiAWmUukVMX7/m6b10/c9hLXV8lehkQq5owh9V5NPcP793Tmtu1hReAqz1mKge1f1CrZ+X63sqbIhkv89TwKtdfaUUsfS7594bpZyu0rSsx0gFgrvGbM1CQ65qnqM7JO8Gwzr5vYicg/X5Q8wtgV2EYekh/jUWdWIza8MifhoxfdVLUG0syxO3qMxzISYHRY8dZuaXD64rIC93nejZUJWiEeDDmyIvwwLCpAxj5GacwkPfhnkWHcIkcaLepkP8fbS70nAMh5EPhf+YPnczi6PO4EUDl4Q73PXd1CCQtrLNCXM5FZtsX4xPRinGXlHo1fxlqb1U05GJFap7vODPclecmTWTZHKZY3ZAyV8/62f0iClp+lDaNRIvB/EO2tet3vRrLCueW0cQH/GdjEkCXfXkz7NCo0RJ6yW+yNJKehZpcZEy3oUjicTbVUf/Y4xhFlSiZuANJ3L5mDVHoOBVWcDuWDqsADTnQHyc5pPHZ8B7YAkqIewazccR/SdFzGkPe8vdD2eiGHsz11aZrnSoZj4Gv+/mQr3Ij0XM+qquivy/O0R6IGtWh+k5fTBUrTexC9ZJcxGDtiC3d+GBM9IH121z+mIdwP0jZ8jZtjR7vdPRZO25iBg3q4nzauEZq4B58DhxzrhcatZ7+3ORLtVkThA23E8DuaGe/iM5mUc8dCNMjTZGaKVIIBm3VOiQbA3a/sFB+Lljjfn12VIDi4UgVz98gyRvpWBJNOPVXUweYxTE70R+o/E8Yi6X0gLkax3a5RTqOijVLNHRSy9hHAfpEzmU1oudr/S+5TcrINXsioynF23rsPj7Cm4jzgnEvZf0ltqfwlDNm6Ct9RZ5Awd7mieFNhPrt7l1puT3G6PYn1hdHsmUCkUBXqneL9AjTEBH8CvcnZ48mer6qnlSFmMv65LmnQOKtaD+l1/feXJvV9RWJbsveoiDKVcgX5rqSHG/74AAG8g2duvu6NWwAM2IxjeAoTwnItoAGw8LF49j3+yW0DkawPrkOpTefgTgBs3vwviVqYa67ZIxOOFKg9VdGYU6F6vC9Sz0aExCLsF0O71HW/ub7IDFtzaSkKQcsME+tkBfehM85sfYhSZjC+ZkL+LT/iJ+Pg1bP8MnM/7HZpURsytE8ac0kuqsNdfS95CTyys2c2GpuGNdhRKR144x7z2f78gKX5r35wdJEH95ZFQFII/87M4w9YpAkmy7dkQtCDNV3ml6VyMSl/l/rnQ1koueLOr69X03dSjYDFIBb0vR7VciM49d0wFK94RpgKpJ11CSlAnmRwG006w8sbcpJG4IY7O0jrFFNxeTLi2dFmU9NLqok8XDkmAFz5Etbv3XRQvp/wSjuQcBCOj0qb7J/QFkLSu6K2JYaBi91CXqTNgeDJg24sjEx3W4GAXcJYeF1wabGokFRGvWiq3JgOR6BYa1L2ntb16zwU6eV9CfMv+Q8SuQNWOKyPHtzc6wzaJ90gi9QCG7FK6xXOv92nYs6ECgDaPWjS3JZCJ+opU/lQDggmCoVBdF9nkV0AKbPF5Jo+Mzbd5atcLAXzKUzsVKDhPx7vJzL6RszJK6LSjIftqbu3bogO/6Kldl9ObChC2Pf4kcAscw7ZBtkAu6i5e9jvQgvBydtVuW5WxRUuUCd9gf0K9UFspSfkDpfDaf+bpMCvrEE78GHueD5FpVJstzI9KUO2p7yJrEU8fO+mVCo4MmMzK7F0b1AQq52vtR5XTBqOzExiPLgrZUhLTSgA8n6p/R6ioLZO9+7srvJaX+bSuJB5pl+De0pz82kz/8V0+Ib6DCffuFPhtUJKzHnD7N3iMo2rIPTFWK+UW+5NT0rfyd7764WLt3ReqKY8s9oHeNYsNNTKrGaC8yLDgikbwOkwCwndeYmu0AR8cX85G+L8ftDDpGPUOK2/MLMAgtm2Dph38uBUv7dZCWpi36+k25/247rZAhqpyr+7usW3Y3gXeLNEUezW/JtFNVm41YzUDL+ChsS9FDtqtEmL8idstIHsDn0UiPECjyrYRK4rurY96OpUDW+S7w9gK/pISn4lCDQ9xQ34xkCOMK7gHaFqa8ZiSQPA/5zJOB9M9jOabEYxMSLby0/GZqxzDDuaEWJ9EdJwosuHkOFay0G1O59xP3ZOqGSe0wGLJpNjUoWidbpuINtIEPP3wAeWmSwBjsjPMKaXOZGrvxy2VCdvNYQk/NYOTAXDKnw0d9gucqS+l1zxHCeWGSdI1CBpqAsW1N6rjVe6xApK4WsEmrLe2+U27dupf8G6e0cic44Ssvn9e0sz1bLT8WS1k7t+Ovs/EI787Z+ezBvcpWDfzQ4CDr9oeTZlp1BRg/Fk+dqa+Ur0e2m+b1gr4Eofs6q3LjXJU3Xt6//Ru2DZN6v5UyIMlRw1wOqVEVcVqSYNKib5er76a6t2pvr1zfGc9VotbFTUaHF+kiLvHLPYTpYYxJ3eLMvfA+dcmg1oQiumYDElpoOD9IfbkFw2mheaEt1e8sfaVN6RqfRRII/4qogKtPNGZEfaRaYMcDeagci0lrmzXSz24KYUlxjA/YQdIwE3jtFC7jmUDI6YgCzwaJmlexzk51rMztWvDBVWS/gVCz8bUfWeLyhgJKf1/+/VkwlQG1QcoUWAGxWmD6pviqucdPm0HL7lJnRpxXTon+E+E+kfUjWRLMclQXsM8ZHLrMOY0ITWnhCGtW1lYf7QSTvk4RTN3Ie+J4DpdlzRJ2i8M1+j7RC+ZNZgH6Z3iuwRuRgHxOXN7qRvI49ZPDGIMwpMchYOLQIDwgtgZED0KIXQ0Q51xAbtaixdr4EZ8wRITe+v9CsHKlQt5e0dWSff368f+fHEX1OEoQ1AvJ2fmXc//LiyynvaMUhgfjUP/QS0mpDIjNcOIx/u0mhUjP24pPSDINSKOrbZKEIEvyjHqUsu2rcH/yyUEAiGUvoNg9lkzgaBicXiRL1pR3UGbXVVxQ3JMan0RwedmILkIIzq3zs/O/R3o7+Cr5tsdcswbEZzGP83Jy5WT/9cjQSguJ+wHdJhPFeq87el5aQIgryumtKYe6RYurZAlMqUVFCwAGQTNMuusjm0RfgBw0rFhrBywqsLiRKLPcshcUcQNInYcMrkqjtfMWEnEFdjhSltP8BMeUQVusPADAHRUHNZPWVSw4QoSN1UK6x5lvZjGz1Tja99/0Mkn1c1f8AztltqRDRaN333h/EeG1s9Sbo9G4+iqBLY6rztv35+GEa3r4o5kkfRwUyra/UXBCik328YfHNHiyLlNNqAazJ7fPaysqxOb0CUs6vS9s52mouT8exJeETynNY1W6yYsNw/2VjnOaqzDL2df6/RL87rRX3z5k8eTiyfskIPd1YYcwHsGQot4GGShKeIBPjTTE064CVAs+8g/9p8BO3UtPBH0Ly2RN9Zj8i4+b845UGM26WIxjg+EzF5LOGNgLpCYwvXOSSQhQSO65BahzwZv1N5NVTkLYkJdoeiiv45kXdrCIU3ZZHnhprEzGxGlrqjailD1cOpFxDUjFTmfgRWmjiY99JwkFMU61v8RPCKpAOlVHm+lHlEv616qOn8nSEEyPSOJJ30v3fh7E9blcfS0q3lpEKEKykElK5I9+0rd7L4lgnqeWEx2rrB4GshODPIww1RoRwVTiBOAyPa1cbe+E4Lj2al4BdzS3CeBM/E/TwLdmzfsI1B4cCNA+WWoccqFZzdFuHcZM8LCcQd2wAQ34FYF8JT2IRX43sATso2ipHjhN1BxeGQY5BHkGdo6gb8vYrwDcdB919iZi5y790QGIyEYtoO9H6Aq0tRFL6Whh/+4TfzrMa8CYtXWSaOEqOuUMTEL3MCkZbPnZhPJ0KEtxJaZp3xQaOFL8XGa4VlK0ODOKs+mQSeaAtPb42M+Cqt1PIIibaNvk0GquyjI5kiwo0i+wlNbK87VSI6r4DbAwiNaxcbjbbaFi7guQCimFhJQiUTaqBrIFuvVFH4Y9VjRbLwe27Ws/1sMnHUL5BCLFY+y4kQkli+RTZ9qaiDrjQg8d/C5q1HwUT5VllpumyR8ATymmLoIhX7VY4oHjMhqCNRKAW+nQXoKxbOpZ80xpuXff6Q3bu0b2g+Kfx0FUtpAT9Tt01SHKT/fo+KCdYMLVTO755TSNSJCPsOJLx6YQ8k9CFhrHoyf5RHhuclbLEDkmQ6GTiPkgjRR80hzR08DBI8U8mYHy4aGh/sAHtb6TpWsI7u28kwQlHcc/VHPqN8Gnly1nrBqC8lRXjC462lIF1wMSFWFRsC4yPAQ+MeqpZ49TJ7V7u+OlVeGmtgy08+Bom+oK38Byxy/OiZumBYVvwyG/8hLBKJr5gomIGFFT77lyj8mOw0MCIeIt5CuIxDSFxk2hew2GhS7G3YvDQLbtMadDXcjm7ZQb3iJEfbDUomkCvG0tEsivLQdUv+YINMxOFw1mFx6B9Ec1VSCDw9mUzmPaXlk032EFeNXSmSqyB+kqfArvd/iWU7vjarwYygDI0+/6hS15CkNDosctAygGzZWduhlr/1Tg0kJ6TS4ogeQ95JEkcKHBjrBak0KTPEYzvY5ofPLFNtiWlegTeJcVlQjnz9ChVGq5EaN8FBQpdeRoaOidbOt6rAww07JSa/Bxt7fhozYhB6NLahGRb905OEdc2usUVZVfd3u2fETV8w0bT2ybNTekifZQsMrPQSfQxVDtG9AGoqlOuXNUhPiFSn/v7B/IkS/E8SCZ44TEjGHZctyY34Up/7bdnWm4KpUkOYyck0t+MTAZnCS3jrWqJe+VDEYgkWeiBpsYXRFKygW0FVl4bXBXaMwVk4zal1ok5hUvuXBhlxmAyv37S7lOt33RlYKnm3rl5CnLMaUmNxyOCICUztiCBWUJQ6Dp/RjtIAm/BOhkLfH+r48rlx/iwvZb7sCzE0Xk/JrFYU/n4EUsNQu8/VibJCncxQcVfJjyQouJeDp8kq7BUPK2bsiCSyeudRrzQfx746IEzAw7nFaSpfZe2mhUaKi3OXVmAIlPYWd9Y8ASe0XYPJrwHDqo3hw8oRBqQd3fF+ox1/wZQc0+PhNiQfEokcC3aOmYmbm1+lZaciFRggs1b2ipnomOINgNN1wjGXS5o5kUTZJ35A+OAR6TU2+oUcoLA65oSpxcAU9rrCA7m9MScJH6yO1jCBEDTrz2wXZ2RZACvnagTIECJivVwjznZ99FPqFpoJMog6FlV5rfSZSLm9Vh2lZ90Vq7HDxNvqZ5T+CwrjInvf/qczFH4E1tLb+ucfPehO3+uL2mMc5Gst+TCegjS/f0p5MlB2wMapB+Ia6tgBUK8TBGhHeSg858n/nsec1sOHNaO3OyYq+Jh7seL42JUoweSQQ8x98Sw/VSbJ8RAO3Bj9dBZTFdsCxOic8U1U1sviDvVGtrTmK7x739+gOv8Oq54cUHi4EggwwogXDH+tpW5me3AJ4CTkvwjtDMQbckey4R4Z8nTSv5BABCp6REXNqJygd69ZdHqrE3Xeu9SfK448/Jh8eEjkkkSxeYwGI/wCE2rDU3lkbY7dR1NfTXqOtFn30myNm0toaepox6b28rpe/jgDxb5pVScXRENejfjdM5A37I4c5EKbY+UnWFgxG+foKQX+L5dWMRvw4MUcKuzoT8gozPxtwiLuYJsZjrAXbEGPwmFfiS+Juhr2hyz/Cya5DGv6GDv8sIY6zfOpjd128Y5gZjHMxTcBHb1gZuVp2VaM6jYKsfdejJ9KgRaUGTsSqvBpCkWXUTxb0UtwYZn3bI/9OqYKXy91xBmguq6OZKSGnKaqbqbUG0sFvnNIInNACEwl/r71Ry7kAuK0NglPUPW1jK20qNh7qhA6dac4X/OJObSb3cqiAe1zmZKzdLq0WjTUKjL06/SEAY8ZdLxeYuQ3xB1PZCP7UC9QGvpKaKVeACQtDDwKvRRqDZyYUSarWQDhJQGLJTk4PiQp7ZunsInCmaT+aq6nA7gtKSYi7PvhKoECvu0vGZrMWM9x5tYmiEUuGeu5ndA1X5/O2RJ95FQHehrVwiC6G2e9YxW8NDS6DmaLSl0NXQrW8kkHTpalPVV1TpewrKLVOzfoIrtLkK1IDP6BHvwN862nIr/MJQO5LZT2k6veVPBVlh8GmUVtfhlpIfYk2NW3Ne9a7wNUCXQqbXoOd848M982SBTEYuq+f/l9amuKUaSAXFSSDNL/Cc8nsAG/Jhs0udMAffjE8d5dkogCt8jynFBtnvETiVjs1Xb1oAKhertcJD4oYxzpUkk/2N2ncHaTURuGlUFvOiBSGeppDLSGp+S3crOylCLRnhC/z8+HFYxPlw55X0MvWMjS4Zd75SI5WcqsqOhZzqHB545M8odQ1C7akKJpaEYD5WJIeRTHjzjjX4orCYn2MqMNjikyYSLm1EscGGaOX51B4i+sgBRUBZp4hcoE71ySuPWpnc1a4Dn0acj8mqm0b1lmdwYqf0Uholo7PgWygnFd5AQVxFY41H0hUyhi2FZhvDiItdH6RKkqedqQH4ZPsD03VSQPM1Z5XPZ770H/T5W+DQZ5FZ8qrxLyjgg0oYtEmPI4e56hLHktesAi4x2tlG4G7pzN5/uc7EbhIP1uJLgIKTaeTTXYRoAmfzusdGBSfcK+VhFcfudsFvbyiU4T6gIGK8Glcsm/4IePt11lo2lhIq/lJ+9nfORxqmiYDPX0bCnsrdC5HC3JZlGmz36xj3WlLJqgx8dpe3JczmOIMtmPgCuwDAeh40DwLFEZFnmYEv2dBKyDujPpaHkUhCm5TWhd2J17bjSlVEqOZVy7dw/1N3dGxiW9BP1Esjzr9ELLhrEZJP5SEPg6XNHY9uQ7UWitHoJJekVLuAAdoYf+QQNEP8oX14Wk+BylIstLybNRInYYfIezpXbmzV5XjUZq3B/SumhkgVlL8Kq2zfhYFy1cfTX/wyUIN5rsfWTu2wSTfDQnDiblu/4Tlo8pItww7y00PEbuD4cUxDNgjhjgbU8MbRccajZxnLi4HdOkynBExKKT/cV1NGP/YH0sBQ1topmLTxYiQLHFaJNDSCDHdiJfxzo2bubhtCULM2/smgTXzSHaFDXiu/N7e1RUNoBoK3IP0ZZlAQmaqsYFeMTHilwNvtXVbZzDYhrwoAoRXSLJApp7tckpTraNCuAI+8R6olnP1JmN60XVKq9NqGXuNLw6hvL1U1zMl9qyGYiNXH+JD9T3XL8KmdTkfjNfxxlh4gTOvSZKhTKwwwST+vRCYGMF5JkwbB+7V6EcD63iFmpe9mnv06bej8kZvslY86qW7UqgETpUqTCfeSz9OS4AqNxjyjmuNl4e8Wm25k0x8c03tDSWW0bwIWKUnPJMkxNuK/R5EiVUOwYVX0LhpITF2hC1EwwpgqdCtqlBERge+lDmjU4h8qSXlwJxBPZrCIRdvRG0GYddXudUmREt9uEsrgsANvay0w/PzuGBXdiB7qJV4cJgsiAPfysbwHmeF6XAhNBx1SbDWmIhHTv1sCwSa/0RVE8DmjTlYtk/8nIMdB2Ft3g/JKJvB3Ea5YM9xJCXi7KJ7jdxbfKRzj5K1UhXDdHN+DP0MTQh3BCBcyjEsSYkxCwhZpWSKNrALzQHFCq7SaqIhKVS/Gl8a0stQSuoWb8KDZjIuq0RcDdNX4R1x5E84pjVAKAbyw2Hy8jRJNiPGy0/ag/lXN3n9dpT8A0cl7/uU4xdcCOWVNW45sNFnnwiy6lpu62oozAovMHH57qiUy/A8vb6qCHcGH9QeckRz6FObLAF7KGr1CfkgPZofPQJOXxLRHtKemYjSAKe0cxQhaAvOuVWH5HIEV/vM/5KhAe5xC0b1DsodvUdNyYVz6PSi7/jVloIrrWWTkZPVDJ9Fru4FoRtW7QvQKAnImQ+MlYZ4ck9XR8fVUS/tbpZcyT/t+WAMd4S9XBiwM1wJ09Y3qfsk5wpvSyrTzDTYjm1mN+iayaf4MdPEUnhTfBgmwYVfMdTh2yEXcYFHLJP5FuvKozG3c/6CY5/ThEs9tN/uq8UGOtCWXPuuV8PjAOeDtPDKn6RMrNfK01D9/Fe8/AIf42Zj5qq5mXuiCop/SEQRrqKPRXP2xIG0VCZ1NpI8UYZ/VjCpp2UzWAj4iVwGrV+vO5toFKRYWyONbrV1H7BACMF2Q1/qIkWrkuRL01nKWDxpQFkhIh96e1t5hritx/A8Duldql+oIYhqzkLsdU0RVDi8MBhOVxt5jFRoru6SRPBZo9CCUkKV7KmwheA7UasBzQtcagilODQK28cVpcxO+DDZH73Ij85CM7AJDWuXLpzUOKL8WJbbAiwF8k5TWp32TcdZKChcn+LEDaRs8VFrqmP6zK6HZ48yMGUfMatGC8ETI7nvYViPUDHWrT9QudArCO37ibGJy63ekeY+EpmWVjBjSZwxa1Qm0CqLR76xupJbT0a4xXy9jh5UwXNoEUmOZ5orB4Irvanj+FD5caPOWANcXfIOPgAhOwUpIwxbhi+7EUIMclwzbDnJHWnkX2+lTP4k10TeaAuFXNTkS0AGhy150IrbJX9gosj6t+0Q+2qRweM5pf4NYICvkK+GKW9zYUScFE0A720owdQiDFcMnekhU0JBAxw7f9RhlqfByUaNe0zSi3DzLVoX8c3zUkVY7Ibbu71bF2eex5cFJYt+idHao4Y6KfxtxDax/Ge9IpFfJ/IuBB4X5qORr/8RRaKQG+lpkdCglVPzAae6xezMNRhps/HDQyOOVTAq8kkpRHFsQ2Jt1hEmGQkSgLWUORrEZoMa5ca7jVXRKT17T2Jc55kgy1TIwYP7izLMdDjITxjFjFVmG+IQkuwVa8jKNYeta/JOyXgT2m1OQH3S4gC4R8uOlmlaEbREW+/sHxdvu5Pe3AjMfEei/bezZUGKItnMJ7clfIg1/S8pA187b43YKPjZ/kLX1Ml8e62SEhDK4yvPjCWMRMgG0YJIhSgfzIHFBsfXjJFxwzpbJljXawY9Kz3p9YAtqjIIvqoJMxdkvUIsZ1Ia5HnjWLFA1rTXJoP3+dNCTA6tBo/n1btYP9ll2XmiLA+2jx7vmQfse+Z8IjG9gdye5vXrd8dHEhtd1fewMqtJiUp+Wk1IYysGmDhWa4kjtNwQmBvjrBY2hp4CtcG0HeVZ2mbsz8XNygBTuqzZxv6del5MYyuW2JcA6mXFA/AQjUKFEeEME9VSMrvcoWeFOWFat4zZbPUamq8BkRFhptoGjdcLKFV1oYjL+ddDKOMHzaDA2aumAEdzxBBSUqZtTgaMJcJXf7Rh1oaYikvTrL0iIKMLNezosbHMeGmRTqTu5WgG1615g6P3AeWofNMAYpP8UMvTtQScSOIa3fYeTzEXmwLSNvy3vlbY8YZaYtW5gJtxLHcNRUKT4M4nV0et9HT6q6FMzt7I4ScZeHSUytcmdfNjzgN/TBQDG5ylCGPBGAjHboiwFqAw1kIepFXaUjvQ1E/k9DmdlvfRMc2BAIJ73knnjomyrUh032xufUvc9ELHwu/2rWN0OtI0cj8k3UHc31oWOl8LVQ8uelecV7YUoafEZZaYb0TeTzDs9BmonI93rS8ZQuj2osKDmbcv96g3/P7Ao97wO/31r4jihNSmZXP4ux62415SvTjn7EDSPXiZlYjcydE4hwq6myA9nz6+lgKoo/0amu3MT5PExfaNZ+7J/J77o3DUitN2ymcDqGqvFEt4oTxto/94ZzANrB45aWjpNHQoFrKzLC9zsGIZLfepLrmXLJEdSyCaaNZ3EFxKnEiahzX7lWgij+vQV7kCNqGnK5oX6drLctUFuRrxsfgoC/grRkfKouA4LTrdW6dU4fvFaHt6qg5PBHlPtXEKKPYe7+EHdNIeuBooZdC5dOrGFJqGhXqtD6W8dD1DNTjbChTQ9tG8JJIqbWpit3oZFLF6Jn/g2DkzIrBMdRY+qbjopM2mLB2n/yST/Ot+jqz5/TtBEzFedQp44JzS4wILI2gbmhhkVsdkRGPBbx4eHV5t4z9rcInyQhncFpcu4MgtYLErROBFxEOJGG+gN7oa55OXzEfRr8zU9BmwwySI/HU5SYN0FMhhaL4kBwE3YiSOKk7FV3hnb7tU7KYVQI98CJ1vKuvTPwkMe+2F9/hCHp79bTQuUy5gGKNRA5qngnVKk/Nx5bVc4VqogMSKOVbYjLAWECXURB2EWcfKfsuixu9AbCo2FOv17Kb0zBggb+y88RffQRdyhQhnO3Y1ZmwGTDOYzKXEcOEkxqFIVAgBlLoz8JPj+moMcMnVotxxPARZmjY8t0hRlawjE9zn0lNA6nLVc0a1tNfHybPFkZ/zJxUWjKD4UujcJ8eI8mUmrOXsctX3KBhUabBWLIp8sbt98NpRoK8b92O2TAR2SpYaji87Si0ZSKtvmDcZy8Z9zliXCLCuzEw+pfWlt4wRMq5YfCVqnTLKoW/N6TfG2Zha+6IdnCliJIf7FSugaUKZBoKjZG6m0N7wYcAUme9fsXNVbDv2IU4oe3AfcElPJwcTD+e0w+tRQgA2Go5IfO0H8w5DEB8FTPL6Tjc1fS6G5yhvM4z9cGwWmEquMr45eP7KTom57WSEok1hwJgGIvr9y6odpr1jxc6FCkZfFuD/dG4V5mW70PY8klVEzkT6Lrp9vVLnoAh6tI/HWd3zLrh1ssN2/RKO4FSOxiq+1lo0wQ+Il4+Fjq2BX90WrFpRBsKjy0/EOgqEi4JkfB2+S1fpmAlHmPZofu2DQI/NQcAWaipQ58Zo+7SMpwH+sOmpTk8PZcEmDWUOM9JGNNGa1EfGdkQzWLP1rLXS9Dr2pOTB3LrmyOxgDrPSbYP27dBo4MgFODP8QhyqOIKvAyMSAGP2LM64BiKhfBLRTC4iSQjBa3tr7fa7Hgnk4XpBDHMmVhXa4wUpYLDHUuEKWUtyeNFfL0c0DvhWhi5j4CJwTaj9qjY3eCMUz23no5Y+TPII+GhqB2WipPFBXCtrqhvHqpnVaRFmj5ylCH5NrzGV+S12+2U0jJSRKyQtQXHDVRwTpIuH8BORR/4Aah5qDLZ96NLlPRq9dvdTDPhyNWC5lLui1C1w1M/468mU5pBL9uIGJzCad42RH1qfmZ7EvOSV6hi9cY1C50WhtAbxbN2gb3hkaMqUGgbUuDiUcc+umZDZRtZGraut2VzRYQxpmSMzlngoXgwXBxzpmEbIsSi6nqOeWPeBLbJETBUPwrZFgh7SFO5tqY9FV5RD6zPFzeUD4mnAENKUfqtWwpgdOVQoK6qew4dU8FOGpdtJGhiqv/84RpeKBJLeNP/o/AfY7X+GWpoolhwroVPHIxDJpD6YQmvJeI30dFvbdbba/x3Jp+8WHGWp9RfRo45tw0xdw2KuFzFc9XpYKrM03oSYbkC90sVo15yI3OXEjqnxWNfLzmuZYfrwWlpLXNXfnCjqeHIS+zHGUG1RSNeJr6aXuggV5o3OXq2bxrtcIAens0kQWJQv5BMDpPVkOE/YDcmgi6mfDS7ENs6eLSgikF+mj9iC47itwi2Y5lnSiW5tpGkeyG4FYMaPaTZtzzu4lkXOA4fLTLQjL0rnwxWItmK/z2GggBKB3f1J/i4ArtfxEeRnXKEzhcrEUpBEl7Q1gTjZcCUUyMsOScuVLm+EDVSfBpl3LzpLYbfaBUg7hx4KbtHx+uCOukrXL+PUO5JDhssyXK3U2SaQ1Mayh3U4kjrsaRQSGR5dkzBfYdEGU6J2t9u8SKvk3yk7tse4nVQT1djLcm9CNUdkfC0DMT7PZx0K5ctMumWxP+n9SwbOifYnHHLeMRBI/7JXGxRXM/byvBw2eHFsAnYZ0jYNRFrsuj96wdAAiHF7xmBNyImlYVPy9V+nlFS/3yS/Z8XMmAguloxeg5vowboxOWITPFUByV3eC1GVLcOWCcsCcqvpuDIOc9MORdUKUGAjPstDULWBPbFwyXm+jNzS3LKD0uNGrDFKU2WHio9HKZ+xh8ygMm3eHLhgnJlkBy1HO9R7bU0xfgUBCkqB0amRy6VgGBRwwz52VkOWHWJl7LtBFV8PNeMcpyl3Ged4lXtRZ4FsQXl0bFo6c6qA8A6nuxUsOAI51JDpL4rDe21PcVcIIOlXJRtenDbWErmKJI0jzvmrdcJMB4S0c+8TSTpXuYlKCBv+rHL0OCIxF/Mv6b0irgvJSx+q+J6WyivRHuGR47rynM6QWJAF3+qJYEjytzA0IfJItlKv+JrcpvKECnka6GJW9DC6hZtDMfyql6wTVaWlP21iOsppM90HxKybpYzEj3O6R0ymA7uUQw34nCYNj6AnSM7KcKijUC04nRiWXE4OVhNkG7nxnNM8zurRKOQhJE/IxjPwR84QE2JjfwLaSmLv+/UUfFYyn4zFZgWrllPK1vJb2S2I0P2MQwjq+NS4P8XeLHrZCYFc+tZro2520unKuNfGa1TD0baOnhwKK3dkADp1DuRk2G4XPR7K/q8AQuBP8t9oAfda8YE3OM954DWDA8PG36Qubq3vtxuarJ9tTZpCua9qPuXFNwP1etwB+ki02gvIkpUnO9st0B5qlK3qAcHT0zV32NnDIqaZYWxOPnnMNeTRcBdWwcUOOLZrdK/wpYDTeVTarjdLdVeuwtpKqonMFQinLj2b3niAzPpkXbb2W3Fg3i9V6OuB++VvYdSOvwPj+9An2+VtcHYJ947ltURc/ulbvo/k9W4nipsmve2+fn47j91oJ1ldEZVwnzuHQ6TJcVQxMSx2A35V07fUPXkIHDNOTIG81NB7VqX4Hb2gq66xfNURJqpdp/80rlF7as58ZxpCXuB577D7LxBTvEp8dQu5oOqVaCBVJvBgLDTVuTeu/IWAiGuB4cfuNtKTFxHgxr52B6TGZHUS9XBARdN2ldGPN1uSgLBxHNl0RPUf+/YVt0joo5cRCEfAebE2Pp7241HM3I2mr4n15zZxXv+oNHI3/+dAafiOn0t1DyA3eafhFf/2+B5pZfP3u5qAZO0giudk1WCNhfR8xkiTS341cOMgDHU+sF6JtoqTJbgLILfUchTlcnwgyDkXZesMSvmSFV2+7Wax8ndqI+LeqC72XGoWsHQaiOJbig+EuAQfcDn+cukcTDALb7Ym1TD4PJzojRtBoLZWy6uHSphLoY8K6pIYQ1jYVcunnWrPXsj/Y0YyvlHNZKp5tbwfIsil+BCgS/BB4zO/XB+kwOFHI/7PVB52gMv1L+Rctgs3BUZYL6/0KnB1L0RXT5qrzhz3zaazdW25yOpzuHGed3/gM+BE6VtgHiMu+OlQHv5OMebghY8qDhfmWfeDEFewioWjrtS34iOBEFRhaHnp8ehmMZaIqj6pDBqe6bDy9o6dTFoXV+XvjvLAFp+ixRTLzPTPniiHZSWCj8DjuGMoFVq1VuZw7bctWJGZRteE1wkOfgzG7cboYQb9s4Xl/kqGUFwBpjEWzTKYpmu1mozKNjNQ3dI/Q4OP2/Ba4iVuBCzZg3VGbQqEW7wT1aI0SFOKqdstRnNTjbKgcC2Ph8/itKNI63wOnBJFiBqfWzybDcWJgz1kR2tiWSGGEtEoIlC0z0WPYdZUuVpViwrz4AyKA0ByevEYXhgIzQ0mpJ4aZEXgXmj5A8X9K9bVaEoRdA8tCZj1PRon/ZOjXjUm4rp3AfXCEmQxU6uv5QhMIXrIGqql6rQ5OOsmor9/cipSmKD77VjtcP2wMMSYDwyh8aB7EuKP+ocMA4NpjFdcs+x6mbQNThaQERrQhmasQwgibndXF+uik6C8JBrsoaO9rNWGiIzUghvtVt2vXFWRNRikE7CZfH4RRbCfmIbljWdmL2qGKqEurxgm1//VVVp9dZhcLxDuDy6DMVGBADUGT+JivDACVI+CO/LMTPHuh18oub4mOd0jO7z7dEeRCWT9fOX8lNP2sK+Y0we28KmgNnFbRJNOzzE/yVCIsNk+GkEeRQUgnGwa2CFmqzGImxHF9yl+IAwYSkfCgEaXjO4bQH1Z9m8DROFhzLUrqmKFehQZt6Sse83ZNxmK0HgMJy6YDBAM3oy4tpfLx803m4MIXApf84j9FAXjAFNPAfTebB6ssqvEEOyJMnBBvQCO0r3EBilDdQe6ndx0tzNwfLIu8rA96YEoMdndjksfO4za5atqfwPIajfH8UB72AY170icsnFjrdk6whDT1PD+0rZB8Yn1Mj4EtkZ+9G/wdZ47Z9oc3nFeCA3HyvDTWc571AtRLA/gWaMV8lsAHIwEDg8rmWJdrUPpgR5oEehOKQOlW/XwVTB+qlcHXwpxjP4QowEwABOA1kH6fm70bYrGYI4wdVpE4Kn2DK36/t2GT5szNeu6ERuzRv2t7mm5OKGRlVFL6B/H8OnmnJqq5eac2g8b/uMZOgOMmsKFOMVigGlG/rndra7td3F8UjDcqClcZGyJEhP7NWLaE8P3x0v7AUzpEUt9/ENvVj1CfDzjDevyVOLb/V32hJa+Dft+14PdPWbTZ5ygy2gqqLob5s+nTk5pxVlC/OoJhw4I4bkScdEOtwYkbKUVSm4+tDoC11VTiNeXXW5xlabWukKlTG0yPuN+7yBewPnxk6kzuzj49KIOVVUVhu9fzWg2W3J+gibBMDfkd+vyBoSVCtUdFa+i5ZJbeKG1mpTyMjg7VPn73SuS0P+LN/0MykJeXbXGhTNxcWzLRMft9e5jY8r/YOQkpKhjJOBBU6iyla/tBGSNQdFLRV2SGLFoRSTbQIzu+/JAIlQEsG8zzN+3g/vsXKUDTSV0STj81riDli5kPmGN06lalrLqQEWohvtMQwMGGIiI1RvFAepL3R+oTdEPq389RVralGnJqai4qrMW3pGJlmGCuzr0QP++V3azKEXhWghC2AXw5qymNme+OxisPQAuXC8QoqNe2LFvsfI43j/c6aPHGTE8Jj5PLCLGiTrqvzjwPDLVpd8KvSER+da4BEtpbLGm8Fymsm7YnI4xM9KxCjzALPGSnBCc2gOn5zK53o7wOPM4qLxShP5DXAVyPQKH2SQHuwg0KjXSNxj3IwnD/B8CewnvIB+uxfFthvpng73R3TIeF4P041EWftq2MTfJr5Z8XHfre2slbzJCAnEIrO9xxHW1o1BBz0BRCFjFKXt/F42457nv8+sDOG71uQeKdQ2OOAQLGMQABp69blCA9V/IOr/PPc12I2tBLE2y77bRyEtpPm1BRsoiVIqHEt7tq3LqKVfFs/zCH/CUdKuER8uRynS2CvW04UO1ipWR1TjYkKXnq5pPXlKUBd60KtsV10b/FAXViiwf+H7jPx3Xzf4EBfUUu9YHXfLgdB+Jg4KU3o0K8dszNysyZGresLC3W4Sl8JZLSKDGbZdxAlaZkuzCLRC1K7PWtvi0lY5UR6891LsGI3e4g7t1oIXBfwzuCyiB29sHZA6EGc//Ar9Br2EJUl1oOZo8/nSrI+0E6tKKNvrq6R8CBmkUhvVNkK3L5HpBsIDUMphQb3dLBBvk+r9QKA6tsnOsFN/otVCnC/Bjomk4h8DWhu7t6GFJQNAnfA/qf7iseUdhD7iCtsbQy6NQAeTUwv3T6nDSXZH0KDVUQIRZ0+91lH7pFg+8yBNoHwUaXbHVCARNa2V3U/lyRD0mgPNXB2PbQvBvUZfgIHER52PFsfQQWCqmikeOmknkofHe9ARUdWP6W8UYn+CbXM6vQcuVYgP4n37eu0oUiETwdRFZr0MdBeB+McvKZnGStT8ckboOgvZvverBCdgrqA1GfHu9NfzOCMEw0MgIxhhaoanWMtCP8cXY3x4jv0jGyMPssd69oZy3JX1wX6h50pvVyrCgq7DA+ytvMYcaH8fpaHD7ZSxbAc9qjcw8VXQkBRrg7UZvY+dcwFwvQPqAEG9rRbjL8bWmVCmpi2JGAjUZMgKBOqFbUblIONuqL1KGjVDZqBotijiwikVhzkCSNgz5pFR4JtEaypEKIGmh68dgRvhqySgrlJBFDE512HRuwA9RN0JnMcjzWu8p2si0h8PidA0uPE8wgHHdJKJEwFbQpUISNrkdiohNrMTK4LGC/Uk2AqGNoxgH+oB3BXozJQ4m1bi7wt7SSPl7OqfB6zxF/ZWHeaLpzQEiJmTplgAxASxZx4WTC2HyEvpcyj2Q+DhS6Xmsyq/M2AWvUA97FwmG/iLeX3BeDEVETUHejH6LVl2lBQNSeSt2ckqedCaJtrL4pEQen9YiZj49biKr5QmV3sL01L2U+DWxn/pWvhkuqaUk/2WkjDoqN5bBfYtVm97CpFrXZ3MVUrOsQ1SS1knv6U3UZ20vzgg4ZnzFqmZPqeVZQJLxsncTqZdyg++pZVbYDwYNzMkKmHM1h3WcmrLWErAdq5C0x1t1wk8LJ9ieV3HfxruOJ7wBYy0BpxvpMzqYa7rkMFMq+mXl6dnG4WXrgX66c2aKAylsXx4FnLVvxvUgI/m2M7nCnPptbz3Ts8ilkGDE3vqG7cuQGrikBCUwGQ33tRWyo3USwcM8HsBJJh1cbweHmRRxbzx5boK3R+QDf5Wavl/1GZlS60ftVfY9J+bu5+FLEGk6m6AwFmcVjEk8MtkF7119ftZ6xvVgFV4MHGGbvQ46lozvOP6HjcjPNxcfTAD/Vw8uwJYarZ120riGbmCnI9w1ayRqKUOZiW7Bc0QSngBTJrZAlt2YhPXMbqPzEsy8T293rf/tkr4IEVJG5Rulr/UOzvvUufQcSiOGC79/MxnvIreLOt4ZZMbxp6CK0xj/Atut+kpbEjFeJ/E7asRI6XUrziXUUFwCRSYovqqpP2GQcUu6T58A2nFzd7vXKBZ0EQqIUl0NXiS5pFXbvf5B9gcj31uklo6esblxXUI5vXj4PRFIDous0gK79gtpHPVdK0iCkVU3dsx47oCaM+488IQru2zeBZ5yg7hLwNjipaWxl3IH56/S0i6bgFPp2s+yCj1PxSRLP4GLK+WeakYqeW1IdxsksIKEeEAwMEoieHcciAZlVUxMXGHyIGGyF7fFSVmLhk/0Sm1MyWW69kE22m5uBR7hkRsSXXnuAkVEAKwfFM6bn3s98tBz4E0FRlOQYNbImFC0a3zmIeCUXi7f949RwRiThozMErbDKJf1rCjwCfqU+vKrW3H71oc2BbNWcK47KExDy7uIh7MCTO5/2mbDLUe4+KHfjJNpkxwDb6548gfKlEx+EUyh5H7q2OmDanKY9a2ZAsp683Ge1+/H6542HWhq8x/ncvGYJ+y4alzYIy3ZYCecNlK1JtmrGZ15FEtVm+lJr8P/8hYNNmIBxYDSs49mgddASOm1MrzMZTaNgLgk46NS9NAIh08mOQ7Y6PiKwIpOetrZ6p2GvueDruL/HorKsI+m0HZdiMhxUFqeMqAtJ5Zs6bsiG6Q+6E6aC0mbguB0AGWupTWHCMWjLGrKZPv6SMM6AkTfAXIA710ok0nPTRMBYQEUbAEj7mTaZiVdwpKh9Rrt1rVlZ3uJtY0ctqlKf/LiXIunzVmzwFPNWttmo1XevSDCEBDOZKS0rC0aDCXzYjKRocdRAkuuwTwZgSgHlKgMr26V6s7QXI1Y/ECr0SDTUVeruj4f6j5IwHnQ3rGCjkgImKu72qdo22iKk7xf0z4Pc5ct4j5kuMm5CSvNjmEG818sav9jHuhVdMyqYJGzGFaXowb5VAXwbpE7V1A1OrTRdL8/KAd2ysU1/irGbsLeDdTECFETkB3fb329W2oriPoO2p10T7NaZJsa3bKRHLmRgw13JLV++ca11immNHqI+iFwQKRtw0YNdNpun4nWeDGqiHLNdmaKedM4Q23m6B91SJAssdx11xSSAnH9anqPuDl7BlAAr7n4xxY8phqmvqx8TU+vg2B//Km1bFwHAFK/4lsJWtM0K8Ev5SA6TEEdhj6NVMBE1FmRqwn2uIY0ZCxr54rnu79hhUUW04rK93Ec5SS6tZQSgoC1FJHCiUukfL04sNBshLQRReQYtv7H609AWjGjO2iOurrs1VJgj9F6ah2Es+ek7MlaFs5+BocJnybrmAwYabC+PfXP0rwSLDf0+tnxN9tm/Le1HxmtbP4VLDmyBqOQzQzlz1lrTDa6Sd9sE99kJlzcFAhiaEHFPsvUv4yvk7/qV9XC+2jPQTkZWJgbbHsMlIcNEApKzPgbKK085SsQt2cS+04ryiTZwQ53c1Cg7JEBArIq5K/tmpL0wuHlx803shNlEHdTBuC37e0z9MyodLcHii3a1gmogX9TqBjKU6nSSiWO+K7netb5EdplMggYcDIufdADsHKUF9kBtApfPi7BHZxcnpTg1KwyAemWJsfmBTkb74uComlj9SasbIUQO1Nqg56CwUAsObhHNzGU7tdFQkQwQuO3eG4h7YqNEGij6mrTFhBMLvqvDLjNOqMkmgRBjghMxeodHmMopR8q4UL9uv46chYIeVcGOBo9pEZ93Jsldk+vRkL7uhGH8nXXfCEU5dSPxVGfTbe48dVMkV+tCV2c0FYohapvOYZQux00FPcsBTD4hZ8y/uK2bQMAkMq7zLzvarmWL+InMkr+zLG5UhrWJi2JBbps4FpZxqfYZE2LodGZjWylx9mKaUmg6/7TT4MorvNAn+cmQ2VLBXn39xoGQtYgQ6BoepNzRd4tYgQEhSQtQ1XLBhhK/Tp7ByQj47k6GdtPWuZ3vO3y0YoroDuqAjHTnoOd2RTeBLau/OcgniwjsDoeVLc/W/+cnj6FjKk94uSLE+PupWHOBk1dvjQSY+sL2+sDk5Fk41CkDCUSj1JaslnsO7saQsCSsGGEVv7fqn5uSGH9/1MiQEpd9Un9Av7hCXZHLGVusALhSv0b/3xcBVd97fUCqpRgTDs6DG2SMNWlHLHUDb8Dmf8vXyIdFlE0fWjBCP+4TfWvujkHo2jBcokwa5CMAiJ5zPRwp0phsCgMs0bPBEuDVuteU0JAZdVfyFqScRvc13wvjLoG29YQ3z5+G7RG/Vh7rWz7lWfz7sdM7wls21rQsLvTmurFY7nMEYjLMnx8IASa0zCps44GqDLyoSbHFSjNqq/hQbBqiHOmkwGH/9K+G7q5ULn+A25IdLMnw68DDu9CPYr32yKc5+pJefZFUmkFupaJlyXmTYD+MhK2L6oUgWjZLClST11bKz+rCEMQEKSWFdEB41qRI6pD+MQenjaHcBtpurpC4AdOytYG8vUOOSQi4ByatFgnjJUbfwp4CxZ8suzBzaelQ1j6o4iNOaIGmJXAEXsIbJaVUNxFsTGDZc0AcpYHQIYTwlR3d+G+WK9dYxQ9B+dXFLqU2v4FFoaZMtGOuJuKd8Anm4qEj8L+Xs2DGQ5RYgdCOEqEwBzIE+oetnqakWjJKucfgnP0JvMUxo5Pb7CMen+ZGHrw5DyjGzLt13DXFRoKGrtDV9QCKHYqyinoaeZIxekerHjgLhsu3D2CmAmrCLalMrC2rClLdQe3bFwmtn/Vz6GmNhgH8+Irns8qnU5D30YfTIYuviYa/kf7IkhPwJmoY72p2J5LXxIvw+GokZSYnL0vI3FvkZcChq8aEZpQpxVzxLgy6xKpkTGDhIsztMkJs2bWFkQmrhQh4YAowig6RWJwlUZUbyZbYPxr4NqyBUDMOAyd3Gnvz2g+5A1IATFYrfYGh9owSAeT5cM6ZlSYo7h9E8cr3O8764RjyaFrzqmuPMu1k0kdkFDMAWeyhNiU64LqqQj7jGkhDIFTYGTvOkfbvHI3Ei/hqztOTaGdgwcmoYrN+DP0kGmPynpCvOLt0471XL0aDsQTbgNEG4aaGtAkVl4p94egCqLcQzxQMQ7B8vjl1IrZ7AvgJXQw+Z90vYmfGN3a43FBEVdK7uiBUOxC3mE+gmbUre3wYjbGmpjL2nDEkw0yqE8uiLESlkEwNn0KegxCg6QKEiSZKSkpWoPF5jlchfLsh8Riuv0FDb++7Ae9bVecuRHUeCv4n1LK/RAy5j2Lk4gJnyPVPgxKITa6GKZGJPZu31/Qd4YY+aqZaugl4q7xGpeuMKH1T1Q/7SlgLt0h6uFXAzDwb4zzMKpsR39ioseqo1Fu58KMINIYruKva5EGXjom3tjLWWkXyRFoNSHoBWCwuymRByuTooYwSXnTiLyPtprkrmUXrpcUBS5hulQ7PuYqDiuzVQpsxBRsCDw33Mf+yXEoCkSJmPuUsg3tSVFzSNzwY1VF4EvYufy18URHQ7vW3oaDdjsY8TZmnua1JqEtvhI7b+Ys3tyh6IzMOND9YaV3n66Gzra9o922xHFwI1AkVJOlB6MzubL1QRds+jEtAf7RCE1dzzp4XR9znaY2dXPM2zQQYFpz+CLHhRb8AAll5JP1Y7EL25cB+ypmdlF3rsc/P+KWa02sw9lfN+KuY6q4nfRmW+Ki+8aI95jAgVlbJ7UOtUa0a1Zk6aSX5QnFsHAbTtawg5qgs+dHjjYeQV1ImBxkwU5hs4qk5giNllMH9upWqGNwjF+98X8ro83gS8If7TOUWeYnmn7oudCXx1dqvrhWao10VYWGQhfRVyI3RV6MiLdN8WgF3e8pOvf1Xmv8YNZtH/TBVqy6zF1PFpBmv8YJkFpYC7tqxiFZpLBzOnEr0r74u+WF6NfUzcT3kL1IJGLcTs1hxUIsExZgfrJWZn8WWzl9O2ItZdA6v56f1dYD34UCYwHwnVPJq5ToXLQT4rbHutGLpBUx9Ef9V0950/t0JrctE1rygF2vSVgeE4oEVQyh78FbBEM725Xj2HLWFUrK96nTprfKVZmG573jZwZFXWwOTk9wfoyQo3uCfySvTEuE08mLaxrQkZBc33jm92HzQM3zAQzep9ZNyD0/lBQGea8jCzGAHPaTh6pwlkhqx+u8ozPbjA4LAPa2IC0RnCQ59MDG4lTbeSQtUd13UH3K403px++tConPlP6it61jWV1LX/aXqLgWpreXA45FihYEL2nT9qfbh2FOFHZqvGUB0pNiEgl7RXBj9Qg/S5IXMAY3iu71mNGrJq27nBMUsTGKtLdvtQaAjJ64Dl61T6UPMpaIYwNESi1OUmuWcq3ZtMk5rrHPVyNoMUyDL4n7EyCM8ZzgBL6E+fDTL7dTzIxZXvcJXRZ0zqJbpcagSICPqpn63vRakxgeTxuDwZAJyOzqmIbMLbXkUj/hLCAsJqTDPLpC9g1TFEgQP7r3G1xtOGRs3GkvlJFZDzOsg3bYKHMIW+d4daglzUwW7g9uOldDEFybS9KXzbKm47AenGTUxTTIzTQbOwCLGdHukz30WB+Dfuet25B2OgmClIx0/vDqstnq6LekZaQcSbT1L6sim0ga2aM4TXTCoZ8C3hKH6v83YNxXUuBqnRB1YIMPiDvfBArc4uiVEXfAq8HJJwjFO9cpKAjZbSjNFnU00xPbOPG2WhtjDQXgeka5Xupt0OO/ulNUUI/Xesm2NVNHU4m59YKmGHgWlVXdLOmRsSbyprjF26FdOTcotheHzC5xJR3TNJx0Lk6A2/PdnFPXcts0DgrqOP2ipROniLvqUoCpnptNCazTg904xqB3qy9QsqfjpkL714J0N1k61/LeZhWnpgK55K/0HrdhK9h51a3Vuf8yjDb6K+4Fr/fDyXdveuXqQPI416Fea+u8HsY2ZvUyPCUr0khOB4MnEvNWU5ZMzQgD60POrnlTetGEpUO/VT2mPvA5ESN1Fvbsc1tvI2etQcRpOsJJEses0w5nWZvLfdVWkPqwgwV1+mDaldlmQP+X0INZ0HmXYJpPgdqCT0ZHLlNw01P4AoZp5wNjJhBnxSkutgfzVLlilYx61+9Dw6SAjQyMtbGy1fw5YQHdCTJQ90ww2Mb6/BqmQ46EqGnFMc9wCzFIyw1jlvABLGyMkRKvt5ulsZ1dtt57v6ZsU7YB1ONRLoKyL4Ax1B0LVT0DVV0qVeN5cnogKv2ydM44AZ2wfGSj+G3MJ00/gzSYHSv6pjYFDxyILBy1NzDEhm/quz/qeW4QjRx/aTWHvG63GoWdI2Rr7e24QCtysh7HC0zjVtIsdit95qyQQivr1it7B4YdBg+OLJvJfamjRJw4AZRb06cMJDIWoj/UvBSVjc/Fl3ApZnQZ1Vf1nA+u6kPI4WNPofiIpJRWsDn+xbZZ/1xuW6VckCX6GU38j5ISr51LzJFPXpmzVkYYKKSUjChbZxuhjzJ7LcQmejB0z1mUcvkV0e2S8tY+ISyt6dLmixRbqUVKVL+/6MkxqDAE2VU8GgMXBJeEw4shTxoIeer6GHZY3PmjxlgRy9S0PhZT8fWKSwQwl1Qo1hlMxz6EaQijWL+arEgid+pIMpGUbFyCgoXxFxPxvzIhlnRcIO4187Z7uL/41qSnuWAGUvogadmkIUu3Weooy53ZNNcwoB11sjrV/2qbId+f8hem6Vy0P+JweTLQrtOdDbN9Ol7hNbc3IG7Ba9KfBGh3N6UxD0jYbw/9KZGYvZyWNcSmGvgJWRNRhsD8a4RxlHcl7T1s9C+uMrSLeihT14QKm+rpXPPyBYZgyK32bEfAQg0YV2e5LA6HMaPQP+f/UW1BKJsVDMkFadguF2vzbq9Za6LSvtxIrVaC7pVJjcdZIBOMqstMwIOnys+lcx8reLx7al0S8rqhbKuwC3r5LPtjm0eLGgME0aLOq/xTLjZxOq3HWBTc7IN6qsO5MkVZ6ePr0zwlpEW+xJ/mAP5U66VcAS1ERyDLJExKtbWCoyWNEnjNDqaTi/mDXBGdS3Sf5as1RBBnUFisVKB0bnNu/XPz2Wi4+qTl+ogZbrLypG+XPxL6mM6jXOZhUYpChLYDu1L/CkP9gRLmdxV4HrZh5CmTJFaFyFiYN7eoFcPMQRLGHFQVIZoxv7/0XK9lE2BoaBjGQ+PTq/mGK5f6WM6up+Tq2ctlwGVCtTBB/cGC3eHQQjl9kGULALWg6kmS0T95NsetVXR+A1MJF3qAKwiqzfJ3kwQRJiNCqTh1FlmvGFLIj0FSDu94cu1+/rYqwsq1zKE2pcyDthCNujc45JVcCFCF0HR1z3UGskHiZOrlrThtJGYTmgsf+wuJxBhOKwkwyGkU3TaimwTe4srgRjGSN/0cN+sDK9pn7uc9jkar4ot1OX7brqd2kirxlFZzO7oCVSb3oHSiT58j6wUC+H+5T2G6cmWnlvaXiJKQn3LK5j6CoKaHBhSn2rjiDQKlTQ+SInSIh9bJUFEOOHQxbv3KPgdPk8OdmADUW2/H4HbhT8yKA750NGsf9bOVQZerRxtAh1yWWzi+rj0f4oDA0cbCWlThZukNsAllg3oEMNvicxUfODusaGNzmtTaLcdwvxFnyBdXy2RJ9Cp//T6besAMfwLGOmkhadIXSOi9j2NjjxNaf5yjgr/53Es1kfp8gQPAIl57fzEYzPY7GP3+8uRNZZRq3Ml9FJjqfrKUbEAKGJn5VG412+v0XeVTdBsh7sPcqKoXXwDpOZYu6eXUHpMjth1ws+HIs3ouiEmSSlU8Wpg5YHJRSctvPofHHX+mMr8j9GUnYeHVrrYUQeVzwP0alnyytUATXUdgt/5kS2LOQzX+uyU98hDc6a76pmg81JU5IM1TpZwBfmA7LOfmlmQkmC5LCslc9Q2K8loiVrZaK6fngBOvMGw1xNODumLH+zd81y5uyPPKqINvWdChgMLnGi28hVvgNj8vLKJiGTRLxv7ZME9h92fQCC9A85dlQr4fUox2hNC96XlHTmpwfsr3VENxg7QwXoZqtBGygr1XMtrIWNPhWL1S0daRqr99klY2dE9krVKnNosPHt5waCDfYJgRRcfcgaHEXYeAdt1bDyj2vEcW1GdAOud/8UV786XWPuQoFAP3cBMF6XXw+vY6N5tb2o69uoHcCkDeXBeGJ/FNPMlDkhji7/D/PdWOy/5m+xDXnzu17k5P7VXV/Cj5Bt9EGtid8vxd9UGZ12s1EvnLVNrFy8X2Sh2wKE9Abp7D9BMrpOMQrVQxy4ObgOWk4fVnTRdQ6+g9+M5b/+zySK2ZwnRbZcDlpOVKj3iLVz4UlTz9SO2wnJMtevzZ4Y5BURKmCj58TuWMZOFE3VIaBGgkOvA1LZ2DY1oDf5piF7WdeUUymXMTF7mRs2/r/e3w3ki6bQPOF66/YxiPkv3Ss2UtvsyWxbgFUu0VliIy9AtVUlRSvySv5tuninkXtdXJ+j43x3sor2r67UFp+E1/LdU7At3ku1yhfJv8DkMz++4JcyooL4Mxr7Jo5RlPEBuBtkxCe24Z0R9L56f28lsbzX4KkmLLcnxeOi8qsEvdlE+OI2Uk9q+CwKNztUBRr75IOTfbPs5dNf2yMBp+BUdjM++XD/inNF5BSDb7cjICHrGXg1uvgxe7DqzZTmiGsMxthRaHLkd7oQXqnFspPLqBt1wvcel2zncXPBx1TeEmg1Qgjso/PoB/Q9Rx7HIEqwN63mFLQJfqHLKxY8OAbSCV9yfL2D+2C/sDmdWj2WTq2GeXtzI6F7NDBy5n5owzO8vZ65oz37xFgKwq8PTcV0aauPU+nkHQbVudK0tLyokhqIZhOLZ1fC9xP1G2hzZN/nL4UsCrdLdC77nJRd57+y2mUa4xIFJZQvpJ1yjZa3mTXRvV1o1I7UocM21zvQWSC1nlU7N1iBbdite1QFCHiS+MBLqeD16Ofvyy4IeebJnV0OB/uq4Q1vTjly9HIY7D7ePT2UEll/tAhn+cA9pa+M1eowrj27h/SSok/REGrXADOLEYPEfO2Kvooo/hFtheWWQgjWgTOrDPfbp/9jxfKcp8Y4gQ6bM2oS61V8f3BYZTTRBwlGzmoKEpaLxl7Qs7hfl79LFTSg7zYtIwK4N6JNeahKczq1m5PddAm0qhKsTnn7oTRcrD1cdgr6eHGx8xQT+50hWbqgvXNz9M7lOW50+84EnDx16uon325Jq0N7f8tPZyov2M9mmm9s9SowFD+VeQjNqdTfTL3a93uqicU+XVsENbKKKdnnNyAbWzGNppAq1awUtzxKGsnmNhybSi8NpT0aU9ImXuleBAEUL7xZzDIJ8O569zogpmaHQOnN7Y2XAkpVGIrvztTEoX/KdOtmuM30YVYFL9g6ZGwMFGyaLguwbUWaRP3KtfeWMO16aJrJ3AXoLUBWky6W2SxGiyQNljpstBIkbgrjY1WoNQp02ZLur3pNFfJ+tpQx0mYxyGSc0uveK+69AaoavnGgG0U2Y8VWM8GYPtHMpTb8KCs/JDPDMfAVo2wdE8Xfc2pDjCnjXy8JGx637gFEkYMdG0YQioOfz1saj/GWj4saj/mThXbTvdM3v7vjLsZafBaytLg4JxGyRq62ryQMBSBFNyJVmsongIVHuDAtj19XIMolOy8XMmrQbnz3RiBW/s4qHrw+T8+gOfg5OYPN3KUeUhappVSgkc+iZSAy0teA8lrTnK0393LMkr1XSgaqrS2RdduyXhTkMESquXuT7ZiESUlVq2WUfAmunzoKAmc5VUDgvX6HJuKJy7K2rQsgi9sBpfnfgP2FhWubWDEEiIRXdrPE5Q5bbN8yhKs6ekeUiv7zlH2Uxuq6Y5Eh0IxMqb8fdAaT1xstpAxpzS4IwxaR5xDsiv7/pS+a0q0/3OffLHRLjvlwSTDUChfZje/aP0WX3qfLxKooDoG1AvRmP2C1+Dwz+vuiGdO2Oo0cJVu0ulFvKBK+sOG8mab8lXtiZsmqG6lbekHrrw8HtW+ssEpl19jTvqkX8fuhLfO50pGd+6RzDHMYiYqljKjPvdkDLic3x+ZhuzS1Yz93S+1A9qRTQF4OxKOYXhQHtDenD7RIKnKAUAm644zLYs4/vgIL+5P0lh5C//+qSQjtwj2sBo7xeycLbh33PaFd7xrNY9UCn67KIAru1W82Gtwr0NO4r8Ak2I9vomz6pMUPoAzgpBs2ukp3blBagPrGWyCXVgDQBWgaEvcm/0amAgN9t7Aq2ZFT9yJ1Yf9wRLDWtCCc20421JI+epbN6K67gwAeqQqlO9/axJsJJP1SXM1dL0cKkRVtfWfZ/Knraocjy3evPWCu0f6HOoAXhF1SUBYBc80D0PYD72S//3qATe/S8AJvD5RD3J4mXVcC28FP+qr3UuJEW1WVUXmz08an6Nf9wbXn6pt1/4YOr0gfrgcjj9DOs8U8l+Zm2aJs97xo5/BoJn4vJHvTKC9YytnGYM68qkymt2KgZ7WimMKd1lflaVuk7XqkkZOzfpsFsLPc426H2PFH/B4H3K/QUlgGaqdNtFBdP/DeEqQsPUl7OewUkmrsb+MsbI1NM5U5jeLHS6FLRU8O7UCsYUe95MHX2lm5nKLDPNrfdhXs8U9UeJVpGOV+hwqTLzVjnO//jw1Jm1WaZM0TQrhnEy+UjSSfooxHHec+SnQEMJzP9l7DufIqcrD4kZKqTLUDM0RZQ1vY4jY5l6H841XUtVCXOd4UN55lWE/kmhHR+hMy3yxj93fFq1ndOikJzlDfkSKIfdVmYwbFAk5ZUFY925hjIoZ1OvRkOV4De0CmOs9zSTN/cBqUqWKTuEvmfRRVWON2dckYGIckZuprzOHJ1IWILeRzfuEr210tab3fD2R/OU7Man+qKqFhpKLPMRa6TsDVL4kWBuLjU0TCsPqUq8Na08xEYSv8p8XikYX+PKhhmzNdNKwc+YlykT763TaoVEv0HHECMS5QZtJu6ncSTeLlvNYKunLZpZzwH1Ee6TwcFUqp2lRopnGPcZkp4ygvkysaGhasxMr/XeUQ2ud5IqQ7yVZ1lVTpxTZxSynouqNGN5zF7hKmyoxEyVeruylDBjVzSi92cEPiTEjW79uEI3jYzfVBbSlShC40ycz5XtxxldNCD/uIbXoaqy+Fcaa8s2fpXGGL0zyIdTp0gxbR4/PHnV3hgNS0Ki3qASyilLmLNMtJ5MPwqPEp098x2yxTa7Hxmxz5TQgNJiRsLyiCWvEIV8EdNTvHZNGZHuGm7DaDipZ+neLLX6aPrFeLdBGaoql0EivKg+kt+TjlLjonpaStCbIUMe+cU6RY8U2VbIIw9GynXSM5aJjnVaqciuDeJNKYS2CcWwxmg7ZnhgTKBmZ7gF8CRm3jUcPGhA2b9BNgpl6JQsBNa5bchWJrur9saKM5pwmcr5wKxzPeTmFxVKiRmZYKwqfs+M4n9VepsFIUqTGYlgZjR8hI0od0piZoK77AaebCxB7OvjUaFbZ9hoLIOyr2FuNxKXnXEp1qOQuqhOpjh9UZXOmx8VZRSgGyRlhuJrJfGfI1Mp6Lyo0PsbJEoJGKlWiAUjsbQRcfFi683wsriNdCJm8ja6+aIi1j5FLEPJpikHOlYwKaGgedcQ3GrqMzDE0kTvI96A8RluWV6292a441vZKbYZHhZZNvruxUuUMcObUiw7WbWCNyVaWjurk8Jcr9QU8Z8q7ahmIBa4jl3qTbG9xSy1Agzi/lOO4B2h4CxdHUNLoKGyi1WuSO8jlidLil9lS4UNRJopZeRkUNXeqiqtMe0yJKZdRIZpJ2ilSDjlSjFuYBdjmXmjrJGTUxWyxnVOGyssU0J6zTAEdYnIt4HOIY8pzHXUHkrnc4dwGT8ovfaMWPE/e1ei5Z9LUzAXLpuCNlBlGZQCK2jeLku9ygVh2eMGRo7IZ2YY8Z/SJhsYkRYgOypPhJcplWZokW+KHkeqhynMGS7KK0NqzJApnfjUZaQSjOUQ0QwcDaI38shRiKqwqlR2cmMDKhlw1WQdNo6TdtbO4IYMSZ0Z8qpX2SnFFZTIUwKlzJawSExLbOBSbxPljWSVjSjhI0JVe7OUZT4Mroj4y1j/mMGhJGLtD4SdbTT8cynqn0vIihOxZfOJMXiDwJaKHuYPfwUADNMjIO6yiXEFJvJU6CwSvCQlZrC5epVuqHYKssxIOoMdeWYomMgapwyWN4NJgVYeLERxU7Y00IMhi9GA01pvNz8ix0W1lw0NY+IXJQ2UAiOp8ZcDb48saAUWJSGRroCiscShNO4/5ZIqFTh/xuWtMljhM5w2g1h7naLQPvWL1jIoZx1cI2fprHEGlnYpbZXtGGjep/IhONgG52qxoeyGH13jfcq91pnUYW0YyDUE2NGJbCA7qTdCyF49r0pV1ZUQO/6oStbRHkrIJG/ocjgsd7G6d5UaM3S12ekWNfBiug0WGXX9J31/8xHO9OoHITJvyfNrsh+9MXupzvcdYAyc1c+xefB+QHglTqMXvRvrYbm7B+2rq7e1proLK7BUFXaJsbb+2K6QU/QdFmNdpq7RX4ckzsc47v3cYwBXRZvIw/PXCgeHws8VZgrWXxdnmY6d1a1MG47hCkrMpdwR4I6ERs+speSVsB+yOlyDH63Qsb2iK/2C+r23lIduhX7nIdzcnAQP1FE6+3TC+dQI2HnPefs9TOOhj9vbfKb3KNrmF0weMr7HU0RMpHDzuuFzRKW4+dPgXR2ZPFKjh6C9YwCvpg6rbqjvFw+V8Y3gWVJLKuSxL9xe27QZY5a+PTSvzwY+RZhuwjAUr8D8HtCjbhwbFdTPwEy1ZyHc9N3eaMfUupuO4jmp3kL/wux6Zop+yjSQLQxkoLj+Jso3//7mdm903UzHWt/t55hB6mOCS325NU5MnGE4h90pO/nOAkCOkR7z1uMTPTmcuvvQDlWhlOU8WQd4tKrGfpSyaFarkDlloVDLlgGFZUVwMhLkO4MwKr5vgnTvmGtDcJZgzhXlOXaqhurUD7k3P416lYZzqtnXgJRBv2lsmRzJeQ0RAFFA4S7KFTzpbbUuADZ+xzAbymja6SyPRiao3d4b19KAnpzzNxS4GAiSCEjVmbEGo3zjoIxq7PzSf2bz5ObjM/ru+Fgj/UiTRAEbApIJOhCDNZxggJRbOVOiRq2IPc8fts4sit4yeADoZ27XRWjACW82fzvd6POqmeK3uqcueSoL/L29Nq4QsWvoos1gyRoOUYUO9drYfy0CRWKR1CFGt77fwfY7tNVWbV/2L2Rr/K+rTtsrWW5NxmZP/jTzCSA6B5XHdv/Tqy/Qw5/8i68VoB6IzNT5sjfZmO9otS+rWNv1TwdLdsA/7bp4NtuPVZYmn/4D/f1sJfQKpF56SfnWzmPv5jZTp1MkzHimspuZMyoSQSD4W9p+Jrs9TM5vds5DRqdYzy9IL7GRHURWF+V6wFkjrXh2Dt/L/EPQiiKOR9ATVhMYPBLO3yp7iS48hS8RbeMfIlzrfOL0QVM8/uzeP3W/fHb8IiJjzt6sGWb6nzVSE3nm6SMQtV4EFSu5RgH++9OYfwyu6pu26fMj/ckfrU/yb2eKqr9HN64oxKcYsK0ifjK4wArW/ceX2W8CI81S+3FjXvUJymFJ2tto/jXRyLnntFwuU/s/fTKmeTASlgobM3nvN7cyWLheRae89vt+eTIoing9pCSm9q0wv4szH5f1VeKEAvDWIQpeN0y13Hemu61CA24+HW3XppLebXGVQYIIKXhFWfnBWdNFAuja9pFrUZSmUR3PlNwIdfv8qONuCXSrf3Qrp3zE4jqalWZqxhSamD2HLLIXUXHQv97LtkNsYM4gMhqh5fBCU9fuRGK2Hsno46ZpfGYw7aL4FJ1nR67cqsN6u/7JeXTslm6IOqzJVGpdJ2ESiDy2mPQm3zZR4hP8C5Mkfm3qJ8VD2lMcmI2UEzLEZLuvf139PM/r4WfsM56PPIE5hNrsn1eLKt5eAyuFgoRr5Oao5Ijr/bm43UlOSZnbd8yN4O5rZo/kCwJo/teqG6+1hTfLBvS+SlOe0jLIlkVnYhWxtz16W/lt31lS6QtyvvksuaECp8QGwdDh46b6dbkErPEYF8jVZu/yepygsnhlK7GKQLly7D8kTRUJ8p1NKbPBQacY3OpcS/pOX9WUKeUhH1Lk7pc6ekn36hCiRroBuPOg8+PbW5HKepuxK7nlRjDgC6fsdziFa3dIY4QpEyIP/DNl85SlBPRs3SXhEKNa8eqH2xU3pzAcHeS6g8OuiFVwvV9Dq9/mVjM2Yxqry4cSs+rQHzhqZ5+HmtHfGxiZV2BVIdkheCjdxNlcmbKTMRifkjDUiEq7p9E6KESgqGMAMuraqL+4A+6TPOEz3Aiu6gPPaUVdbXzHSG2IYlMV5XzuoZ54Fu84EpYW94dphlUo/c62AU4xrFMcpRZ1LPV8pVgoT0oM2+i1GJJnTgbi6Ynp+Jsm8noNmjxXGD7FJ6nsAWjre/WdGAyfJr5wAbtA+GtPo6RmWTDznz8F1+KEPPEopbnosxIOQDLP7V4y9W908c+oMEZo9FRM1I5+41jibp3VA40Lu06HxSnWGThN48d6uFshkqAnvPrmNzRNTcZFXxop1+oLRWS+eREs1fAZv2Wx4Gy0IjjssakGTtZ1y0qAQ9LMTKaW/p1VTI7wcIDxuFLaKXSX8bPzPHKGuVtOPqMIoHN6jkUCYeijOm9sQ2R0OmqQMBnj9Q7QAmpy8h14EYjUOhquGRSAXsbD4rrHtA1jaNVbgndnwDtm/Ib+MEkgmENVo5lFPYyrz473OcqOnz3+tpQXVjxWPHCHwqh4HDM1mrg9qV3ReXsWPH1CsPyW4A2qDgpW4lNri4fBo8Vk+ql22rGnl1x1GPYR3e/2pZUt9YmYg4xGgLAwWcD4obwQxQS/lgVvFnbD1x6xr8PAh7ANHplcGmsjNLv0wv4U42IKM2IAwU/zMJwjFMYUB5W+C2EWQLijwsjI6eoCq9PpAtPp+ny16JE8UKVU34vbY6qwWLfAlxSwqXmCc4mbiolTTiX6r6NLV57bEL1xWK1/o39z5uA09UGi/5xGHyT9n9P0KQPrpx6hGIXyBJAdSA6dOdMM7V3rIzQLAdeBjf47NUb7cVENmdBavib5UU36lHMP9G9PkDp2Nlr/xm0ywH+9NBzY2ib66y8RidgOa6//Hdz1CPMYAvgTJMLXXwa/SntRTX1qf6mpT+0ZNfWpfaA/4uCTqlN7pH2h8xOPC88B6mSBxJ475cHNe3EY7lBCnj56aGPt9Tc3ezZ+y4I8WWslrmmwsBsnPKSc6YePn7M1uzZI6VkvI5tsShePfA7riX+1bl0v/tCSf/XJsY/RU/8KHRJ+o9cQvoib81fw76PUlCFRG2w8PGlK3/CZv0B0KGLs8WZZYdeSw9k13Exsyf3Onal016q6+z2McmvPW417nr6drk6j0/T1XoE/JB1PNqoFTugq48oSRxMpE3fJrHc3ggFWNyPaFgim9ySE6Alz/41iDFr5IjESa7HovVc1M9092BLNSiw3jPuO+f1O0afd7Xevm6yZWiXDQgpGtATDgcXlQayZvm93l37WI+1iywFNgZQwZcIh5YoUTlqU85LvP3M9fH6eNxzMfXloVDhZq+tnVcWG7tn9pzbn4p/6nOmc+vQd0a6ehhsLBfRVHtRm4g8TIbUK8xjODvnDY9F1v+SVyvYk1zAwvYDxj7HaCRL1BVUXQjfzsom9LU+HkA4weNkbPiz4M1gPQeaqzQsLrMOPOkA8RevMysvU2J4ZwMaEc2Hp3LSu2wqVVYZO3w36RERYoFky7/Dp2dO371hRJaPpjSqyTk/OdN8Ba+4Wjt5xVQOEEu6ZuwIihY1T9AyaIfYccl3uAGiQpOd18E9BkI/5EcEgRWfwAnSdQg2JDciRhy9gHV0WYjiOgaurR3iUFELrYbUbpuIIUPGF2XGN3A7O2bB+r8ccC2dqdHgN/DQ3FGGGfLsUzqqmq4/6MII583wlAqSaYtZw3OJ8gFnE1mTB1Gf/NHWq4jkEc3Fmk7O+8Nx5ERms9IbkNEYBkA2jf//24udov3RF+n7/dpe0CEtTpLc0tq4BCyfCCr+Kw2MmsO/Rq4oa+9WaXz7GOAL9jW13zUOpFHXR4PByHi/6J9aYLmWIvbwPxSrj110QwqU5sYbArme/GT2vHnZ/xtJUbh71DNrirud1uhQgrpBSltkBRkzp4uGG0cB6VXJGwVapgXIH7oWY/CM9A2e8YDg22316aPaMS/v1ZJBAOoT5h1qEHbYBQ+jZGeNZUdOqgIiFBuPZXpQEzzvxoaQ3F2fdxfqo2L3NoyWIYHNLpkirm6/gcquV2MW3tMzr5ppLGagm4/Pnd9kekRK51MyL14HHjEaKoNYYmwy90KukX5g9hh+TQfqEWt87+zw//bC34AK2uu6xYTGXQOavdcvVkKNxY1agdWGxUYUSXKBfhseyCCzzr9J47qYWDOFA0T8tTAsiuM+2QaskIZODXaXraIoo6/kIctnr9fUDfxzg1jebNWWRyi5rftzYdMQ6ziNs405WqD7DOrqeQWUEhyHZRANqfQXjeesGrruxihJWSRjDkbtD9fSjiay3hQy0HmJlstFeWbCYoFCLNWRvY1qDNxkx/xI3EclQUO2ue4WFQEvc354HjtMYZ3ahEoloBcazt1HPJvx1X7ih907qD26uXvUXyaCKZpmHDNyPj7TxYXycjzuu0n1N25Csg5Y92ivGm06aGRrsxURuDndk4wPux5JzasSp1lQ5aEh/IwszYZhYfiGSVJdumJDylOU64HDcIg3Piv39nrVLfYlOq++Gl95tQvmr9goaK2Lj9m2G/F8bKXKbZeOxWgriCFNXhlnE2I0/ntdiEgEGperPOJxvppr0v0NFqenYuvpYz4uP9LUJ1/q2NRezP0EVXQV6jSCvyZSrW2quUWaNM7TbELJ5zpAVeL+R/wxhaR22rm87O7gWLKJNolgZGcjNOoyGEStMVWzpyg9cRmIoEhiyZItSI5kfxaIIWx/viZfz2wEQVyqQEo/koNxtvIOGTJXbMaeiAZcPzUFFCjuCFmbMdW1PPWzjq+LweRhMYVsf0tLCQGnkptYx8IAwgpUKLm+3iWSxUniRp6/C/kOx3v4FBA0od1cIRpBQcJzvehNbfAt4fBxxJ2OyRIVn4jXpT28rRiwqD6pyf6SNSrTrmXfwnlGGH3V5mADY4UpOQ7EjoQxgXy7H5Od1kHhd73L1D+ito3+gu9XQzrplyNyYFQc0QfovfKSeR1Lfc92fDrHfp0t835nIUSo2h5uBkNVLCtpoLUMcPRmVDbtXrZN12MGM33SFjRRtisNKcXgykIEffjSGzXVOEhNCGQjBktWq67Npn0ibEVsLLCFB8CX+ZZx7hxSaU9e75K3VKbJ0jQS2+kjGWtjt858j+WtT+KFUDmgDS+gmzfdz+T+X8XM5Phv5bOTzUfic62cU2pifyxMFTIIu0iyfNVb2z0QhVSB9fD0AFOMKqZWp5Npv6ekesSYFKKVFj6AumKoHtAvdGi/NlohO1hzrISivCody4gE/2v761TKgooPoKLjuUHXe37hz5NMy0nNOuK0aT05TjDHu2WbVwX6e87xmadC88kGBiVpKhhma6eMiPDCv5aPpcRtrYxf3nOJm7QF9HIQyy4eV8A7mD+FDgGO24Hm7kuFfqNlwQIzQaGF2fTgG4zEMtek0atROQ2+NoqGSs84zJtYNpQv7vfjmaGfVGEg1iIgOWn+3yhbXgB5ANIr9i2Yo7mGqJaId3I/Qa7GF/3lHewWbvB46JxyB+13Sycut6/qEJV21k00y/tdKw0KCYZuIipljmWmNt5AZHubv01sN4aXeR9yEW0elqDSd201CjEPyVhU+Ta9V+nYolMwUy89hmdH+rvLjmTs/PJUEjGZoHX8SnwkhEgiM1vOLrjdnXATuC9a6szYstBBF1pQubPz+Zo4AhWiQFrd1/ZtK/SNMjgqMLhAkB6CJIopkbgwRiFLxi/WTZVxC8oP7ZEh7pgpt1SG8sc82qwpLDhLLWIbwoQ1dYWJu9OQ1O9hgDX04VFDAlhpKyiMK0ravPQhUriFyJOFJC2hYu9BkIcXfjiVx/R+VBGe5K73deioy/fCBg4tzar/q2BDiYZpH30MXrVPlqWrVDN9C+z7K1C4SxpbSWp1uDF/r5DXOv4XrUtbZSIcU+7elY07Cdub1y2rDRyqfDIOZHp+PnpExguX98RCA6THJXtWSUeamr7Lr+Ul0mM0C0/PrkIE3824136ThJvrnvy4Di7bU/6JTzJ+eQGuV+6U6OZZX+tA//lZJmHTYj6K1DJ3w9bdIo1r1ibmtgCqR3/5WWTQ1FccQGWi9Kjr5BgX0KLlbLeSKxbVdyd+WRlgbexwLABVjtbpiPiNSslzwRBotcvNI2y839ExTcpOIyXr5ustZunz37YELvNbTOGHxdd/VTnqjioYTR7tj5tGKKv/pJHAesdpJXNVbRKDwm5/1lJEKPn/GZz3hv0q541sSUDZLglo9eJPLYQ9w0ZFbAzYdvqdEnTtrnxPyXv6y2iVaw2UqzDkz99ONq8Aplh4jZLd86rlYFa15yHQ3hPFT9OYrsGzmOyp4UXS7wOE9+ex3F2M+ODjgMfswKMaIQ7L32Bmj75ieWA1UCbjB8SEXt8RC3BVWyJJVKwMeDx8Sv3M4ATdXDgEOdqlcEegU9LRsU4JCzfSE3iVDLOrKVyL4FqgHBO2x9KbkmunjSwtEM0SfeD4oECpmDN/RLYdM8reJS3uiwBqLhAvlf5I5oKd+p3hmzMgDYlrw01w164RhMQ7ZfWO2znbtP6MedrVSpeWDb28VMeCWqtnp7QrfX0n5v55iimNfCP+OYLW6GlFLqW9yc1qx2K0Uk8AnOazabYISOsChVk86kvBF/92Y2Ww5bPCnaqxnB9ulcBWvzIzipC94s01+8y3vVG0erF5DT5uIt95QBjyZDXjN/94/tq5Z58p9VIHM2ukoLOkMJCQOC1AS37olNKCHrHxszwW+Xr6UQng44NMomd8n8GjUOLuJ6waL8rV3Mob8Fu8HTRz5bKi0WijwZN6EVZFR8SE5Tixc5dYCxLBrZgH/9DqWuFvLVGm+xxeaHQ9SE7NrL4B1s24iMp4IU4NArNPYmtWyKb3cngBrNXadax/ehqHXTsu8CygQ/zOFJAQqsXf2EvlXYBm0qH6LDIdP8s+hldQn8Xusyz+elPIlC3Bd3Ns73m2UpxPg6WoEtqwq27LstQ36GZkt+8+3WPZcS6emQBe6lNQ2SWSQJDaAK9WCycr82jA95uG0lw7hUYJxJrWsY5LbUByvaNhcUnWsQwZd9qCjN9/m2o7brvBAPpdMtXA+9IEY182Lk4ABZ6q6DbhFRBMg32tsndZWenTXlLN3cL2wUZM/lLNf867tl+kUSyTF36pJ3HcRalJDb6FmgSv43+Jdg5pOdagWA8QspxGpXe1yzjr1q3rZhIGo1LG4MlOKWqphX//GMagIlhmoJSIKmb6Br/lX1TiOxp0n+3MtW9MqrwNKvN38Rd0MqaYtp0Y2SllE6otA/aP7XKivEHb5lzpuKGbTVuSpdM6J0pKflGhemWFdEeWdEQ3T01x8j9+HJKCsLtZV3d33h8ThLC/LMb5Xa/EVxTEip+QEDw/Jua6iXetfwumhcQS9xVbTv4gsOV6M9EfwvUu9dI0G11jI20tgsstJCe2nXgzDK9ogx/vRrjvaIvcYFKuImWJ2kCPHTV5zRBTaPXLZHCHmTGBFgjLChokiRBFr0N/i00Uo2hGcn/hlcZY0wkhU8B4CEWFRg6Lp6FIgpmbQoMgKj2vwVMsbfJrVJMHbsGjwrYqABJ9qeXOkKyqTVcNjkmrUpFlGd2WB1h15WrXJW8MWEbdSmzzGmpavQYKkcn22YfRD/p0Hr4ZNXgbQ2Ezg73mQajuEZQDMsRl6Th7Iuo5qGZ7WkqoZeQjrO85leHsOxszKw1nXqS/Dp3W8akYM4WhxgD4V4ITqja6oHa3tZy52ka0PVErmic3SAMnaXkTusWRwMb1BuEVP49Ax4YpL+q6MLLXe6N8spbaBDA1qmQfUcumWfTeiU8YDpLyz5QBKi0/fmoPog0fA7CDBoVcJLiFI4y85mgQODkmsD2T4rL5lqLMvzLxLQwKcnfGoj57xbsyKTtyF5vKNwLd+RuN87l9zqNAi4eNG39gRomb2z+UHqyNdK9fIG0W8qY5pxa+cp2+LT5vDOk0NBq4FAoF8byclnMO4pqDa06j1ZubxHi9QOkQRvqTuJPxRD4KIoa+glO+XFq4JhdgIYqwcncfy4nyovZRggcdpfQkvB9AO4DeVv9dNhAotMntTLpautVeKieTnVZei9HI/A294c8TSaV6Yf+fRlcxMTHHXpPq1V/TY/XQJwU38SgaYsPI4bSsWRX1r41HwlZfSCYcsVh+qFKwGGP4aMnIQvX+3mgplGpBWqa3OZ+5he8w8owt3iYCb0NXI53CSRdkX+xBufzf0bJm2Hn3oRk9a8Wr4eZUSq/ESRPe0VS3/0J2E21oQvGwOlXm8aTDKlp5vko8zX6379aTEo1au907DgsGjnxdYQxNFZCCfLYKNtPTViaY9Oah2/bcqCazmi0NTeOI0C+sB0R2JQMQEirtSX/zRPMVQbem1T0zRd8WubzwDWwB7XYpefaJhtr9sJvbxD1Us3X26HVmGgEipdsVlLOVwyQX4XpNfwDleLNueWQFtSvpUgegnZA6LvjB9xj8VZxOke70GXO440aYWEXS/buXXDZLbJPoRvUidF619PwXdmk9t1v1/GGisDAQaw39U27goXInMKkYqAcSZnAkqIcm4sfbpAM5obfUN7KA6XlYRx2HVCaKPh+mtzx3Kbfcg4IeFnUf/TVSRPlU9VT9XWsDmuAYVLDQn0iIA7CLyP6/89z7uEMgMUdvkwnVMDUWB6hG8o+Vx8f+lMJZqbvvoz2y94m6Hd6t5VOGOr1Q4uPB6BwEBfyQUXcEVO41hE1RC4X/AH38Q427A8Enpdo6zYBckxNL54e7zn57uUwmjsWQiN4MTc5+OLIi7NMrHJsnulnSMlWZsmmXKEt2nI3nbim33xJIv3tFR+0fNqaFkDVI+vi12IO1yE0Lub8E7xyRcPXK4Yd12ihjxKFUA8nz1y3nPPn4xWDuuVoTbY86U12wvsPF+FkSUYDbO6oNwQR9dy1BP7uCBuGMn1zhZkMPcqhtVIjhj9FKPh+/xRo+odU4zYsV4tKwp6R1jY8N0wcnHfmtli2i7vU+7I5yxOWvOoh09d8aqH9Du4aYNMnklxhIb4fU7h4y8c62kp4SKWSdvrUG/OiLRHKK6Ir3xHYoYwTJTMa/i1tsBstWbpE/eRVSf0ts4Hk8pQYcysbvZkKD2UcEHrF3k8MRKr4JzdMAPph6dlu95IR2U+aIAC/+9zK3c6/5DN/jg9GmBFTFUX8c141Ubnh5G76RjY25HTsXttL9uVw8RobJbK5uq9gFk5NI5Sl6K+0CfLE443Zsb3lwk81Zzp9fjKW2Aa1R7k4wP8OkKRgHgzErkS05WlPMkUVNi2qbWhHjSj+BIFZfFC7VIQ7MszxRmKgG5eq8AFJEZbJAep+KmyceL22nDP2oBwH4wuxH7c9P1o2RF1Oz1tQFLUG09FbJ/8/AIGO8hC3Oy0vN4fujKjZFdjHDh8jjClma9UiNdDGZO4UXJvHqUQHzSi0TzdXN0SLOe/z8Xt2u1CjWRRdJPkq8YeqhUYHsBStHghoIRV6lUsKTEjq8NzQQRyj/BiUEmSQzigAawqNULqKIg0gUkDARrA1VurJva46tkmeTf0CV0NeBuNFa1kB+j8mOXe+718JHTrvRNtvDKOdUwrhJHWffclMCuS1bL3Sjcvjfie466fdzLjCrGunDlWoWZOHY117j65CMU78xX/AInFyzvD+xexVJhjgrJDZhfOoLJLk8rkN2uiXTiS/NVbtZqRPMx5Iyr1caWcS3KqiknY8yznvN4DDZEo1Ma30oejpcD6bJkDc+8trF2FmNj2kK+W9gEziXRmkDhbqTuRfk2ygOFYJV8IJC4mUeOiJ9f8EjkVnzgrrkGqQyCAd8Ukbo/G2o+CZtosxZaSlUt7ytyoMFL/bBju685+z2XXDgu1GBT3gYSMG0VsoEAOqTzbK1ayKMIMj60c3nskmXtsmGes9aKYfscmi5FftN5JI2W0xDBitvERVC7jlKRJofmZSXt1k062UcBw/67/CT1zEfY+Nd2z+F8aIkSpZjQ37A6ERvXX90raCJyrjhUerwQiaqNla69bgFaPl25bA9bpWX2PoFThQdIxCXloBDaoQnYteiafE6zCuNemQ7FIdwyRDn2fReJdbPCo47AOebu04VAl6GoAyuzrvOZFDTD2BSfQstSe6WjVeiCqF3e6kivmqp8QRYbdWoEMF3QI6FkDyi3fE+68IjWLMAN1/63v725VEnavKVPXLOamVWTU3AlZQdYoqyp5HQnLBsDmCHQdUekVmGLF7SLVTc4bpQ8hD3onHBHSb3AlFDSDS5G8cgoZkgpphQwiZWiFEwAhRY45qv3sl9PEQTMmUaro2huJAR6fNII4FuYYdII5JCDbRHKbvNibUqzaon57WuE2jXYejFry/MDcxMdj9fYeMuTGWrp9lA5i5uJ3Ofbhr0k07btvpriyeXQeIp12UvJTKZRGliKHkGitViaU3F74XH1NoMu9BBaSqiLlEjFUbbBhFpqbMyEKyPmQk033GiASknplCbxY9pzKWPvx2UbaJvyB+N/4qtJOefa5K+6ztqIE41iS+MQpffDaXQs8YYWpnXUA3KD0yhbecjGH8vlWLnVzfmGavd9W9Pc+j+atlzLmXosDCFNPUPMBpKLUp7ZTm7xAr3UTRvyKwzXhC3Xv2t1zUvX4awjS1/nYrGK0joZxSlWsn1GH0rpZUQ70Kn+ibWKmI8z1sF8QjuUKgb+ck4vQeMN4OYHtrGS9VSn4fFVptpxgmjBxtJbCNi6V7wtiwOUUTej02G9joQdc0ymD6dVjpfHuyTOnmrJsVzObTsAzLd/Und1H2H3dG4bnVpu7PMu71aCs03uNagUe5f3Sob7NfjIcRrW0zmuh0rFi218xqaeO+d1d3Y8XyF7vN0fL+MedBpjpmlVSlqxgjQ+WgVLtXSrkVy0Jfd7xM5do74np1vYIDyc40HU0aYVKbiV4Dp3+Ip3ZKD/AmfoYw44Vd9I+/OloIamZQBElT6Mk7Fc8Ll7a4MAT45nEMk9tNQkvxcF9pi7PYxB0qpkjq/7Ubs2EhBwLS0wrk0U32Eks2HPwowl1AGogAtwMmkp+nLKbezyodSX2JwgZzPc7aKo5tRL71anBG0L7G+9usnx1FXBO6skOruflGcx9O+clL7GfKDy5ex5Uv3Rbb/QvxgZLfdEVw0MkF/aw+joYxNw61JOcED3oG+mfBe8AZiA27OaSUqkHjbTTrh27+KV2s0t1COnB8wJoSsEs1j4pTxD02slnaXM1qGtZ4xNeZ31Inhsq1S0tdE+8LLFLiAXVJqzV4ixqbobl0PQzuTzeH8A1JeURp0p5/G5oHrnApExtn9JTxw+COZLXdO69LGdtzeYlrxR84AilBkfZrwfnyiV0/iZJlCCgrtukkCxpJ/Tx1h/YY4Gs5zkk/wY73kBj+w19mOKPW+JAg4fgb0IExG8oqLcgUXquM5cJrZUEkhBxskvjosjGxXvF2/AE0VLLgWODMcBsvq11XIh2vOpztCepTpDdZb2Y1VdiNX5tGdYnaU9Q3uW1ceqvRDr81mdYX2W1RlWZ1mfbbWwAlCnUREiROdKKfN7pAaz9ry2pElUlbxT13yUFf0x1dHRKR/L8SiWBXfxckWWTGol4Zs950w/pyEYVSvaZNjbNI4OY8aPUeQCBbYVVsgOJIfaM9gwFbXiTH2u2HisTAVXCu8bz72cZW4QZyGxIZXOALWdQ9wrLd2u/WczT7PE5NNKF+JvhRh8hVFn8MPUfKRLTYtuqUp3MKHMJzJVU5MbUjkaxM+Fb5pPdKppkzjXBeAimOp8nI9NKR2fM9Ay3c3nzg8S0erGm8SJN1lJ9F5n+viY1IYIBZ0Xk02i4R61pEuz2CtwH8EHEt/RLHjf/N3x/cxzYB8tj97zU4L8zHW/f+WpBtNyfDIVOEgULZLX+lYCrEwR+GKOJg5IldI7AYYNUFssguUrwcBxbG/9IT0zRz6sZzE6GQxMcNKP9thAZeWqQuHp1uLnQsMaEFOptKhz0v5h9Mlf8bD+EL147JY02Be40umOpVEB7f3WvaX1EwbOL9tHUYHxMg8TZr8gn4Gr2mdHifWcj3gSZkI9WMqfxpqVxhpaGeMszyCcj6C3ttXJJKWsqLp5q0AHiAMkkrQYZJ9jpITU1MIrzKgRme0TGziKxw4jENeL32wuTJfjThDBOnWROFaHypbCPK5+ycyiCC/LgIoZUqQwpSJRNgsxl4HTcQkPUQ4HqSCY7vr5YY2ThynzMUI5KhBt85mzeSIf+AsQGRWt0eigSBZ24PqkQe13XTlqoAHIPHQYHjgkI2uKMbou1w9AN+xsz4hntFXMjqzY3ynWQEcsuqoQ2NDEEngkchKXzSEUYD+ELTPucTtdggqIsix0V0nOyQs521/QWHWJcFbSovcuJHTZBzsklJcQlNnZmo8JPYkCZPFcPIVImTWUkwqaU0mZzdkKzXIeVynYZQgBHQoLASzhX1A6ICMUw5DqfGGyFPWCNjac+YbJYcBzl5QIMn2fp+Zuoo2pH9g7199M+MVq+BaHhj8yQ74lmr361cRKWxpbBd15KZEyWl+SAZuMPacONJ5wddxovqnQ8zC4+H4nsHaIcHMtZ5U1Ze4y07n6kXdqRdekv4Am9GHcc31JP09QgfcnDvQbdF5WxtMkk1SIy2VTQvFb4Lhjv/GI0bxVzAIbO7E0Jq/UGI65/++QCz6JPIDxRhzlhpGNjdf4Tb7OERsuEcBoU3by0SiMR1tPoRzOHrjYrJ16jeCcL62/jAVoxCNiHMugjkfJBvnRcUShbFSjxq+YzoD7zLJHKi4d6fbZEoo9S+kDTb8bcJgM4+xOfhuoDD7VK4+DvRmPFwy4HqxRB+Z/9jjAORmx2YU5lG+ka4QUOBKgPPn22n6WbS9lcM/WxuRrGIOHoRFX0K3YR6/i49jSQxsbfRyYIZfr7EqJUcpsXLgwaBt25e1O+TQvsTiB0HiZ0mYuG+Jf/QbiB/L631wxqi+/qcmBqclvVfM1MZ2fqvamzHQh1bTcr8R0foaEWKROF9R8J/Y/GezzTMJzoTqdnnRoRwfrLFjffkPaR0AZYgxNxZvpkftS8IbJ0G+e0/4mlXAOgxAurg+cKLrLb5ZyjAhqBGIHz26zteKdcpM/qouG0jVSoFOHEZYE9pLiFoHj1xRTNOM9tYBW8BsgoI8Wf94zx96Jv9ptbuI6HB/akjgjPC613ZaLJKqkPHGeyHEnuEsBhR7/1pEHoU3p2n5jVB0Tl7zyKenv+9CV+cfhDKIoBeVdkUzrbuESo1FIFLCSoZXlN3HvCY3bytv+veNxBsj/pajldCivPAQnqq/d7FG6UwagPQ9xcdqFRg7oVwpAYH13YxyfNIhLU61y+cbalJ3TxeB0J69ljNxiQHU8sgOnW4JSMeZwSg0JJTW5uRmjK2RQFuDI4ZYMCiPDrliXevJrt0RtmP6rOfv9q5LKUpMNsXGODH5rBmWPJkjpaW6gCFXh+VMKmAQDF6iWFT729Hd5cH5nCCjlE4Hd6f8LcYa/mzIM4T4m2ixq4GVHMBTzO9RJuNcyM90EkCAPjIonqoYTTGkMnCVn9pM2ililUegj8nmem/I47XQHpPgo5RdJ9sThYd7jmEp46mre3KhghzI3oT2Hfx5SOxtvwnIceAQLUSxJucCVgOTH2TwU5QqyfLAtusrR0EpPyecPYRSmW8OwPFVKtdJAZ1CpL4IuOJQwyRBhMOBkBbDzTzGIbCWuhHQQIJFK8eLTfQIVQk/c+F8t5HZx0bpuIhbgXYOxU2ZNxc5gQzXhRAHIyVRQTiqjENA6cIvpmem92ALMK5AYEGkoCa1DVo48hXXHax89Lm3pb4IS1xQjYTO4+eBFfvuc1eag7UPNjg4drwAUjIHjsY1hRPEsJhEyGxSl4riChnWx33rcnhfKAu9AYPU/VFD9VU/le5f1yjsVQigkKJDRFu3TO8JUjuTY+t564o2JJxRHy0hc6BSKMFVFS6JbLqmW/YFeut5NdJl5P4NXVmhiT8PvFSrNGHMCtRlhjGLthaBxPMf5KWFm3LidoWmtn3T1GWce9lM+2t96/2IcFzgP+ZUzdo0i6nxYWrmAqDkLZJnh29iE7M4rQaA+Qic15f58IBppoloi5DbPJosiM6zrYONeeUD2JNeocKAIm5GNR44yHgrhdnE6T7htIDzC1rrfde6IdvD/B66xSmTRSkT+N4jXGDlCSROUkqFPxLXcTcKW+fM9J135Vw2Yn5wn4l/E/nxo/7CJvOsEpmyPSQ/7otrUYev1qnyyQu8yV9kWtchPrs5wskUeSyjSfheyu8eEIw74zazvF4gUo6QNEX+DUkAOycoB9FbBMhSF88vQn+uNSVVmZdoBMgcaFC5a8SD416L/+sSkXh8+fTQ2kE2SQHCOZYI+QUwpwWrovXbMPqf3G6LNMXRjcOa4YrF+F3xmdKjyM4RF9RdfExtxi7BcHif6CL1hwoX4PXDbsLual/8uX72YL58UorBe8BZZM57VxVdp+8lA128OezIR7wncECYcFgT87diKYHwN/7MbNMaeEKPjBsK58ctHt4KjFUsxMyOszOTgZ+YnXAfvHnDY9NpB/T0MO7FFNr5WfuFtY22w7FK/IEBQ3qUSYh2hQAiUxlgY2G1IXlP2MK0mUy96q8AcIagnSPN0u0vgWMapuWIS6gjD4kFxdiRRMwEkQo/vGKWN66bDZ1USZFgqGu2hIXXCscVkpA42gETJZDFwZGcyR1UK+NddBnz+2dEgPjfOL5NpVUcpnXenyOUJwyvdVW56/c7p9h9fswrM1jxAN8CPI+XGTTXaes1jDw3LGYirVT5iXBehcFqVcbNoBJAmNd44Zc+i7hQRmCnU6APAY5dV+ggmAPG5gc8tnbJbn6pKbciE6KgGL98LhGQ5IloPMMrgZaIsLgRq2/Zts8rfc5f5Y6LpsnLmqow6SQmMlh4AcKlB2Skcju6cAsVb9XxRuJZ4xwZ9NhkDHjDTsDAQVViBSXimJwByHAI43T5vySgw423XAdg7AaWaAmDkoJRT5MWsVopvAazCAYIIxiHHOvozgHmsyEsMVoSmmZhtNYktvdoXAhbXznY4uE9KQBj1rJNvczOIPIWEKYCKUei7LnRChfA9TylsD9bKRfAgh/N81sTZt7SbbLYK8RGY4hZ6ybS177fEbe+B5EQntZERzKXcFVfkDYhhm0PC5VVgITRUGkgDtSgu7IM9Eo0jjRhvRkuBa+YS+m7Q3jHtRJn/Dj5pGbgV+xktEJkcWkNn0gGvoFOcmwNV2ZbygRLLJoXLAAjohJpAHg4aWBc6bjWnqf39X8hU5wn1su8WFqs+LDY+Za3cX8TZEUYubTZpk3HK+fmwx/7Bc/ZTy6q/IJTj/5isPK/97my0IoHjNaUAbRIWpJC53zDMf3mCbbnzG2fIDsPPMk1OdUnr/l85t8H6JgNGGJ5NJnNA0Rfddek4zCrT1Sl7PFigAL2Q2TVtrj8QqGtrRPODAtpjeVg4837jS4wSPEtnD3D88USLPB376Q+CY03Crm2+Hd+D/kl3Z3krHaY93IyZtQ8vab2jkRvUdubVnGVtBwAlW/jbPKBpdwGwfISnjK4l+3wCM6jXjCpDkg8fAKiLxD13fqHeRumPoqZD6JX/oYFHfnhwZknQKYXQSpBj4eLrDhM+MH1FSN+Y+/UpRxM+BN63t/ghIGshwIHnpv+DyOR45xcAzvxBU3cHkw0IzUFrXdYo5Kq/DERKyw9hXmZZGmAkVh2gCRNYowlBBYsRGXXESLh8d9IuXEoESUwcRoScEYbJEvd8X87WTuEogT6CjDm+x/hpLKzzv1odviCP3Aa0ebBGDgiFFQEpz4rjH1+mBACHjuFiqcuGII1+WVF11eiGLinWIZcHgsdAX8X/fQ+gG3ffPnLgC1yTvQTUdJVMoCqA2ZF1bqNMPBtoHFW85y0YkyWIE+qV4sX7cVB7/ThiUbPK8LrN1tjox+KTXmCTHmTFoO4RRpLjVoltPNBz3VOoDhzHEQP6DrHopxOJTP3WJG0OeVXniBcKsPovO0T2pKKDImCF8qEqfDxjACoZsuwSBhO+cs/EQjLwgoyLQ5od1bFT13yhPfFnUUaFIEJhccMPyECkuyT1Le9pMXz15UaL/L8OeS5h4JP7KQL/HzCjsQXpQizQUsDKPOqjduj2uWeXh9weNPNTQQEIdg0eq4GHr9Ke7Kzgew5gNv7oIgsQVoiBStRGGyOdXlRWRwPRl+AYPZfQhz3g5cplWgOf8w2I6GngcH3wo+aIXC4U3t+eYv+6mQeHyPTby4bnSbKvH0XQMkEx60m5sLhvxt3OQcbYc8Fp4ug2PPNvD3kFsMxxKIof4fzonoNN+nVZC/AfgIRneYHcvTJoweH0RZfdvH7JcftKDNkbBKq8YMdCWX2RQz6T75gHRrjDuXJhn1VedyF69EtAtuuqAlqjLDDm0wdOLNe6PgbC9x74dcGUu8F9j1yvUoWDNGHxRLcnli547Qrx0Qxo28STQ31g7O6qoWiPNftJDIS2tO5HE8GWx57Fy/iNy2EDE75daE0btoc1vbmYr8eDh7fMlooN7jwtE4Ti+B+xFkvrwzHIyAGRCMgO31B5AQ+ticUWiN8hlo555CqVrWI8hn22ck8isY9G10dKto+yD8ZCetz4XAlFerxYsGbsUqO+0ePCciyX70NGL+HrGCvUkF2p6ctYth9HvkuTBr/VPrgjYlSdjQrjYVE1QQSmzSXG1C7Idbueff4J7vWHg4D3m/i3XRVo4Aov+2AjVpbd30JSw3gbC+b2we3ki/falMca8Vd0T3ivoximRO9FIqJNvLIDViiXEN+sLCI0TkJ+d+oOA1cs4RhooAjLSFaEgN21Ro2CIcrrWMNQqK2uccnt4+pXQoG+5g4udfzCDDd+CYC6p5L2Zm5LOL2g8UFP8F7OkbwUkZf5BGFwnVE+CfU7bv0YYF1+3sFp2ikBPuIhBgWI3w7YXbxsgGlaAu2xhmPIV3T3NSYenrmckwxgMjuAazHYzd8mYR6Cs8/pxwY3yn6mWnpc4izIFB9UrStc5JSQEJ5OhLIpwgxCUuUvkFVhbc9ARtLFiU8oUFk24aQG7yg7FHzod+xfZC+9azg59io0JIAtHF9r3SKi/G6O3HO/h2K52r1MZYki8jpN7rq2SP4ueBA5HoCOoax2dn0M5bRBVmruLj5tXUsQ4ObtQmR2P9wmLRfA0bbcO9jcW3YfailDaigbHW1H+Z72KilDEFGhpoX21C6HMo08/KU/1i8Y5pD3B1uLQKJsJUIOIcyQJ7BeWfk8wxRmMiWZsZ1PCINTLJ6wMjZy6SwxINK85TYWbHBqEuw2JQfgNSxWykKWTBKDu+29hNmd12AuZ9WewPPvobwzIhyWtX+AcoOZd0URUiBmvYMbWRTk9o7vo9+qEEtLZweGTqGq1F0sU/Fns0wBI3NfMe7xjxEVlXQfpitt1Jvv0W/wEytCwJ0inBdHsQz80Ow9ODVSCNzPM1x1iseuyJm6YRr4vgVXWEerWQfPXjI8A3uMlu9GFH5fxWCdLlmgJpd0FvDlJCfqUUN8Ib3UQN1jiPhSei06sL3c/VgYj6mla7ZiNrneBZUo6FUi3xUoD8+pZP46FHxehOPLZ22XiNQLmS19H55lmjcu1F/W9+RWqL9GOXpHSwnsbjJrHHPuQZnIzGvnKNDrn2OMZyn1c0Tpez5AluC/PL6sKJ60+yPzSjnKmMTKLLc5dgXw3JO47TPY5udkVpqHklLKi/CtS1EilKiC3t2Q2a2gVGJRK1/wSp0te3OsUG42IcD5Bx41NUK4/A8o3iHxjMT/kHCHhDMS/hPBa8EvsXd1/IBxg1wmzG33tkxc9iyQQGesfVLdznM/jqiqMfZBEVkFGv6bnypyJynCTIAc+gXvmDp/+HhQzmwuL3S2wOmMOx8LJr7/iKUCedO7RPqCeXRuGZwxltDnjgI4hmcK3RLg6qg51fT48DnOmNq1/QIcleeh1HDZ4UzTWSnrhMR3uHOJfy2mzn7MJK2DapCpzIqKjndVClFh45bWss7PEFrKmqomgPZB9u18HOneBlrtiSf+lSNgiI6BvKdEJjwLL4JsAIGHkdKEwgKlIqj2WSTTCGRDr/4JoKRbRJVZjUZO55MFyma3pB7yE6okABgTBD+iIhtl/sNMbkgm6GhmhrPT5KCIE0xH3JmnHVhdRSciTrqcrPLb6PTu5/lahw7kI28vz7U7v0pMQf89JfUMmKq9c2Hh5wv3qmTrFw/mzSdWHgHpTItYygyr2fLzHaDSvgkI/r5ir8BKgxLYB/h+3WltjZVEzlCe0LxLsYVKtCdPJ0jDMw8+2N7H/7mIAwbCMn+m84DrxCpFk4ZttoSdGmx07nnjAIMoZ+bKYl9AWO8ZS4bhti5BG4COLc8rpLwCFxbwXYqTdbhD1vF7lohT97063/vp+r1FVEDTRhWDQB3XWHVboyUgxlLZjCj/5GZEgzlABkfiNigmGbZs3jhzOn6wgoixmpXvZN3ZnT82P/b0J3PCOgcQUvegMeLVlDTVz5YaydMwytEhAsEsLtQ6lyH10oe/eqj6Dya8OOwASCvDFGxSo9dO2h13dUFDzxzkQT4fx7GFkS9851NH5tpQUTAJKxZBn2BNNpV5tmGouYTaSHWgAobKhjVueS9x0vLsNlELfi8cNtDr5EXXWUUINrm784du0ny2VOceXFvUiOCApL9/UxI7CwuECiKmME5Zhqndl5qJQjGlRVnXnQD2lMXC7ayrS0mnERUtN/L1Pvd1gjqNmnoJyM62PYbNUymjf3bd7Tq2b5EBgZXmmO8Bt5tHEtQB2R/CClcS/4opg8jSgtQtnOI1W5GvOM4iEXWhhXT4jRgG3xU5OB4FqERGln6NF+aEOyXuWUVdMHHaTLnpWOx7C+fjCgM6FYkcN71k6I7RUqyacnVhmDYjlt7FOv/yUgis+REMQQHPi2/zSjjz4t2ogrs5OsRRiTEb6K6GtDzkbQhidbSNgtg04TtVHnVrTJRl857IjttAwh8MLEgJfAxat09EI5x/hHolyFVmKrgpmAib8FtLdbS6RPhenjMVQRy1FG4mwtQny0CFJmRjO8C09pP7M57264IG+aPh8qjqgRlZ10YJfFaOdfSLOdlPb+LAGz+PgOI36vzh3LUDTjn5J7lNEj6xG2YTScVTo0Prd7yJ4J3Fb9lvjdCtUOuv6JujgcMigxl9E7H9xoYqoymNPOkvpyVkjSnrog2DjmojEuOjSpoFIlNXd49zrQaMAwcmoVbVg5S5e1/7Ke5w4Tfbmgfys3kQ1t1M85IO3DE25jauxYqwIONs71sqnsxYtBLt9rPMZPvP+2DR29FKUh3Nxv3riow4u3Qk/H3fj89/mu87DnwUEmyLaWzBmHSLddR/4LCuG2LLm7TlAkXnfp0zjiJGu/oHrZcPR/XIgjnCxI25Bx4dg6eBSNA47BimBWfxCQdjz7MhAv281MPFj3TlL44rUkgCTcEsYdQqQfTt/7xtZ2Dg3qCQ67exJamz69tKdmvycv6F9awyNQquuh4d01XGVuMd0z4rOqZ1e2WFoZWg0QNeXBjUwLgWEifGWbYeM6PDKPZAYNgqXZTCBW2VLwqu3Gs9BsMHrVHuejZJlVmNg+Fu11vRmfL5g3CaCnQ7rEvpQZ1l3q08vCsPrg1EuQLgty21ARUC+MUqIGKw3kr5KstJOH+gEDwT+33W2g6fYww33DKB6eXce9KfLHj/zoXr+mCp6Guc2bNRxoPJs0mYdbFU5YAE1iJOb8GrX5q8Yi8x0w/qxD+RXqZayxUyjqUtzrJmd4jQoawMTDpcAMmcxjvlIol6Q3hxgogEZQHzHuJQtDFO0S7juOoDhZd4P14MGlShPMzlRxGW7iyk3WTJtE4NJ7sbeh7qXXzvTRx6XCp86wwluTxR2vUbPx2e4Yrf2s7q0wqmtAIhr6kdTluVeVhy7XfQ62eu0TznOAS6h84Vci7xhTrhZw7B5ewQ27j7Nx76D13Cz755fDrTAOq8lSSSPmmJdO5v0vBFiIj3OYNVkElIoDc7yf2KWT/oGzlH8WY3pQY4SxrDdem+F5FBg1OjnQp5vIczkDXYK1TcqVFYtO8ytGKA/XhcCXMQtl5ItVGM5k5Y11FlihhA07tWnq5auqIxRVAsnWOSlyW3oWk4u+c2f3HlK+XYzc8g/Lw2Mh2f4CE1kkXqTTdaZnpaQePuo1TzFMKXg6mqpdKjsDFdn6lYsTQUQ0F3V31f42fixPXIJlfXRb5SFU8WvsOV8aWawiXmr5PxAWSTp/8aLtbShM9DNLgAYkcpxZYm/aUN18xVCcVw08JKJ2tNcdXTBllPvmGiqQuTnS1VNoMzJDtalf5ASEsVN9E3W2Rdylw1QtuJjHxKP3kXwqD79JmxI/X0FGzAOmIjUkcUiaXM7LeuLO03NdROBC35N+p74eAWebVgEIBErnsEUnYy671JWziHNajFU1d1rFtCKSZy1e8xrHIlZLGJzEPCiA0lK+jaE8G2jomUAz8cfryPGuTznuJFIoM5hScxGAb5ucIuM5ck21lOZaGDKTxqafQQQeF57bPIU70bMJ1f7w3BW/I0frk5jPHN3b9Pf8kWMUp5ToggeA/aSf8IyqgBXL8sxLNRwGuqcc9JVAYjMtlN4Etamq9fqHcvldeEO/OSZBaXOUxYgQPoBzhglBbtrAJCKt29S6P4sIHmORrR9c0cbhVgAQ7GbIimQpBAopiPPpENU6fTi0sVJMz+Mysz0ngCFhTj7MMg8tcuPAwZmt817fE/j+rdK7cqXf8TH3BA330cieYXC0BHQH0UM3ijXqAQhc81pSmbXkyWaYHqFjy3cCmwJm24KQo6EpIbCxUArX58mICJBNvBmKxzaEBNZaAz5lmTYiLchv9qtMdF1hZCO55Nbrv3S7ojWH0CKDt5Uls0bp5zBSeSGHMAr18S0Bq7UPCx2H6O+WAZSnDH/Q0zTbqLqlt5tj4WWfTmI1v0cyDbmjcYGj3jNc+Ns1/qdRVtBgTo84pdi3ZJYTuX5PY4AbklJjocNnMTae5ME487PDA6N3zh1WK2jOVyxzRzVaGS1YtyWjIvpaQdugzRLURoZvckyhuI4eNUhq00t48iXXYxYwugqSAnOdB7YtL3JkCmKyV4xgjqoIPuLna72gTFMVNY3AOGZpuR3K5fhs89GEfr18ByD6ruy9DoZD7ukYtqpFGLQhvFhB4S4Pssx2ADXpVDSzd0OeO3Jzk4LT/LjmmPgjBngR7LIIiGwAVvme5mxLR2MJSHeWqTV1q1p0pnSBUSzXdpmalX4bidYDw59wzH+nPDf01F08bx0NVUvmQMyMGVzbZpm4DoYzfkKCHE+B19GcdbKlxcUFSjhnT2CcQiNtYN0ReRNfMTnFEdh+eATfKoxgqFI5VVqNd/Pk4zYCJVAjBdakSMdH9EXz/5OvpTLMmp7m0xnGWi5hDrTqPFVc63AjopqZnLfqINFVF5cGoNZR4fQRxUejZWV9b3yJXP+sM7uElZT4H0KBPPLGDFEawzxhzr20Kzmp+5tvwaUcJaQCCpRKvMVjvEOw0lO8kFt8K4YHEjnKzoIpF2UvPfzfkrLcNI5BbPFeOiOSsaF5RIA7D6Hn8uh1K9BOU1VtaKXZE1auSyoDy65stcu4QtG7msEIOQhbVbfrL2yodEE9ONrFdgER1JjrAzVXhtd6CgKzBadOc84LRclTjLp8ZEUDdjeT0m5QorzBRPCj5ESvGyRexXGhbQpM6PHw18BNn2n3X7HgpsVNldC7vEdbwvRBtpGnC1beg3Za13n0Em3xaRM7WvvNXnsV3bd32mj5r3sV/NkTS3b7FxVI/3/3JTFJMmnZQQFE26Genq6MSaR43qqY4kdkj3RSN7NC2LrAoF8EFfelVRaM9KNWWxHG3CUVWkFCZqFOEcro0rUEvSD2EJBflo324hE50GNyFuh1Os7PGSnnsekEsZcvePkKLbN7Fug5xWMxBb5CnS6LpJSjyyRJC8w9+5BPurOVWmIENsvme7og36pJgAl6M2mbrjP162hqXzHxP22itOKEIPJFQS0aEio7d4jW4recbF5BnP4U5QPgYga4umlWRO8ePRGYAmeLrGzxbdLGcFNwZ5xPBVDfnxeS4gEbtRyu1PeM7f5wEYLJtrEiHGPrU0u7h17Uy4iKfotSo8ska6B45eessTU5+aIGzMLVqriwrU6WPYVNN2/bxeNICAPaRNYDBVnBVhp09EJH6i+mOY8sT4+wbyNcZ05JriHDIbJdxgjaKNSMucOccUkc2wIheHWylE8BVLEnM0tGs21fiJNfITDqASnEvBxlQUXhQtSsEmGECpuRGSBo+QeFBnoogUJNPy44QVS/B9u3uiCWW9sbzQzdiRSIuQK79KtZFWhAXqO/q9q2E39AbPPFrqmhAxzBJ4NL2vko8QRdEep42q5haKQya/ffCzaAUQ6SNzuwYwi9Hm+AAVwJlBPUuPPA+jXZwfGTqSFrI1sZIA+LrvK8CFHs77OSQNe6e5+cY+BJzydX/ddQT5cgwpU8vPaVtD5MdZ29TXCD3CV8rPfYo4hl0cMnWLbLQ6VK2XF+P3jpNYyNSJwg6LWkCuPYm85XDgEnmLjZeJqWCDwqtktyFw7ME02ihqM3hR1xLwLtisyC6xUo4YDKAx10t2Tdrjds5Cuwh60AYiQZtQOxJFKQW3qJV01IKL/UYyiZCUYcajixGWp0KlGZYT/c5d2xBpwcNMUroaoE7EvDAb6aePcSuBSHzm8+JQQSHgu/NsRl6IftZdejU/8DyderRtkiSn59Pznskv7B/NFH1dOKdOK9QFcA+7z99pGOHAUkFH5L6/aBhZ7Dd3ujmIm1u72V9s0Ond/UYKeycimRE+mZiIHJvaippwMyY1LcIS7Ie9bjSCJTxn6fE+Nz0b6rWYiNAU5AoAw3D+VDxyjwxNk1j8vKzArId1QFtZpNPs6CY7PQAjJRf2XzfinWG6LpI075WsHrH0kK6g25os10FSM4U43O7SNATMlhZLNnXw5Q9e6R3cDNsrQy6G3Wn7MAgZaGuHkKtCnNny0OJRcSbKh2bsHxhdVnp93aEIGHL9E+dKFNg2Ing0jPAdhyvSdzKuig+hHunEGufyVCCAp4jXBKCwItzgT2D4TUnoCx7IMI6MDErwkBF5kqSBGo9ZabpA4CiBIohTGTgdcnCh2mNrnBzmEu09KS/eCgyshTy41L+Ti2XzVg85RAgjwZzO7P8LOK0QZnISidi68OgAA/vivTBWC4SbCLXPTZjBlLmyIB0QcWA64wx6pSF6lEcLznJqthkfzSXA4y9TLBFGdhQT21KpAINEB7IoPeaViz2bDmfvZPT3Hx48NNXkWKO3R9FT5oEycebVSbKX4MmRlq9gbLowb6j3lALbqKyy+k3naxkdHKMxVo30r2t5zxBBly1EQ+Rs3OZWcgCLIiliCA14zz5KPTWKHVi2zc9Pjh9C8Mw1zY1ou0c1YdB9zc4wP/wN2CCb0VBY1EhPb3Mi7MoX2WVMpnCgzWwlC15S1UBX7/lw5wuoha0++apfCHSp4K4soV4uJrKIBEsSzh1+dPLQi6HONnO1tBKDy5V3Yn3+XVdoWLmfxvdGlFz2Ag5Lln+J1KXucl4ibtdP8iPRza30eVV8ZjprbTW6n650ZLZtwSBKC/faNe6cWXrqqd3elIRx/DAYBQLBEFIxp6AtlGQbKW0Ll2JED4H+MI30bCnXKxxCf1paTLeKtO8YztcnKa1v6Fx2/HVWkRocw7ba6S0aombboSAXVmjphP9ektZtiQbgtlWUutuLA235WVuIDvLvE50gdauCjuVF9e71d9dAiwwjZ6dXPGS3V7DDPCXJokPYw1ONk0sr5QQoayBK7+6YsWHgp/3R9guilrU66fhhqr5GGK1F7rIXvvTL+RSkL9smeNkGocumdP2OIpXdkjRlhxGk7LOSCGV/KKYPPAa19KqhfOwexsVUYJVMUEVjeOFWa1J9a++gjIsuTB3jdEcyJ6spKl/65XgUNl6xo8IwruyAvZVz3xF4kNmWfMWccMn7iWAqaWQJqve2t3eDlJeaOQDfG4Q6ExbLIESanqxgxjwfzjLLNNDKdJznlXoO7Ju13b/XnOaCOAPpRg1NL+zzcl16qcsOB1r+klhmDsJCFosdS+SU+ydybV5xgmOzzj58JQ04K10V/bS4IDxxV0rlKytC/jvKwwa5ms0AHvd0OO+lNCkI02M6QOQjDrnKwYIQAGd9CZgTagfJSA65ncySjZNRhS0Jf5RgEpcAzaZd2KA72B66xMOE+8tlZCO8Q4xEGF46CBlDdM0DYRqCDWZISmzCSXE8zw/90IFbNJZpNWoBUb5USpeN32VlWyoQcSmXY8ozsCItboNx2dwjKdbiicb2GpdaK6VbAhmz5VXoqcVlYDaIrEblXDyYjHYiQeMMYnqDjAdp3Kb5uU1imZTipt4yX3eF47ggENGbpd/zdlk6iiJ2WbyeOof07gCmQgWONunauUCaIsY1n5ifz10fZ8jMGqYu88lwtlV332IlZl011tNBb/KAyIvFRJsHMBdu5isnAieHQ9JwjWXqngkX21TfK968bPsGG1pZlc5sgp+sarEzC9w/unQiaYWlEytTpSBdjLS1ayPFL4sECif4NkZsKZcYKquqMm8ewYlnAHkAR9kO0VJMImADeU/XWcJN7zLzHZKF1XJyYSHT095hlIQ+9VjqYxBM56805Tr4Vwq755dR9Z+N33KE9v2ysHf0jistBbKkL9zJHNomDTKF5K+FL9MecYdaMnn4GNGO4xrkDGagm1u8N/TYJu7Q3cBqfwR0Uyo22sRqCb/Zy64Jpzv+DYh/I2Sb5lbfnA82gWgHdWAcHy7hbUs+aLCnZ6ort4ryEGg2IFCZ5laV88EmlIM6sLJE7rYUWMRF6vQoLbQftuSmi6DQHtyvyjDgtV2X90SGtueMG3mMO4aSbcjTpaRflOY9X6SsDydotM3KGA3QqDu9B8J1Xixh5nM4SD06WyuD1BLRQKRpV0WaaVltTm3yK7PuneaSb4yzcJV7US7VNOc8Y+cdZ2xERrMFW5TSHecVbpWyf53yQB0BIjZQ+NOIbnm9LOhQicPgoBeOFx6UQuKuHnA6a7qCsmDzwf/GQujQeABK8u4VCmgA4a8B/aBs6cBQZOYt+BCEsTrSemacMA2BM41sTgKcCPSDgrslguDD/rJQveYD4+Eye5uu+RI5IqiCyNp0RhZGnDBkQ/Nd9dx0O3bMloYmDRNDH9ZFo4/P6xJ94uXF8OT7uLeCYHJVSrlmAipkiDmtogSpJ90itaensqIOz8YqR24LScBxQUxSBJRg75VcMCCo/dKb3lbbML3zwCZSCw7qwMoylbalfO7aOfR1eJX0mD7D4dvmByXEn3+TVhwkBmws1ZjC30qs2GhCMsFCx3giOT2DfmorfnmOvbQYsds3FgcE+FY+OXw2RQZEwd+MeeZTewh0EwA61cA73vf7PmCC4DS3OiFgf2xEVpA7an/aA8sJ8ncVkcPy2HsqqVbcDjvFB1UVYFdeyaSHXyRh20DeGJaHem8B8BvF7ou+PUr1YdrXEOHAa7NhbB7suwkwB30HBrHn/9W2Pt62XT/CW+UhAfynSW16EDsrtwbn30UXJlj/rq1Nj29pbXrE7epnSLiDzUsKJeCp3K1i+oI7NxdQnAnbCS7mUZsNNGZQOyWgJdNxx0/qTwzIr6vcB+EFy9ksSZC4bFuzCuq038jMgpr/bIw2gKb0CyB+BkkHChx5DiCIbz5QXIdTTLrm4LZYGDB7kNUdNaQrAwF+sgH39siFrYDy4Tv4bY8W6ACsyGGwIaAW9EFZuSC81YgaXbIJ+EaNBwZ0YAXwtiAe7NsO+kFpPdMEAn3rBN3GiLjNEG4bpdbOLUi00vZE1BngDsAZI9U2OK+zuQM2AfDcgIFhn23URNsKBRkQuoaHoYiy/MP02bY/G6LAEt23Ewa2R4dBf28GPthkhQaDutNtg/D2tt0EoL22A0P4eKjhtiK2s2t7fHZ9fweNyKbbFqOws+024q7TduhIaxpuDFeb9g/nqr3tqxmD9f8UeWAqf04yzYS9a6Mx/Fp2uhCks2ttOUyZubIOebOGB2ht9w7hwlhLsYkjMOqwcgSzwwHL4jdwSF3rJtzIfIyruI7NC+5yVYcbcFFAcptwlovuJbeBURjMgSgyb8CVIN+9iSPkMUtHND0cOA+5gSPQIJt4u9S49PYmhzdKa5ve22uEPePW/bWeAk1pl3TRX1+o7XbHHJsm12ucZtOgYbbcxjXE5F/IsXjmj7LyltrYQg6GUogljPFyQYvy/dgLHRqDMxuQgqyW5uj1xuKkY81ewkISrOH137GAg1JIjo94mjC8O+UTNCfgUKZ7BhTjDhch273QSNarOEHLWWcZ3z1m7df1Z//GQtjno/S8G5MyeHd9FJSAAIAAQANAPCADM2tNZcxi03K/IqNi8LqmLX/SynBcuVclWDxETplX2fNNM/5qb4CKHcHUwYwT1GjXicohRjEhhdas367J3Mm4EqqdEJiCZ+H9LNQ1B0wmCuf6GUvCJqbAILXmq966U5WVACA1NPddz2y+3I89akl9OmmTkpPIKGlEMmIS6d7rKuNcm4g13FqiZqhKqnwlhS58Yc36eq55r1kDSK1Yxt4+RHh9HOkA/XNJVXxgbXqYf6Rfs0SqGjkjD12OR4H50w99lj0dyT76OpvWiI4/2JkeycSDzwbT8PUJX2hvqs213gbJHBkbS26c2jprP4A5k6997EavrtMY/XXYVTbbAB09r+356ySgK6utu9Xz9BjaxZ8hTidoW0i3iDm9sX5TRGRiQQERBqDT/8kwux0DwiTYlUtotZCzpcGMZ5w3c4eyvppG/DG0yCLJv2d5wVeclnHD7OPLFJi5v5b5Kf1DCl4S4aq0cOKrPhv8spAI/mZb+l8cN8MWKJUfgUhMj6yaspD9BFgQSwnxNJiLkHC+r5GtDXS7LnBsRKwB49RqZFZzhQ0wuiQzoqPkXbTO7MHiMs/FI0GTy6pYYnqw+syNSf1TWdrUCeDMzvMxkVjglGHNjW2jLgqXfC7VAeSHCw4OaWYI2cuH0wrhfnAB/hIXNwCoIwnBrxcO+Tcsa91RhHxfU6CRVzOXonA5U4b7omQDCWvWFo5TANXjH9Qrpa1I2PcuiUE0lZRnTPgJ8qLnKDeZAtKvfrcpQDc6g0q3ncKc5TorbO47x8TO6jDS2DsDA1vn5QaxJ+93fgKbvdW5cvXVaqdby/v/tnHJ5uoURo8R0fBv1p4As6x/mUgmwjlrZy2xTrRGMB4YG40r9BDvUBj/u+0J8B76zSm03K3JniyTtRq1X6OPlbS6zaSG+IOeADfU/HZZOX7gIRQz3nbru6C/b8eOLeS4NZ6/0SklIstJ2h+R/qojuck4IvF88FQrUnkIXdj5U9fizW824fvPYcbWuJUOgVbBCK3Y9Hg6oR72r5t316rviqC6c2IeZlAPmX6s+AcXk2K5K0Za2j+jmLi+mL3/exZXjyPZf1uxnx6/KIeKVwkRnp1RvZrFsADk9qGB4SvMsnyu8iCdPVstP5onGi0NN8L5u0NZSOLIb95eFNoFhz9dihC9Kv+TEIHmOyLUW1y2wVuN6Xz2uzmlHbqr0OPKbWo9lwNssQI64y3R7uKhMGxQqLeX7psfGDAgR8pl6LmpEolzGJIlE0dJbv/fafaROuyItpJgakNHGfYnrF3InYQkbxcMaAIjIQ3QpsBLuO2+lDfWq1UNuefU0n8Ll1QeyNy5Mk4HFruODigrVbvEVOGxz3yhFhLTcKzXBZEv8F9hF6AFzHmtMKk+eEIpU0R114kiPSuEQVr0rAvmfZ6AM69X4fCWq4FJi/eASB/lCVzxZdclFxspeJjtgdUyash4xyQwkz4461jUKjcLCiA9SiHO9XAzxcXeNswXQjCA64iehnMB0uA/QfDmTNfZ1Wh0pDG0sVycYgY6Z3Kq3qQmmJsoUJagTE+MVdNc/2T3kOWyngyU60l2017FW9oLNUCYwAbLM0Cdrl5RBq/L40DszMkbGuJLMS+sxcvDVNCKQym4eUwhaBoAd9cEo0ZY+YSHSu/+apRvgbhbGAAKhavdKc6r7ONHoIPgQsxhTRiK50iAVIllQzEmfohQnVxAKyxQYJEAvxSgMXl3ORg43GLwAy2GuP1w0tIrOlbJqrz7YG5lW5T8PMWhZ6Q25xhQj1hJ5lwrAAV6usLC/WNDttDZFvcg8VVybZ3YLPOrOkCZ1QkU+FEEYhwV5FQBcvtOMUH7le8S4OB38Sx2H1qggqfskYKHjPIJPCva+wsE39HWoSNKTfnke53nxAcD8403E8eXDDtUjnWrjK2lxXRThRNhaaCvg5gfTCezXklSav48SuZNgvBHlo9/5SxjruUTfLCVB2cJIiK1zYf1kCzJOvn96YdsIESPU9Km+IMpWu54eVEYVHT3ru00KNH5KiwwBB38CixQ3Z45WAr2f0tc12KKPbHvVVdiNHstkW3Hqxl1zSPgpMEGv2Gtqld3L19kelcNG6+3Q3l9pIao7emsT9JhbnBr7UZB3qWUWBN/r4WrVwvpxHxgpz2t6TmKtqXFSij2ShBC89lJAZTMGAIuvB4JphSCrlmLddPryglmYNxV0Fwqn6TPCWVyHOycxNBJ8oKmRT4GPF/ehloMyS/xQzrzPk/ELJa7gVrKaC8nHyPqEGaiRRQ3/snEyEbS5i0SgzMgl1VcX4sA5BHeywvBeNm79w+gSOcfyeY6+I9E5R9kEDth5pX9p+ckjFtk17omzgXh3XMfkClNe67ZV4agMu/TaV8CrgAdEevFQ51ehQpQhJfqAQ95VEyfk5akAD05QCdYxs8CawDeqCrk8r5jSFHsrht8574TAJXdc2ud9B/pdQ401e81T8ibKMbSeiNJIxSBa7Ej9zSaQBF1S+O5qKEeDNEdqvVcTqVOoRPZpJGdd9Vqo6vu5YO2zdZUZ1q+Eu6MzhGFVq4Q2hiAJq5jE0Q76t4EB9a2NURkG/z1QbAz1gTYna4j0T+YjPW5PQY0eQz9S6fg81PzlqQ/M88thrDeaWbQyn1C3NQ108C11pKo6D9TMFeu7YB+4ZRCPc1wVbyjnxNFn5mCR5X2FCiLiCR94vaxyROHPibsj46pQUpX41llBjQ8H5HcReGITLDSQ1SPKUcFEQW3nsEwpMJ+5V5CQp3K8rFqwYyrz/CIlNcdR7gln7YvsA142i50y/9z9sYZHRfGH7qYXAiZYsrxq7uqVN2PqPYIF9DZvk03uo4uGHQVXwPfOOcQ3bgczQKTZ/tEtuTkuwWnb+iXCdNUKr9OcwlPVaoOHzkgbFSF2d72iSJ4jVzdZF2EkqQnQbLbS/8A2nx6QxOt2NqvQDfB6wO7rwmNh7DbK/25kF6ykEarWmQh3z5cAt8gogs1262w3U++f4B+OcNfusFP1hTE2vLV86sIOAehD4Bm2iWlOjQBpk/q6mp8vFofI5zIRbGguTHivtWGxri1K99WEzTjpQMwyhfvCckACnSEYiQCz2pQCEaiaqc8gvCtybMv4CLzvRRocV/ucgsfoPsIq9ckGxtaYmJqXHusi9KyY4kQISJ+yAXWTV3e+2cueVX9PcNob39GbGH0lset7eIz1/yNB5q6NqRdtOBC2zfBBhg7FQCX4x1LdVL2gFJbBXakaxSgIoWbaiAU4rluTVoJPH+1ztEhMZNX0rMq056xa7n4FhMkv8jbLIXT0xFddRhyO3YvxG9VHRyJTdtLaGXZNKQkxUrsD/ex0HqlZK8qFeVEJmm5wLdERpc3A6+tNF/dm1XwXwerX9DXK2hqxhhzI5fxFkUH/8rKbe2rT7DUfRI7tWNvrCDyj/cwEjZncjdnDzepKuy2SI6T7r+esez+L+EG0XozJPNuBANI8+7NFATi3NwaAMIqShInDi4bAHsvNTMJ1sPKxTVo7e3OXiYphOk7x0TspubSByd6w8IzLHYrx4WXg/vIzsmA+M9J8F9M/5Y5/8WdaiAK+Qg1kAEb8uNtQXzgKcxmiW4c/VIF1SifmlT5ekYXLwI8o2gJncuSNxWIz8VqkkwHawJSmp57Dd2Q+2yaSsc5Rr57SPev+k7Ut03FuoXUKjhZqi13I4SwpkwQZMavag76u1lh8mrl/HScUwxT0Fy+5rhp6bkJlpjCOXNEJ29iEuZ2EmIGIe8x6vqO+IaR7B7n7UfHI8+OK014ZssIW9jAc6yCl8wrt576Y+sPGqVCqUxUXFBZYeUx7c3YaK6gM5aNRP+WnshPnKFRvbHJVlC0PXuQmpSajRGfbzeVxrQFZaIZeO5sbha398XybaOIdzPv9fvVu7pQh0d5bQOp8qKMnTnAzIlFzbIPdZFv4HwBtJ3X2mOa5J6A2ZdEPYmj8xM9M0XbaMk0yb5tbnppf3o/1Dgf5wgQIzYUL0t6ivN8BcYA2vlpeskNBSHLLUPizfeO9fw0Vr6w+HOM0oHW/onPQwmqqK8RwJxgoIwQ/WVMpA0VObbC/1+fjqKKVaBb//5DKnyfPUKBc+lSoo9y2xV9aQD2042LB8GjcGISnKOQSQP1SMLXAxhAQl3UmOpuLiOpuNlEb32KthNqBQZ1oQzJVrAZctFgnrtPrk9Pq6LFLQPJSkTbs2lOyO2MUgvnWGanrW7EWuuzE0gLaIc2NpWB0JK1ewHSErU7TeTFPwJl6pcLk/5udFJ/Lsk6OOyBtO/SvPWPt3fnnXJTftwz23AHNQonsUrvMBbAWCC4odz6FMWCtdCR6dvkejVGfiUYuj6EjHca1G6UwSneK4EQplRfsUwwuTccosvWZMdnM8pOVAZiDpGPbKff+asOXc127P65yZvvJ7mqLDPVcJRbT8zqlNfReY7/WzBT9ZDZmPV2NHSu5vZ0y2v/Uc/h9cjrURk0Yq+pUGNx7dv9hTB2gU7IvbAkGGoR51zFJJ6+ADmqsMCRSupFj5l87oAwWq1hXLDfuJVDdrY3eVbqkExgSvPKh5vVTcugoP1b8x1G2M7RwVByPDpB52xl4jDu8bcSb67vsiyrDUcoIJxyR7ejmVpRuH8liYvppVGl95wbiYQQIdU6verWv6Pt4+5kHO+DMUub5uhk5Ow0JXdx2XGSjeAyIxe8HlVjtWftfY/fG+Vg27ZjdDtdzmTfWQgXnOneGK0Ts5iPrjnzcku151VD/toMqllRnpnnQIPVA6VtJ7uOqRwIINo5ONjGdaOyG8Uuqzb7PhJReO6U/Ei8hggxwlHFbuT+W2I94LqRnw4mT6PcdgMawK857HstmHwefDmn3GgcQzzZ5AP5Ytuj5TU32hGXxHsayDUGxius6FDIiZejnjynm8IUfnsEidR22UvpvXBbW3ONsVSvFXRZw55nMaI4h5G6EDFjiedQeiXioizkn0Qutn8AXrgaWKFEILkv4pKfQ248+yaxPjyL3HF6Wr1QRrVySVLkD+AlZdNteoEV8c6/ol275NhnUHeAIsQTZ4Q1rU1kTN9VmAuszECF7hnlRp3vrTIl1RmwIctbryYuPaVcUtpw/7PXg82qiNBxS4YzAnKVGoM3aw9+8JcZkh4E3JUDrt6jzZwx0tIrA8W+nILOUepEiYQM6up0MTyBHs9SajXzzOXosEtA2XlWSoiMug1IWii3FdlkpOsUFeg54WBm2Amr2msa3Q8GM55tp8T3Ordp8YxBzQHMbNkkWx+GDi2y/HRzrV1MGjOH4k++UR1/0N5Wv35KB2zkY4dKpUBaYItfgUbZCeOWtNNKKU0gMCroNhsN4HlGGeauB64ur8H8/YazFUpruPCV+43s0hho64xDyHFaoS5YJuws0i281Ccg5rDKOoj+6BMrDghJ3zkIwSOkQuJjvIQ4qWnEVjRB7fixcMCaPLr3Cttgxk5BqP/HF66kIahluD81gawRzw7z8WoreahW0G4uNwi4YM8SvWPKRDVxfCoDAniwrQbH6ZOYLXlVPVHbnjjVrFKti+4gJLIKgSSFPtyNZNOzpT906vqPNGHmJwm+bHwLXZmOqI4O6fiJf6Z1CVeIUPxf6SuRf9kosc7LBWXGHwfzrpqsL2vIJKrZ9P0yHQC3mvBo2fYi0Ptfu9NnGfbiHQGKIn/qwNUTdU+1afEXgETUUfCYVWAQyBqiNHT1OXcltaiv+R5RsHxKputiFls0WtPWQMgnrF4+CBuqpBn6WjN74duc+Zzrm6btD/jcF0eGtr3sLlofnQ7b3+dGkuam/eFR8rfUKDK5JH+Yf/j9ayqJk3ZWCew/r/tI8l7QGc1tCARBk3UzBJtzw7z3M6bVnRq+/gxb02UEZK4H97iRhSp13+cZAxVfYE/hVazfz06MxLtbT4TPsyJKdm0lEA0YvogzC4FDBp6ArCFAnKtpI2QZ+nPMmabnzXwRseGJwgLaVvfPAciMRVx6y/a1aVfum+Nzt7lTgR/yG9YNuF1y2liArLN6sC/k0hOoaOCqPo8/oXs2cXUWE9G4S5nYfoW5ermNq9o/bMOZG7XQeWXZhVRe68gpLbCQaIrMUun11Ubpn1APEPxMwDFXVetx8vy8b9YOjoNqrpk2j4gRnI40XwDIyZWSlrJ8PFsUvPz4o0vihYJNA6gGNwu0xCpYN5Q1eb+Gc5/BnJgMEMxbNEhc8iRQq4/B6h1gF45atbuW+mDofRi0esEoLjrQxH6uMAW2afiJzVp3WNWqUMd9WD1oXDI/cfa29lw7tNavZaat5WH4yO9Bzwu7oDn2BaompGIaIL/6Icw8RcuaC8xbLz4/KfC4oeUaiYm+8hm/SNqo4yg6noHJgiHgJz8nWiBseabDVP2Ka62yyyg+1el5kVoM6syTpK8mlu8wTybLhNo8qzWK4lC/IVoAKQakQNzWiUz84zIZQstRjrxwLc0shE47+6rmV8mRvM6qAnh3Boif4LqFKlyd5E3/aM2uGM8IC9DKiApGilLJUPbH4acyur/p/rZS+aTe4WK+K+KhfAJES9O/DkrJym2hxFwjJVn3N6wSMIddv//Gi5L0lJBVOsIlBHTcyUy7K3l4Pe8LeWoO1DG2maZt3wjUdf+Lc04vJZFRarXXYQ6vQeshDrovZRoHnbkkuhsquekDyUamkZfFscQEhGibcecpgd3Tm6pyMZGH3XZrW70q4Wf/Lmtbug80Q1LzvKotfdb6JEzINaNImRlysZ/HoA0O6b7RVekHEtqz0Fc0GGYaNVlIwFJ8eKbT0SZWZDNTomqnacXISs+eU+Z71Smg5OMp8mLq+52ex2n15fc1eianftTA5lNAE2P7BhuHifrcXSCH4tEz7El2vb+kHaXiiDPTAZzdzAnrqnWw/7uJuSMGKlodAC31Eg2dT0fuQB3otIy5uYfPo0GhasF0VaZ87HiGL8gJJ82xWTmqWy4cWzP1E0oqdgsU7mI+TPj7eVuJalQpcn40rHLEolyF8mhKvB/mV0StNV8fRPPgadAayf55QsLdyt8YlQW/xwMFTpsHjkizylCgPQsrF8956DFHsqnke1aNHTJ65BHfkEQkE/h9qku6qzAdkW8iOc8JHxZ6vyiL5fLW/V+hezlwwnEDm8nBx1n8lZVorL7YjWnxGbroDO0L5QmfVtshkyZTg3Oa+gz02cIYG1wwCEhLGE3DLHVXgSJvurmoKUBSl1GIp06igNlB+JxH9ae2SqKar/3/WJxYoqIWiO/NpfEKkeXmEeiBURzn/v9n7w7lZUv/VulG2Vv/2zu8NaZASt4HcZQJKIa0LFzQELxY8fAsW96E5wfxUQW1w/vJIBQKfSWy5sSO7U3FmkcxX1i6TN1lAugL9XgmXUawoPeTYnB4BjmBO5p34nrvL9dH9kDPndjztQxK7d35puUiYUTmjGWsH5to0AFYXEY8hPa1uwabQXwT+QlwwcO0nrgvxq4VCropq7pxTYffVDfwwFbdbXwpxJdzM5wSklvw4wFKhyfov8iyhPGL+ARsA5qS6yYFl9bjb0OtI1lPV2WFm839g2Isfuq9qAQDvNkFUnhz6o/p/xAEsujybrH/dcqSvIH3p6IS7EPZeKZSC4wIMP6gomyfWB+etogwkQ70VeYd2KLJYSFCGUUQXFhTRKbwKjWkj6FacnaYSRhdXpIFWTflb4XV8IMecNq9VwooVlU3h4BzhYZ+AfJwFwUDAJiwUtifR4Q+uKxSHe4aa22KXhd3A0U5YlcCJfGQ2mCle/pMziTG3ZNSiUocJm4/3eLH3UZcieEFCFdS9KGbw5J8gpol+WJsKY0GJmOAWrljZsW83N3jgwvO8iu5MWUTHAXHut4LaFDDSTbKFtaHTofIf+6QZlEc+Nwy805F3YCdRc9rqE+V5PnjPpd+FA637DzjyOXI+le9ud7AzVT1l0lI67nUxh7krfyWUhCtFgPf7CX936T8CeFmIjLXMvb0MH2jwc0ye1RvmUBgRNxIpZbD4ENNh7eDNCDPTlW4nOrZryAQhFFTSleHWVBzx/8UWnkq4cepZJfdev9X/DooBOx8r7fyuFofOya9mc7Nb/9ZUVR4lpuKMzL2IJNkCxraM2K7MWyGZgPkU3fll25Ee2KmpfjcWs7U9GHMEdidXTRcObTB7ckRmgNq6mHE5xWB70YNmNsJKcSaxc0lx38Q0QNamsxstxvL2jI8M80Ddk9CkS/dMfEPtYdC3KCVP56mD/aS2OnEVhyCQTD6II9mqdJJ2k6s8k9RORcYN0sSeMgS5LhEmpOOrbOLgTp8WIquilgqV+Ep54oDC5NvO0d7B4HBAp7/t/XQZpSwG34JLCyos995HAQc2TZpJ2OHYIAmiKaaYuqOESaJVUlFMqk0E9NIAaR+E5TTKWZw7lhaaiCGjjBnlzSZmSR6u+OPwlOYsv14wYQVWWKfH7+U7gL5uJxglUCrUGYRxhM6NAPX8g9wqDhFE8GXbT/oP2g09V/f9E8HModjOosg0/8WHj54FR+fzk5vfbPjdCDl3b7voXt+JxAaNSeN3krFMbKcF7J9wlB7veXG+s/icjOrBhDM/FtprxosicPIRBsjsSnNtbIMsg8jk77HSGOGHroSsFKQHS/ZrPPqKUsqsZkmwjSk1ChvgwDNa3J8ychtONBwcpa4klGEqox8DEBUqIWZ/98WoEOomG4YNV7KY4wOsGo6TbnOH+gpdEcbv5k6kGAl1b028oxOY0nmUGvQoecvTPXb4wogzX/p559XugDu5onZas63RbPzXbPxiqb+pTAMxs8QCCEJkrtL4MEc+oDaWLqWfxpiWFVpwnzQ5rEVUxbUHvX5VanFHeyko5euG5zkMRS1nEk26Je0TZWRjZ5m0FxZKSyAbW6+qPppphtA+Shw53ginvhOYT05AVSeTK6x0egT196fRBdCeaPeFtlmP9c22ZMF+6kz3J+id+TF5UMaRVsRVbMMEhRUfL3QccMwPQ4sk3ncwsMXmyJsF40HPO7sV8FdjlJEq0xc5CRHCZNJs14Frfwa+hxz8/lB5jMFdZIeF2EsNU9d4WRnDhYifskKUuTrJI+BVg94/kiMVTQjZuLgPoLg1n8AwQvaryIoNH1/KfaRFXYlEBQVA5mh7vz0gxAqPbGGVS0lbq7XYczTWuqOTihkezZZNyid2LwhrJT0d7qY7zRAY/O60PLEB9fuu5HMtgBXlBrTYyd7u1NnkC4v7LRDA4qDgR4MqTKgee6PqbDw8voTw1xrPPE6dxQHTXTn1XfkpSsMaDkYp9CeLg/jAQSJvQIoBPLSDNqDFM/JlvQIZ6wDlppfFzXCIIdod9UOk8/A8tDrvD+D/plHt8EDfSy3QFO2svK+6TrfBvYLMnAtMEpuqdN8Uu7wt0/vY7QkdyNOv9usne+wGtoYzkitkyTYxuvka/gitdWwTvpSE9KbNII2t6Kb1A1u5NeTFkc8ydGnIOGaQ6s9inJv5R+pQwT5pW9ih56GVnMLRGqpVR4rptZ5RDlG1NESeSMKK0ZhRYi5P61aIyKp9kgWidIwfSGiNkzd4GcHu6QR/WyT/80pOongS3+jOv7ggsjz4TjBwBf8hTJwtrzCzUbaPEwd0Zm70Zv8hF5uV3GxRrYG5WaV8bRncnfu+jwx+68A51Zq9xufBFc902m/ci0RX60oSGMHdn2l47TSDua+5+NaqK/lnrdsLp5pAYeggPDmu9XJCVNYpsDwYazUmo+E9u0gqIUwri3LtIiFJ2lMMNE4jaPmAPCAdfH1Jk9W/jF5lNzFBRdDrKhtQ0LSgDsZvexbY3v5xFxS9UXCxwm1IPNI0isJze7YGbE+JEEcCBNmyfBEazOm28riflhVZkqcVSV124YlXK4XQJoZb32S0EGJ0Q3WiulcCpEBXZaJCpaXsudtduepqXj5j+iQ0MXj7AXW2ybeTfuyclrI3hLiPbSDdFVz1753aSV8iErOSBLSExYlLP2Wo37CXYfmkWnf+9jIWr1MdJf7ctdXSeRG38urYn4L6PNSUp6HQqWu1FpY4CnydIEmp1xWAJnKps5CI8pRdc1oKV0yQ3f6vpPR5rsknavN2g2vg644Wh43u+zwak5FTHMSUyS7WvJQhySm8MvWav+M6qzBXZQ5ptUVobvAU0D+cvC9D5JgJl8sB7Y225yTge14l5iCw8Gv2ncVpwqSvG/epsaH9+GbWtlwi2ioPTO4u7lIEbM14Gr5z2hrdjCXSyPuJrxiuXab5N7mcJ02HxlupSP4etlOM9OEVuS2km7KYtc7e5MbcPmTpezwwT6Oy0/PVdUA4TOmc6XJi7+g8DWPRmEyQxfAs79Q5zZH4uvH1AibEQjSfXf8zAz0FMB8bJ8G5VImOfkxUduuE4frzYq/FrCJJm22o7FDm3Fr5xc3f36qiTd24ZW1TVITLq6IoYnjr2LEOQtkCnLVQk8+v7hsYiuGsfrGFfe0Ruj+Jr8nGbWuPORdUJoL85yeNthuN8Ai2kQWxgM/vA81RaE6lYcDLlVeL3leWuMgElkbejX/np/XNGvOK+9+VSNILYcDNBB/opPZVr54SIgZZLniIYVfGJKia+wyHrFGo3ryYzUIOkxbczMtXjbiGxx6cGsUqo4CEqnOBmJ86961AwaDv7RgVn0pwQo5yzmvE4+HBNDkxxdOfxS5HQwn6YBpw9AjVUJuAmc1qvvbTYOAcWHszwpAiKk8Cu/oNYyH1qtn6tHH3d0rdTFtFTv1Cp2shTefgaFmFVt5Z4WVhsbe4T4GzS1uJoQjKdEYeTHBaPB38QLxkBexYXTc537hNJAata26pwSpFGm+e6ySUqUdVUvpqm6xGSSmJ5OHzwF8Rq4ae/ohQ6un+0PAt0OaeKoAziNUgwES1equtbx0HoTcMUgoeBBVGGaMl2CMZHSY0zirSwVPTz6Nf2S9/XFS3qpiYwcV8cf8axGsXIhzkEr8XQBRUirz7LsmK7G5jQReYu0qPLKP5KvqHcthuODiUl/Y4BKUg0+hLOZgdHZUXqqBRstTHiKuiFWNXOafNljMURGXDzWdnwICF3wJCLHA7Ly9/7Kp9qZ6w5svDVff+M4DvSWNEFCXstcfgJ//Z7KsyMKt7VdauMtitGmWYHisgK/5V1Kc8AH/0Nhx7FWrT2YI/nGnz1fr8RplMPcur1U+yIKmUaPmpLzfD8nQvma7S7E+L9anJVEPC6wL9CJToUB8fMA8DyYVj2HbvEcBvV+zSbtC/PRaTf27o+F6omz3RwiPyXf1JZvkjFCv8nkdpuis5s8tnvVDIO+S0k4/WEuvHyTuisu1qowYBhI/BN56kUCC98lwQYV+KNIKCAJZCDU7AkI09gCHIdqZSEUAKgxIK1usdMM5EQ99mXDKkxF8A8DDds7AtCZjq4thVhkIIz6dH0KmZKBQKYyNzJHsfmiu6toOyGdCY6e85DFrmRD9SpfRizOkYJpIKyAUK8fU8MNOddCfLh0cbKWtmOzrVwC0d4togAPz7AWs34NGTwtnmKrWgpxnP5faQ4SGEE1F1hPS7w0pFtcUlpoKeIGr2VVK6v0ouLTXq9FALWcBv6Pojd2NXqZ7hRaSvId9gThgAafKdnS8p41rRXFL9gSoj73e+cCOgY9sjIGOSGcS16NicXsTmkNxoyR34Qf3Kqmce8/GHAHdoIFun2uyFVEzwUGFRWSwxcEZZjdUZGVGDVIiD42UxoclY0P2JBsdposV42aDTaoWcQW0ZM0wRidvzIjoLP8KDoCOBrFSHhvgHf0ddT3v3Td1Uqv8ISVO4ejqArh6W5GYHT5VOKvlBJE05jPi2/MYgE808XHKK5UMMoLjMDK8fJDd5e2AYUyO8tWfgwEHj4ylaB1gngpyD58KSiFOIHQqQjc4cCS3YloqL/mJUV9994EaouTfe6H6tFlJC/Jn2i0vtGdxu4TJFQQKPxAAUfcpaLeCj1phU5ohCM8DAV01NMDwduVv5WTZ8ugMWdoq+9htcIQnk6jfr7ejMSNEzkMgnazEOdBFvfi4Zh1drtHdnAKViC8gbRxLnA8otMeZwxY67khBVDqjEqX42o1yaYSR24Q+kB73IdWFyZdzMzI8AVemjLts+qLCsNIgSBlzVDMzAWNE9AxRysVZ2ylhWp7VTGblAtrQbGdY2OhwlR+3V4mQjukuVXZn0Bo3boeXMDEow4kzwnwFEx7EBCphXOjPktUFT60T6oX1MBPbnAUxeKjlr03KIxQ4Vg2UMXrvjxQRiP1ySWSwIfw8kCDbnH06keBfgBsXBJx5ZQy1lHABMDEKualNpEf9eb4XJ6NxXv9jrD4roZwY5yNydU0tAG500chvE1SSOtsq1B59eLzYrKfFl745oGvMI/yZ3BNptOXkbTfB08AzqDxA/yJ+HYYIq8L8EgCSoQyQxrxePu61TunWXIbpkTBuTcIgWKTK4Q/9n/bwcgMI3BoqzgUQ0+rwWHmDJtoyAETWcXb96HpuKKsU4o2Ixe9HwnrTABQCGKQOKiGVev6gdGAUU+i2HTZZYnVzIGXX2uGXikn5PT3WrNcsge551i1JgxvxBSh8Vw6uCjK0qIwlVDm0HvtAyNKbyeerad8CecnO6c9EEHtz3EixAq+L94siVHQTXmYQtAIFyji1zKz0dzm+WfoQJXVjvLxBUfgwRfUPu3zanP24SPegMIRwr2LPY2dmwizJTab9DF8gbNN87aITASP/i7JOw8wwu2s3P5aJRx5QsdtKJzJLtyVqFPFAgr2z3owLpEUROernTEnWv4df7ZG4wdw5U2RElARcj5Hq7nVoREuk3FEtuz3Rpy6Nn8s9S/Nim313DbV1FitJOGJAsszuVeLZvHXXfI43KFub7Pait0lNGv0Ee51SCejXZR5yZ5tcyGXpisKXKHKbYysxr4r46EXR6eVHEvuybKpNzq9utoYafHjuOFtmhguh52FjqklTHd/7NE7aZqNc2kh7pWba+JMyA/95Uk/Zo1O7bGT4tEYHClKlCTXayJIQ6FEEYBamVFzg7YpD0rqpWbKH2f0QgU5a3mP5jOYISN1S5+d5NboHglesDJX//sNUR+tfBjJrSfVQP/Dvcq6/qAcv+OI0gHiylR0qJvyjahCpOVukYWnRtMeqEzfy4rprQGakwXRj4iB6qKelUp+wKM551d/VDGhy3cDPvAXIeot/r7Y1i92EFIu1Ss9upR3EwK9p8NW576V2pj1h80dJCEL4rxDgdmYlbkry8BJeSpXgzsYVG0FGFGivwM6bQIboitRwiIyAauXEZNN0Cmul3xl186VXmeaNBfdMyRKg8FNpc85NU2eZVCmFb9PfT2LR+vqojZWUH/REGO2x7s24mD/T8rXZsgCRLqH/FqBgrXvsFV6uVKz2r/5r8HDvPaUdFplnTVIPgaiKib67CRO+rkbCQPBzSTrMqGJQQFFtSewjASqI7rv4ucoDrQx6J7qKss9BmvpxJhXs6Q4pWaYQz9CtTlv0P4Yl+eLWhddTsvpA40O+JDtWb2k385zmguuyJwzlTz9FG0JXtTGY37C6TM5VpWHVebbJSJec8jf2vZLVBNlJ3cyUVLWWQluJN8sEl4ZoL1Nbf7CJBORCW1Q3UWoUIvEM1Klt3x7JMFXNRXeT8osLgcugte2oP+Jyl0o+G88YYjVlvRgVH2kpBTBdSzbCsa9L2AvRGG2JikTJuG0JtxUekujasqjcm5uA6Feg47RYTn2VCYE+9mlLAK/OO71sGCi869YYRHl62GV+/nMx4z1ipXBrF0ry+oTQKeY625z0+IXISm8fm52g5BeqUOzseNXkQJYo4oL5RMY0S0uiHulvk+6wsskQ8YU2iSARTIKvA48MUiQ8u0fR67qZiT/tua7R+0QAfpfjmURnJexXdHv7aUO14b7PV25cITJJ7j7INQbMdWI5z0klJW2WvHZHX9QKt6AakeAsKJCUjEOb6qk+38GS9L+LECwL86XiAInEL1CUm7yPn9XjK2uS32etcqGv1wg1Z/Ej3NjrcLFxofMN0IlNPTOGJMslElC0Jk41XcanJnksmmDHgA5w4vDKLvbIedqdlUIRfS8HdyHNEliyzN/mUKdvOKOHzzLSgYZTu6ZKh+eYfewtl8bUOOX9KMu4X+8RByJ7hgbMRwnhkajNNBLhLny7vjrjqedE2dFH037eKPZZDViRUo8z4zyyfxw3nwyTaPgUV4sKq+/R7dU0KAjkMRpmRev28u2tqmbHymqi6RQ4g/keTGG8UBjOzSEPqVJQeIRsrdCIDVYCQWf6G/rfUxSH/2ph5LrUeZiq02ORy9AIrEVUJdzHBTWvtmtM5oEidtARFkFOjz5MvxoaO9kNk1jap9KdKZCwFHCMK+/cmpJiuPCTpOCTAFOpbBPbYZrOV++5YV7KHgJu+WMwZQGrP005rWs1stn9pyB/7QDzwZHIma4/pSqbEoW6OhZJGR+qK856NPCIHbRUyjI31uuS06zy1LqnmWNXs6C2PvypWYCOGaXXKiLKkHRVS9x0mjVCdR4RyIYiOa8Ik89B7FMK1MaL30Q2MAUkY766QAe2ndVqR+ubAuT2i/Sn21VGKF5/aNrY/dgOlcLi8yzqP/tDcpUKR3UX45cD9eojiAMtIRzuDYE0T5LjKWJ82YmPjAwOyimUgP+hFl1TKAprrzViw4itBZsgd3dQ4oos4GLXntik7iv1GYJndwemuNf8B86S+1XW0aiuBxysMGh5PP5TyXqncbcYqg6BmYWMMEHh66Uy6X0YXuA7hCq93xDQPSPwXJP/PpYbhggxjdW/n38hJqe6MIQNuSeHDHyl8Uo2ljmO+bLugNU+KaLS8nU/mQP8jRlaC1RLZnSiwhlDFdWUsRVf+xRK1ZRORNPGfMr3/SByxAEIn8yD2F+BfZf0FZQzzMornprWgKGjllTLhsGF7QS5YVwCbFwkp0aSsNvoTz8/L5W4iQSYkPeMorrPIVpVj2A2nbihZ0Tt7lOScnm3Iv7YZV8taE2NRTcc7on9ZpgXDHJugpKwmS1RugpEvsdtYs5PlKxSSod9pOKQAOKQTpq326q0kLF7O6A3zv8Vub95OVwfWGMvt+apSt6ExlOrIuQcjsMb3YdZyJxZn6OMTz3GcB2271WctuvnLNzISpqnrFMqbLn/uohCM6Zb5l01EyW0YE58zfv0YiMO3YA88BwwshKxt6YbGxfHggaxyGdnTarY/VKp4IxrP0UnnAiQu7m3P5ZiVgcD3UQIIM52iaQA/1XbEcZTkNnd5jZVc1FgISChxqBo9yHpWtQB+Y/OHsw29wG+K0sZ8+unV0iu5P5u1K+PABQoQnIzqLKtx3qqVmU12IrSVwBk5nDZP6haYi7oQbn3yr98Pvb0tbbKKxCvIPfi/Iz8DAkGBq1qjIRY5AhFQELEqMZCRsKsSf8QA0RIy/2koDFiFBF5PITwoe/dTNZomGCLfB3uNxy7/HbpGqNS8cqgGM7tIIc7RAo5r441seJ4TDwxDI/7V9q8H6PLjO3Zt8SqKx79/YHH9zQ+ciXKy9pEPJm3Qz6+H6v2r5LSYsqh96nrJ3keoEGt9NqoME0bQ8hK2cAz0ytvyQnRRTlkfYsHpq+zxHvIbY37BK+aL9y5wgPhxPYSecrOd0JPOtZ++n6Uh8bdaMGJviPhCgj8ArzVZD6TXOc7/9z7skx+1aJA2naWlsVvG5jqKOZWRxnrGZZnHdZBMqyAkCXwC+d9tYGUxOGWU6xsGQ2bT3JPsIMVnConYQU/NwdEJL6s6mP2TmuzByu9SkqT9jshi+Znjt5O4BnHG8oNO2PlXKMALVF8+xo7rXwh2ddYHdadvPwRA9ouLZ10CM5rzrG+NplQ0G4IUPfX8slsFrG52XN6kxBr0VfzqfSGx7AaghXsblUTD1J6ru/J+VsAmpoH8hPN31yQvcnZ8JQKRhYrh9o3UZPeeH7WY/+s9e7xgPiPfz/nYx/2ALnlNGCGPQ3yz9lCPeSO34Cg2k7+BdhzDvw9FcRieA0OHqCjBSi/8QdRDprWzP55rxC+zGrukwHK980tbqoN6ekuf+GlAtGpuKGapDk2DPdUC4TXm1SKfNXLbSk0LOyOSDUMiej5DOR+la/Amfgl0QfzmVW1zmadPDWTvxfphuQOOuYrngsiJdSvJ9JCGMrdNumByJ1QFDTVd7wCf55+LDqQ+rOqKQcUKZf0KiceN3hOyQ2ngKNnL9gIyOrUWSu2G1CMFo/K/YTGrf6DWwvHHWyxtP9iOScJycbAQwOvVYowtSByt8rhcyKk4ntj6LEV9OkicCUzDLz4obFFTpxKze5kF2ZbIphe0gafCyT0ZHFmP1yzyV7lD0+2k83AI4WqiaM0kSPskaT9lujgNFkYr7NnuC22JuKxOYRwzWZM41mnZZVjUsw9Gn3DyowQJ9onNpmi5Zh9WKemjaZCQ5chJG+59SexsekLZcuWnUCVAEPrm3aFvl3f2rbtp68UnOnwQZlCBoG8jxjzwvsv2RnVZT5jAfGi4bSE0nGAJpwa2+WYs90qwjl7LXJIroKLqz2Na9lk2B1pAxDrYOU91f1V2R0HzYqbnuW534Xx1s21S4IrHRuQGoVp3GSh/CtxywN2Pic4K1s61ybfiAffxhlynF3wJuvhDO78hMuqEway8VnV+LY4kjNXnSN1SjM39nBqE0C4yQjXK4H6/Mg9e3Cmjge4CXIOuEK0Z6Fj5ztcQy0phSkHaxEWM3PxgaK5ynLbE9Ybvmlxt2jVWUF4X21NlR4WtaSfvyPWUysp52hdFyUzaa6BY/rK8jnLfzX1Mi8DoQZrg15zgQ3Jh+Talenfhr8OWLxQN1KunUDjF5qVz6llAzgnxkeza3OG0RO5hGJxGzS09uX8nuaCIRZUX2fDZHNOHmEYBZ2qAU6UV7qBvzG/QQWGLlHRb+XXTHOKnDdDJPoju36dhZMOATXE1Rm28WOqf6OrNzULHfpgVX9IY7Kv49UVCAy6Qdow2NdgKYOf9PSed1Zo2nJp/BscdkEx0xMWN/kdHwzGF+7zGglKy3Hn2Gz7cEuLt4C2vRWTnPFF929qn4GHeq4ZLfYwBBvmkuB7P0w9nX467JsPHDUn+bsNrhaazEnf8aOnslI8ldcyXRywVV4XIRv1H13KfxGr75ZpyloYZxbqSXvA753HIjVSgE2udXqSxwEC9IKoInwfeuZG6oQk2hHa7grHm6RCOpW5NGezSk1ps1ilhtyeSO//2KjI1xcR5pDioFRUQoXxUJBAFQFaGbJTVS7JRmZMWpXEggq0TCLNn3o8BIdV08WoXDcg4vwUcd25mkF8c1cQ6T5cEdSuKG5XowEadln5AGCpPKZwEbT3PIYn0M1fyESJc6i+i6PQLt5Vcpl9NDUYfaDPWIt2oaKdIt/ks8PRL327w425wyPkL+Z6CO7W8rQqYjuSV6zTlDjVZMMB4CulKkaYcBDVRMC0uLwMxpkZu06C+6D5xZsIueqW5XM6x5IhxXnfuFNYUT/SOaTA4Kbjccs1+hqEnCANSHU81H5OPJax5CuokOLQ7aeA7lnCY8RbgsnSG0kzi0eilqOi2rUILVeRR3IBhMWLOwQRuX8aESdgFxu+D7npvwQTNd4XcoyKBvDpnyGnNsvhcADdC70cI+xtafmoCFFBB5p34JEllKqvxdJT/2j1ug+TOgjQjyIQ5AiFQ++KLZ2DqFmQuIz5o64BinhZR+Y0g46ExJXWCWmfrOjPqZm+boQ5tcvC5jtoAzw7HdkrBX7dSvFOaRCN0gXccA5kxZa1PpZorE1Ld8SdLseFm9sTcXV/48toF/yh10YVWkjqtexoumephnb2yDJqb56dqXwzLWJF1fB6B+4IS/40WMLOMaaLf14o69Igo5r5Bdej0SRLCZhLavW8eWqygCFAsoJLMjO3rdRRkbfEOw0nGx+7S5wjQR7YxstCOgZIsqk+bmA8GA864K52Hu/DYOZBD8Yl6ITt/f0CjFvHG92JQu4UARMkAA5UhJT0NJ1JRBsBQHSV6IZ7wRbPdw0sySfY+H+SKkwscwko6xDS2M89onJ5FXY/AP72NKhXIeu02fc0yNrXSGKS7NBm4XVQgWXqPe61UmK6TZbqTrqLkbVANFplFnUQOL/ZLnVDj1A7ZyFwVQ4ewbmFQevHpsJE5bBL93Al3mB2lUaPEU2zDzeBDatkkv7oEk3litO7qF6okBkD0UiiXRHNX6A66DBnSOdE1ef22e0PbsG6Fdo5Q6wYJUzobG6wGxP2ZAwjc4jHgPeQOxGyAgdDYMgMnJZsS4uWRDQpE6deiGMJ/Eysyi8z9AEu1VQDqqDcLw/O5x2k89WT0tai9HVlvdO4eeTSnvLXsuiz2jwmJXRoc5zn92xpNv6ee/Abc+yN7Wg0rzx3nK/oINitu0/hGIMh9oLTWtOYie1BqrgGoinVTu9VXREXtSms7qxofv8G0TvseP7yr3HKe6UnhX5s9jVrfVVEugsWCTFWIWlQ2B2zvLc9hc4mAUKQhIOMhIukR+99QuSnyyUvgC9BEQC60HOtMXAn0lDE7BctKXnRB1oJw8yb9vyg13VIMQWB70fqmO+aV5EOsWDPCgSJH6mMcyy0pSLuNEos0hSeHWj+uZCQckahiBHomP6RjFRgavEaa/SWK21LkvfFPqKU+e5WuBZlzao5t5I/v92PQDLYBLwWPXa1zIIkz9UwsEFW9uf2yjVcAb/4atXaoebNZ5kma+5xXVc7Vq/hSml1oLHaTEUgBtTb2Pqbm+zsk/KbxoUKcHDi3ZiPbdmewA4yHRAhks/QLVdDKqmM0fxVsPoDV8ejBUcQ7hQ5jAFB7Y6r4okdx62dV1kuwzdDQsVEcxTH9M4a9yiPWbR0uBjpy8ach4WsF41bNhA4mtQUpfU+V8I81h3UVLj0sX8OhUoUwW9UnnIDsfOrqpHufxVxE15JgF0s5hKPtkDJhpohKrf8Lxp8dFTEFOPQtzZviWFNS8OlZpiaWsSf6rjYn0MuMeaHJhlHUTHlkJ5pC8Di57bqPZDDxShXMMiJeJYbyCKEqflo+PywsRnjPgOLRYqmVADtC5/YBz2sFjWjAkWwbFmXugQ0T0oLE4HPiUepKwV9fgALd75Ckh/hdHk+rZEzW1Jw1bk6MDT22iqlklJPAW9gcGnYzYcUqPY4HoGHDQifJjNPG+cmVDeh8SGz00kegF7ma7UCJZ0bB+ubQXKCzvoXIq1ZNnc+FS27+KmkWgXtOZYfKTobnaH2i49WvarwHWVV4vHLnTIA2DkvH6dzR3AL7Tq9OrZWDSp3lkhp4TBM9mkSW5CQBHbKi+G1BThZFqdrfWrQn1QEMpsTl8JIwDGiVx34J+IVu582lxvp/euAd4sjFRnT2LjMzEIfGvQVSyjjrH3jXjro+61V61dWolB1sPWAt4AoIo57TcABMAHvxfDDWAP57cY5pU3CUCe8Y+YeVjO2sLAeh2/Sk8hXJe2WAtV4dkaFXzGbqG3a3+wDiLNFXAjXOKjz5UR4OY82lqcIo1Ls/AiGXP55Ngr5bHd1YXRsBjKqADpW50DARuez/hkJu1nFIBCqVRFSBx/ybgvyRPHa0jJdbeFcg4g2deoHt00DoAAdtGHNatPhsjqIkw2Iu52UVBCsWXElUTuoU27SWGrKUjbtgNqkbCK2y+/nnNnYzmxe0l5q0lSmmLrfVrhtsoYCdJG9O95o1V+eYR3kuQAS9sS98wWcGwTVlJaF0uKP72MjoLYvE7F9kcYdt6zCsn0FUNukjMD30FWa7pOIT0v/HwTAYIJHKoz96ow0eJvAhwnvOuc/vINc5urbEGbNEWxnCCGqS5O+DtkjN6H6dsLbW+Hw5mpVyEmqAMXf9HX4BXCt+C4W5+cVknFhIdlWa79tFmZNPj+zGRK7rYAdfzAkUnXbnHJeDC2FOhgZkavEAyco6xo31GW8A/SJNXFqA2Cxwlhxj108j3RxH/nt45YpP4FVU6xB2HYYutTdAJSsoQx03qyVocnmAodwyveNq9ESHXMGRAExEH09GWfRNUuUZSv0fM8hIJ+65qNqI4ps+7KBU9l1OtpjunbMUlYCQuC6qPZlKsjkKKmbeeQcuxfJCDYq9LxEEEui2tn3SRE1SIhkoZowHNeuK+UVifnDyw2vnygzegeiuISAHsN2pmDhI+P3YaaXovhL84ap1uImlEi4QpNaA9ZJ2mUTWqFzmUjY+BgrekiMMHusFJ4VBBL8KDDQPa4J+FQ3/Lw9zca2G1K5Rn6Tcm2lGiVhC/MqqYNBkqulQw3cXSy9AgIzWhRhJFouUQMYCQ7zm2f3PFa4458N3Pw05yO697eW2TE5+5AaBnLTIVsZlE65OnE6v6TogqU7my8CBjf1MMEOPDwUjCEDgiqVajzii/F8anDHoYpTGOIhX2SrlmgAOBKnS3CMyZn9kUK87CpDFhQ+PFN9ZLKygcAU8jim14KmS52AHR9OJ8HRWw0fW8KIcfH4q+b6zeXZ8+Um6FeuXfGOjTyLZf4QY61tXwr20X+GArDGJRwuMjK5lT/r3ysV84O2xviHp9OS8tOLt1OwRdwVe5M9u2dx/s4SCYB/pwL98a0oNMci3SG4BYcpG3NXM5Tgn3uNB5Nj27+/d8ZS48EBv7pRHun/r9Wu6VAl4yE0eBsrhF1Ud7WyxX6KRzHaVh1IEk+28UigF5/1ShD7PXFctgsaVJIsN4MRJWHqyNFYSWRXNMmhNutV8Wubzn7wbpt0IG6TmHq5yKGeqrLKWugweMQpFOTyHuPJkf7IeciVP4i9d8Jr4LxLDrmuVlzIu+sxybgLZk/nyU0wpVqwG8IvojQAecMSm+dTs3m7tXShooWBz5SYVL1ewFX935laCDtF+gCPJ9LCh54jxi0+dYuEFykTIOV6fRm12NauDzYUo44L2l0IoiGwnPJoM7/+Qec836ZHQAukr3cxrImZhIrS9MWoEPCihYzwblSWQsKR5Tumhw2M3EDg9701lEXVwgG2A0QrsyyQH3XKyMRidXxUvULEewjvsASpbnTDeYqpGXb/lmyXrYRkhE63Lw2mczxCwx6zQGzS+krRRgqYY8m8ZAuFDQ/csFqxqJ2vwhPoDKHURgqIeuKN5Yj3X2LGe4dlY6aiG1R0MgqxaX0eqe1y/BrTdIx9gzH0/4pLy4LG3QP5cnvv0bSnmHvbllimmcDyCk9VSMKcJG6NfJa3a+4R703SsveSDORKGk9LsqYzUN6uj9+T2UNLvMl/AQ6hSPHRy/1wzrHbqyAp98b01sl5Ebarz74yBivkN9LNZwaeFEzcCxO/8Xy5uFv6Mw1V3r/HnFhbxOXnuDqme1UVY/s10DvDmXCwZ43BnHn7OPNEAKLEEa4Wwzos7jBn6iI1sJzpXpo/mWReodAJZkH5V5ldgyTc0ICqlRqd+ZPpYZdHiY/Oq/HwcSQGIyZMu5twN+IzQTKe1WF+dz5T/BVvc2YqTEfN+/iZMqt4cz/zCIACizgQZHRClfBM/RkFuQniFPttrf0GBhRVNV09AyOgaR8ld+NKY84XFyXQi1+8bPMWzdsdmkYLsv/rDojGY6DKs9UCzJt3TTRNdkFvpGia2Lp7K5pMds5m6LZ4fAiQFaidhDfguPfaUThWtvIifthFOOzc6Xguo4BT7Yd5PJo9o5nPuwTysllGfgRMZU4zUmlRErPzZZx8Ge3XhTGZiTK9W0T/q5Wl5fNtTmb36LmdImOVZp0CmGUSQZk1HQA54gbGDtUo2wUK5suBqVe5PaICcxTTM26FveSblPTwsQs0wwhHn+4dLibzmF177DhTUHbE2uXhMCY+5K3sgdXGkhtLAaoI4VTsXvC/T1XLwLC8ugBri04WAmAQI+0yMKKf7f7d3Yt5fWTtrvzRuQu3qK0PSWvorOlA59xX19BfqDuy4W+OlHXJ+7kigcGm3DoUILR3h94xDQIqo65xFqpE7FILF7DJceGCNY8UkdTx7PYnF4x5L8qfexmskVNnX3pLsoRusf0rcdPP7MpajYfJNZAbVNhVgAABfTgnPFWF1S01/Qz2IkNazUM2usIkiigcM3K/8Eaejr/XcLhBXw1oWhax3F42FW8WaRgD4k67Uz59lQGRB8dby/AADLU23S63iNVzb9W3DfyVfsr2woaCSBlGVKZEWLKRVSM+WF//C+savVuGJNbF/52wJtazsC0cKicXcanKmTa1woFLe5OxDFKP4X/tudReHpplLJ9BWc6jKN4WaA/xQxos6/ajQ39IJspNsRJo6AxKv1lC59FePJsdnlC7ZlEDu1oc3dhsyIdsMTiYpYGy2bx1EXFr6A36x7N1RUAHxeZMDgPhD5LfHEq4Iqcrf413IwRLdhkwyc01/3SzDf/Hq9bp5DvfnXjkbNu5uwvNIuMJcoz7kA+lk6HvuLfLMh6a+26BeF/PuIbjYsZbBD8L2qJ8lGDfCVmIMtEKGAmagzVYB1psnR/mpyfbaXpJ5OHnHDaqcZ9bZKWRe3GUjvGyFrYLMsBiV1piVPDD5irNpzlaffAJhl1199b7gzg0S9SMWfBCp1oYD7ZyhSREr4M7ywDvvwfn3p62kJMFlMHi3trekjlbcjGD0EULrjNrLtMXrstxglJLqneEbtddtuZTQa/WMGK0/NpY5vXN23sGhI9dg8PcON/VUbN8E1TtKRDj/RkCqQkVMTwKZzWHmQvc68aloSq3047PMOot25ZUzsjapHKP2OpeqKljuzZsiCV+uxea42LYhl3tWFl9R34h/tiD+siWffsdf0LsvFpMdus0LlzPHeRitDNU1uuThIZnwL/dZRyoJC3WLIHzdgyC9bc/V85pHU12KKlndDWDeWQqupGY6MbWFu62NqLVJow9HBFkzT9KBwYkVEkSxvqb8QM5YuAjikmGD4m54JrV1Pf6SEeKGNWtqizUUUsddvfnyUVRRfm+E5BwEPsHJJOjiMEIJIvF8BX2S77eqoWXIps5o8EIC0RamNa39iAYe4WqMV6PkrhLZQ9/VoQFtwdnVOtgqByH1Ju+L8r3K/9spUgSJlKgvWkrk6Y+7ha2nh6pjLfxS5rk50k+zdqY5ykKAzHnaRYOJbSQfnP7C4Urms7XmMBZeVt1qUuN/oH7cAmagEoMEsNdkBMnctGOR4scQaYE/9Czl8enFJC7qqr1OXAkHOaDcjEhGx8a8iUPM7YgZQVznVfGnwy/ETZKxESlldrMGH6mxhZLUfI5w45sumNCWdu7NKzCM4aIOGrzVFlshFBWyuLs3DWYBc0G0Mt13BbM5ltURNBnbVwQI5Pc//n4cnh1WFH+1HM3QVtkuhgM3KwmLfF3qhOTeyMa0fwGIwJwEZqMAWy+mcfEgpm9Qs+gGQFdABRqmFYmvIJMPE0a1a/cKrjZFh10q6DVwORUy5Q9neql57eRK1aTBnV2hk9edN6zOFlJqq2iq/HcAGY3obKwTd2vaHRQRV6DHJzaMsYkbef018yN7tpDqAQspVryWeV4ZivFc4wXRnVagWBg2zArE879PyK5I9gtT1uEY+41BXsJMCM69TiqAQOU8YjWmBFYeAxRYTtIY5RTDY0mPKr8vy6JMbR7m8/oLK6YVxk7QslB8ghocasjYp7hgCoMS/TGgQAI0q9kTXerO0LJlqhp6BwMiVpdfLCcdAUcr69HzKnsE3pB2YlbewRtLDSwcN4gcTpl5G37B1hh3+PEzdiO90pAaEl/s6Y/gZX1tbIyg/qqoDfgX5RMuWcgKIJKV9OQLmv4HuO4SuVa5iBC5Dr8yjxFHe0UZgmcgixqQZZMlKWMMYdvkWv4VBtVeSqT+tMJsIFn0Zb3fOJiG8nTa06XJ9CXKQ5wUS04ZDr+Sj2HOtwJkwC39mcuUinsi7xJ6CCmQV/2qNwKPFCPzmWVPwMFhFm7HJguSwgWW4gkLln55eg7lUOh2wxVs3qqjFSNfcLd2wpMg+k/Z68ANbhux2VvW3MUROf9YYsA7/0gKsbIJ1oSTz1GbIAvpjOu7P8jb2PwYVJH2cNYXr/kQXIlpVFg+Y7oyWmAQqQoWNwa6jX2J60h1ga7nu8sHEAPu3FWF5ZktnC0vwIycHeolzR7r8a03FbaQOTcJmJv53TAguHpccTmN4cUDb8jANOM7vTjKvo8mUMTowMRZi/nO3W2XRh9lQqSFAeDCbfKukNCLBRjkKTFAow2X8L4NkW+IJ+GKF5FgUPEGnIQMQgEhBommFkBUgg6QQ8H0Pb3u+7QPdNBWTr1HlamHL2rnokVdpW7CQevh8d3fiSaRB2xOu2yw2XTrVtJQc7PA4HueXY4qjOTU59j5QEh3xWf9gkqUqGwY6UsdK0A9FXJSxw8ydmXNAzX4LwRmwi9q2yrFteBD4PkD/SOGPX53cP2s+cbr2emMbXMiyCnF3kFerILiehALLRyzpP/ocOLghjDpJT3tJoDppqXLKm1PrTL2nVbGhAB7yRWYRObYRwDnGtgkdrrMLtgY5H5TTk4Q6fKXBXnJzUI7AY1piJ1dZF6JZHIaa56KE1t/wtTj7wrYn/YBahjbKnvx3nHlfZxBMn8XPufCHYNX23H3SQ89fo/705BmzjwoBX3gV/l2jLtgesxCzFMm8aTrhK1T0wVrm1c1EQAZiLPolPeYCWyVKuurrn4FbFwHHCtVlGw4LoNZqkTFUdSsnQj3q6hC2cTRRjP3pqux4PZtPGs3ulHNJtMwTF99vVk+6HCQO4+RgzPAS/KNbwJk9bK0ilHpChOHkiq6Ic4zg9XrmiTSt6xPQkUNbCaQoTih+LDWK1nKkXsITYokI1a0aP9S+0DqAIOAGtSoyV2CBecniebI6ncRmpNC7NhFHaPqbcoikkN+3sOR6dhM1G5wNlEi3AixaaQPaCZrSX2Hdz01p45GNi/wCnusU8/hpNwRlNba7HVpkT6yAOmiKMMmBi+uyQ7OUtEMob3WwABsj2MtFtuzBDLJrJSik1X7CjxmtHWSwJ1znvpZbbRdW+TT1g6bqFl8f3B18MdwM5r4+Yp19wilYpNdX+QTbCVOQToSvxug6hM9Ws30Ra1q7U9mupX2EI5tiPt+t5gLWJDKg3DsfRhaAeGb6G+MgEmnj4cKqUPh12DNt8MIXMqHW9R0ke3jW0HcQOeRTYnBOY5R513QrV7cefEOtf7QgyG3hLqUN9HG4h1JYpPayii2NspIqvgLJXzpmBAwOtVYAw67mTG+IWvc1sVWYacW+9XFR6tqPdhckeHpglCjt847Q7GFFLYRqAuq0Jn0lugCDWybgj8yWIFg8cneiNNYfOs6nIOuIHXJK9ZPhZpzM5rteUd4lrAIpnPRAS2JvUPPgjz7zqgW4xySYzFQcllGUVvoO5pH5Fch7g1gHlpg37oX9ZNROhhTSZZSwO72pSQ7W+wt2pNQF5/E97TchZcnJ76EgAVIA/LUPImfZTepRzKqi8AmWRdhiLpoDU9CkXergZKWDt0OjiD/Zyv1PGs2akwRV7p5bCulSuS2X9i7RLcEve2u8lEQy4aTw/snT/52dJWoAHDNkPoXzFdIyYfvKPF1r7Q2VtYA9sLx2R+fNVTcHo31/fljv1Z9zGtg3mC58JHjvD9wzRaEQc8eEdH6ZILsxLMsF5WP35674c3oWqkHxUN9ApSlclGmBxp3v+X7Gwoet0gkzPyM4DmIETefmOM8rf/I1SK7OTKZ/reWttwf6rc+M7sj89ZKDTxWmX8ne6SHJddP8TUMnvtEPhIGIWHLbeXM0QPrauIRU4NVFKg5WDRVcvkG8q4x6vY3Y3wgPpPVZ/sGz/XQUuDYIa2S2LlL2z66WNu0r0lJBD7GmYgTzGVDmN2qIlbAdjr6xte0s4dUOl33DcmrPh7lSV9cIWha3G5WRSfQJKRfwekiPUrvBR/v12xZhlJLWfXIulgJLLw3y4+upAuJz1R8CspNGlDN9CZ1QYOKLkloRm4c0ziuYvmuC82rEMdZmMOMS2TnfKM28TAIsh1f/0K+ERVP3aYRy8Pp6Z3yaYCubmkp/rclfRuCvvGa3y08oVOvOQLM4asUudRWD7c0r1t8N3WH3TqQqpNbY4xclXaFVZzk5Asbjt2PA7fcaqAmChoWQgotVnqjM7MbS62+CN+y9G9eEhycVbnhOl8j4SBR9cCRj2dW6v8B3I1QuS24qCbQOds+u70q6DhvtulnIShVhfp+ryWzKtJN2EcrVoXBhGPzlohh7OFx2M3/bD1sTacHoobfxqiIEUIS1eibbi7j3QeFrh7/jjXRDcPNpahvb+/WRi6axIfvTuIYiFJDJ5t7Ey0++BucTpzFeW4FOicCJ0F4bEe1T4vcqh7P01tJJzelA9R+d0j3AKwEQnsRDiW0iDOJ7DfEcUyUZTR1y1hV+Pr9vEX2ft3I9vDEn4SvK15dYJ1iCO+8TYn9/ftGLoY5sBZKrq8RaA92sFvVgiFHFuEAWC5ukp9V6pwTddegqIU5cnDSMZDQ/EOp1ufhMyCrKGW5lp1+DMGGp1NwX5GBcuLTCqsOkVBqAgLoOe4yu1y+dtw1dvdtoVImXtwkBdQzluvlAS4cRv8ABQlCHI1JNr4kzE76DITimAHiFv1j6fIOYOBslHUpZkb6w+rYg2BzcCnZ1DBcBUjLhmzPZw6jImpNHewD+Tio/TNzPnUBGa3PuklHSJljr34Jtv4gIXXcJTpCJwaI6E0H6yevUqvtceTUlCrG8Bv8DHs+G3u52hXGZXYTAtZAWruj+WYAfz3lZ/PoEaSTHl0ziVU9hp29BZeYS9hVEbiaI9nWhdcakP6IoOIksveCdjkoAbsicuygNH/QK7DggnQDgOJjr6CqL28tIdgCiAGij7hp8hBdHiVZCOwvndwVL8/fq7X0eAcSrfm7rpCjHWHNtl7Z79B7oKjDu297zZhjWJwD3l1TYNaIdgRPJx6GFVupZc03K1ouk9qYyXy3GZ5czG4Hw2eTy3hYqXwsAg9lcgAM9YlqaW8FufuiWRKCIHx8VhtOxrHMKyjgtAGZNUAOf+3iSjs6+uhVbH3EOR6wmeZhXhjEh8D8LjU0k+Fx2YAENn4A8gEh/RLjUF5IZsZj8y+VQBP5R5FxOqofFMjt6pgPc5LaNs7OKHobFeaamT1ndKeKqkFTUOXEGwpQa2wzeBE8i/cl+BDtpLphJvKH3U/HZWSaWpgMhANZxcrp2IDKAShFETatiqRKEmRc36XtG3UUjxJNBapMQVF0kJq/3BUZmqq5mHFn81M8djj0DBz3E9oKbMMnAwSywctcr/Mo0QBFQNr+oVMbcW6ZeIk7Nnnj4VM7rZwK6AMHJHUpm73UtUEjexwL2zBtGx344jj5rIiElWwZGnIJlQbxbJCd6kNSLk9OcrStRByqngBfeQOpFJEADDTJtZ4oqFVWmvFw2yGnQSdCrr/3Ngs0Ks3Epkt20wWt8dB0lT0YvDqwsUZ9M9hnB6MPXSEu7SoTCPvwdoh0hO5TFjQsiEyeYel3/r9XyPNH9VcogFbtxrFQ2dH0Y0L3ZbfjaEFZrJJsvrJc4/pfEp2lVnHW2SYFMNZAPpSushfOWVYeNGrBmFfxDPVP0RKY4G1Up+vQEV4NX0PaGY0npyWfZO93CTumpyZxp0gpTu0xVx49ub5ZEUV4Or/DKOyZIJT+q1eq8A+6V/1oNyqpfBmWem6E6LOYP9ipS3J7qTkai0HTylFyik/nYuZ3MLM0qCuIktpt0K7a2cFQCPLBJ8thvfrjd4Bo4ydS9dwYFihI9kfA0Su9hHvhLauaWyQBYGY9E56HNDQBXvPV9Qvr4l8JjnZoppIQE8Qd7IbAgFoDhggUNvvfXiknUJ+qzJDs/sOyD3t0SORt9ZMhZlT4wh9JMAQgU5uZLBzD7f3OgD+PS0/kysVjfpS8EKI0kqlbVJCSQCx9gTSVGQQUVWAxzoI36U0G2bvRKAuPZ5zYQQL0GJKuezgJi2NVPcTPfJgpU8TGMlFnVZOl/MiW4gT045MOGZIUouGtJeqJ9kZSaUmvUO0TS13n9+G0bZvsXtyzpeyEzW6PBib4UQZr2YmtNY0IVl3HahGlqhADMPNaMQ97uqkbEEWHkzHbv0tjitzeKoZggM2M+rIk8xnyoKOxfV51sSoDswimd/nGaKEuQN2EAEREmtiEWgvuXQMMjVk6Bv2YPKbdtX9xYkZpMfXAqwe1oIrnnnOIhmQZxtg0TDsEPDg1k8Pa2QNk9Si1hvNmZQBZmyldBuxR6ShxgRvQCokvaWp/77j6W6zXldehhvvjVfEYoDdiCJnAYLC0TQZvBl4Ycpfi1ObUoMQdAMkM4fYygAQmoPZ7gSKS2RYXNOpSJoWER+F7QLjDhPotU33aKdOUHFmxUTcWvuBS+odr6onZ+ekdG0F0tBVeyqlfUjwQu+6fSFlizsUW3jk+KtehnZMWVTG+vOgVoSRT7tXK4WB20qml+fMbVSJJqld6pwUDmtPGaxrB46J7SNL2QCa3AlTxkRWB9MwSptHm53rhtssOCkgkFlmnOoku4LNf5mvY46PV4pzvAfacTwC90GrziCTKMECq3bqHzSdHy8FukSIk5lcVBqD2P32x/6ROBrtC2wmpAXkjwiokgKvV4EmUpOTXsjx49Xa9IH4EM4stGlDC/ws21w/N2MYnAacq/piGjdchMEP4OPFkMsVZAn4YQ5NpxyWxO37CIdr3x1VzY0uIu9g0XBRVDPU3BnQiL6CSixPhGJRhmINXZMeFpE8Nd5XcCbkPAmnziAor+e3YU2JpVI1NJ1b3sCay5axL2vW3D1H8nowIwPCIAjCpzfovEx6mbOMYlf8vXtepDV/YQD2avHO1HXfYFDqrymIIbhK1+a7Ra9aXaM5vdOKhV/KtLIykf03NfBJcAh8rW28NShrN4elNMMfRsPlYthX8H/QQQuPy/HvfrFFKsf1xIt7R/w+iRObTlL3gcici6AzvtdXW6BB3RkK1J2gPx08sD3gNSGCWIPbhJpjylPvHJUA1HB2+INjvGXV9pF18FBL5aQJLGOU0AfI70t1SKM7ULZcWMd4k3Ov6g2lAJuSqDcWAjlNMG0R2vxwJ70ndlfPFLd1Ffy6Hve/lP+jWiQcAhWSUS04HreJ0qQWMYeI2SnvwjD/ukf9sf/X/JIuBPtDSAJmaxlR1HEIC3aRCDvD8uI6hpZcEnHvEHJtGUzAlxpUjABChSYiMwMLwyk0IjWjCQOQJn7xxtAlKPAdIvey4bjiD6T4fnkN6JfucN9UhYgyYPCCV52wN7gp/NlBYgBgqcOW8K1pjk9QF8YtcjNL9vJcnMVVgmNqTBYpX9EIFqwJNtYdQoELZf1Bvk1HNhg3dXo7llbIBSd8btCyBBadKHNzYIOZklEbYZp70Nw2rihL55C62H0RXGVvnzOmF5TcAkQzachgOmzSsjgsZcGwUVkF8uKkLBfICgcdOVHgXFqSStSdlB1T8JuxwvSqaBZsX4+3jCOxaoN3sYy5Yuq/X486E9CU3sXXfnFL0Nr7zSZ2wJ8x+b+lIH36/QxqrJpcd/nbgKnhbwv+Bhc144vQdecU/taIOG+Clo1Bp3Nn6+RBVxAjKt28YTXIj+2MaZaqvMSTPUUydWL4muF6ShGyN/RXGH4LoY+jc27ZKzL58oV3fk9mQUHDUlPn3C8DPzfR3psX4GmV3unjPu+itds/7TRa6ACnH0tTXIpJes5E2ehC3QqN5e/BXKjQa0mY4uy3HMamRmUi79JItiTz6b0OC2AgOCuQF3NIuZdml+4cBHgzXAfDwQKt1EOoMIGenWjvaKiHmHsKC2gD/VCGCFZIhYSCDWxpYMGA9LqfNdEHZpwg9TndVIWkEQNAVjcnQzyRAUePuodS+hiQQIYeINCHzJ9PqT+R2sYpY1C1bgUhpKxyMJ9icyYzesgCG3mTR+QOh7nG71IL0+pRMCm6feQ3b+uGwiazhd1Mj7JvkA4n2ZHo+fG0ARMAVefMA3/+InwsueMRKAtrVzf5hsSh40nKO87FoZmhzYTMXNhQaIQ4QEupECJYQuQ/Sp8+LLD0iOGApU9+2vR+UlNVRZOE+gGgn0fF2SMqdjnDZO7llJhBhfz3g3bzNZDj1Ib0dYAUh1nSwnPS792y4R/uazENYCnR31VPZ2SuHwcjKJY9B4wB7C5KGWVAfyo8jtovwoq4EZIBUSZRrohQIRWkdOikn4Y8jd1flu5bjR1bWab8ZC2nQuPEyaQq6EaFRpWlAsJoPslRSh2e5LVkdtU66J4zIGpz5ApgHMU4MiHRlbZ379CmeNlacteJkTCvoBeTiWkh8SYVxKEANKvZEkSuxorO+PzXknIrd4NSkNFS218aFfckV+nwxo17Ne52s3wV3rRvVgIcIAuLAslxvHlPjR7u7FJ+mQ9ihrbuMEj0tLVLkBMpKad7oaRFoWNfDjGkY8N5ywzGeslg5XaICC1ER9wo43YWPBcYRfL8dSK7a4bFdCNTdStSRoJHt/OrNNoNajE2fPpKjbb0rUnV4HadvVAFKV1znQBfdgNPKKA16gaYWD/LVRCL5UyJzCdyKwAnkinWg+AOsXqwujq3a3JRY1e8xljEloYJb2cLjLSxN94+RMrCNbVZY4mE43R/219BFfa+0s7nle6y//tkeAxmBKEBpZx9/9t7hOmFp+0B2XI1HietWOoePji+i2HhvpxwyQr985C5tOrMxzPcZ238qCgN8Ez8EpMTFl0N6Z2ez2g9LZ/K1m19CksaPtGq1MayZr7d+Anfio7Iwj4zyDpND980YDZ8WmQjgVuByoUDi0uQM+6P3d8e7zBRqKy1bSgdY1f5fSK5z5LBvlV6LnNMko1LGBkuzH4/7eAnWygHNsLAq0cLWGNIAEpNXFyTSBVywn5CMk2As2l9/MuMqomczJW4q78QqUqB96eGXJ9pTwnRKNCltO6UVJrNBu6VZj4s47XxrmPlYmsqYCPSK6yxLgUlrqSSUdY3vWFe9pcBIanT20i6jqNqqbEAXLgDiWCUXHfjL9amHs/Q3EXUx1BnpBZTPv1aAsUJyjFstllyHvHpNJI/TrfeODF2tvCHrVvBsXVOhQnTudfHQk5u5U1qxi4brs72zEteMNHoYjups0IqUt/QmTLdGZJe1CfpX9fAxy98O8iRxguNe7lIX54ui6+E/Nsg8XFrI961pA0CrlvXAaKi5WVaV6oO98V4I81sqC2IMVPRKr+Qba77sTf4LnSU3+E5Wa5NhAMzoesJZPXUJLnhUOpxlsa/LWH8Nea54MoCo8B5fq1s0bP/qky4HnrIwwlYGcYYocg9V82AmdCdCoVevoB5xTwECcjU2Q5ArgsQWqhhg3mdCPKywJTZLCv3R57JEC+T3desfFi+2HAcUGg5VHuo1RIhGvYkYX8yc7Q5T5lFO4ZOobFiZy3AjsPYo1z1AEVes+DPAmSNYG0BcBItzWFc3RWXIPahvhaspjX7vidi8iQg9JtF1pvGiYZMk4OjRV07XgExHZKoxWVfCFO2uKxYHrf9PCGYnlcGrqqFwXZ/PywJ6BoS2yumjmnbxNWfAg3WNu+JMSE1mIyWFoic9gFsaEtAodZBNspABbY4USH3BF6oylQWFWfxPX5z7RSHTjiSwj4XJETN/R4I1NDvj4G1i0TgIHSIXY0WWk8ZkYver81E3sc4N3+SIQUGYwXNhMtNtGBNqFKMvrQLSKA8w55BAxpjHocAypvOSKbq4Zlim5d5sKcwACQRGWhySWaUSYpHNrq0sTGRLjfAswnFTION1G7lI1uHRytA3VM8uIv2Ug4d/IVRl2wglJjV9JyYQx+RBvFaZEfDmGnO8HKYnaI5Th8LWfKYg/fmyFQ8WYvuKtNq53cqC0ZOIGHMFvrLOXZCsdip5bLx6I5WfdEU2qRRV55Mq3MeMDxm7+nNT7gUUprelTIXsSwfPt2YRy1RV2DKGe3AUFNNQL/MZCEF/rqoj3yHC+UK51HDFsgmoFDEH0BffEUvmoZHYI4ZwwNJSZwAYIpeQwGj1USJnpKCgp0Ln4LQh3LFcwaD6Fbi2weIa2+yvkI6JupK9ZPL1wJOi5MIho95S6BzdSIOfwns/FBH4sYcSYbpmgJ9njLjISMQ9DbsqPtbOPJhGOtBd6w0mNdv78qkrCQCvDQludYiZWpM+OgC6R0hFffrF7dpcNSg3sQwhJIv4/7ybrFUqcTcWRQHdOtCo/qkBlm6eBjMkd8/fyWo9KCa66+3IfQMvcACApZahI3EpHgwVmLq1nVTs0IFFXDf7pHoR1epidRqfkgzLnClgYKnPjT5PwKmIzdh1AkfrcnonxemtsEgZd7yKzuneCkIWlQIEx4Y9eIy9Yfni+eVgLFSbwXhpns0ORrMcRBs9BTRpCEzRBF83JGNSjDlbo/RUZ6ujX8PeDdSCZT6KJbIQD0G+ezbeTf3vLQq0NJmWrH5Zk/5m5bDY3ZtCZprsFozp+jdtWYqfz/IGYtiWSLP4vHLswv5qvuQG2Pao0nvR905JeIZCGffAcmPfgYEOSrmiAhrAI92YVj63+7N2QSIYZGzuMKZAYUsK1Va0TEHdesJEkTFsUIlzJAzEI+iEERMXkIiQjqllAZsxRNk5yhKwgqkHvIISl30j8B4ufKcLx+FpuATBlK8aNsUNzFNkVtuyE+9zPAja+TarGnaHTfOT5tyl1M3rVk9X/Xs2gb9eDRALA443RzHfKqOwiOatoD7FVmo6VukDRI/X6JuasoCrqhmZlDZV8jHZrK7JCe8UfhodgMOXnQThBmOLOvxww0Iuzi6Bdq4iX4uH8Sd80PJdP1Z/wkMmBfZNWZd9KFPKpIQfqxpUC8Ny6f06S3RYG+myNemOe0nTsoa9qqFU/rj0t8VDZMS/mYtWVDuwWXkSp7BTdW/IkoZKpRUtEpBMokel4C4kexBf0HPRA77JLcfQ7x297Hri4N7S3CuixO+Jsa/iEpvurO3UvJ13nQ+GIRafVc2OZWd9KzDWMrACtJ4gAMNMiK+p0J5dOmcjRUlsU1ppBQEPC20eBm3gRLNV/zTCVEDNFZdp44NC9VsAtJUIALXdVnZW6WxaUhGhypvFD1YQORWGJ0N0Hqqsq7xnODEF60KL7nLvkS/sCRSdL25dQHknW2OM361fSKV4KWTXCRHAwdsF7uTEkSkAkY6GjQoPGczuZowOhp26F2nawdS0zoEgmSnaZrO6FDYSy4KutrG1IIXXDOLyK5XHXCF6njfdlGfwgsZx53MjkJmkrnkKblKXVyrUw0x59/LmarS8vaJyH0cLWfPI0oi7N7s0FleJohWY0cLWrMqx2kYNPQwv0QFn/xTLA9ZRZjE20XjuaFYQPFLMnES0hXFyxFUWJdyo+L7rHzp+QpJLM2RrGAUB8dGa4aD3hXkEEH/oTua3I4Joo/UBaaGsFXgZkGMQ7zNRSSa4VSu9riodZ07jOqYAGU7PnR1KScpW6a2wFDdsBHwaEZhmLKIKSTkOy2CLMbAIVc4KLQia5Uj6ECEibkUalerHXepOFIrrY+w74nrKQSjMXoPsbP4aT9PyHY6MKmqqquY84FT1zC2LNqas4ZbNpKAHsqa5shy5WFWCioiCjZrt8m9I7cBcB8u7271dfcoDtUUGawWkvO67DASCaReMqg5UkEO5DqNN0ejE9gJt9ADBfqtDMNXVkER3tNOMaAcT86KiTiSiEGiTRj2BAGc+VW4ogd/ZDp1KTJPgK13Hh540UBzB4/9/b38H6n8+cyUKBJ+YUoqEJNKuWCs4CUPPk2tkoiwMtWyzW64kMb1TthF8cvRKnKAPOpc3E5lQ1gQI2cceORWMRMDxeUk5Cw3fL398cy8gOaiJ+h09UrHABLH6G8X0ZM9HRQFBOlDafOod84CkzG3gmpy8MSWp2EvELCGxpK3/ntS8ivxYza/jH6szNKeSRgPya7Maiqbmiw+y69DCt61VB5GDngq1H2p4TEhKEaPWwhMDHxRFBoaEsIS9XgoilgZVCmeGOGWK8Tq8Yi4o+WbXhF37ksyANRD4JSb6f+5zH5aBrNQtmwSsTWqO6TKcMmgRBNKjtraKXEYx8m+iwnRi+abAkB36mLespor3JVIny8Z55dXESbJlU4XQbjdDdef6Hha7APNaxbmfmiecFIYNijLF0lIOD8e2NvIERfdiKWSuCNCtN1pq3wB9fzpEEoRXFBpQfSt4GaNoihYilPH3DviYQ6qmpHhflyrNpVNc0Oa1OBQp12RXR0jJ/KX7LDu+ol6mmWbhNh5svN80aGE5UgA96iulO4HYYvbcs6VviPKEls4N+u9aKNyw/TfYH0wk5xrukq4zvCWksg7tb5z0191izGLnyDMBX4jErKVVVJGbeJvhGX4r8GS/4s4SiXAd9d6/G8DtbGuJSm2dj3bJkQCt3Jq+jA1c6LOYcItnv5ckbD17TymiZh4bvhMd/TfGApACxAQXjnetAqpqCFQ0nknOUHiJIkI/gq+yPg5U/gu3kPVoyAt5J/EN7fUi7YRO2GpO5sy5lZVbJ+qCqTK50CUgWecLi3UfZC6vlyiwP9I9T/VdPP9Z78JuYD6DuieEo0tOg5FI/bWbIlrdAANAfSZBOLh0LEZOTYCm13lagp/hI2dGOfkpiWGIYpzW+RCCQeSkG4/MCYfLJR26xmorcxcqstcS5iYe45ckmHH3fb+UERrRq0wm5+qmbG+sQxjFOInmuDTnpiEJRmjOU3ABG52q4aBSupIb5YOYLHtqCOehRaNj0qJnsF2hvN4JxjR/Mgd4zV4GtUT5KlbLE9dWvoOXfUCS2CMjv9JsOCUPjDxAf28aZx7bcNzaYYq/XqPoD801zoIzZeWDFRN66SVpz6Ikr1Gi0uuJCJse1WjMFDSTjNCXOJRAcO5j8RwLlaqOK3KF7mQs1gLpYcuA/3v8tA1fZbwBHKnBOVPP3TV8+zmUHuNEnFh1V/7Khv+mKd6TmnfAkHISGHLnUjZ42JwqaDqMNsVi0JH53boxfEO2INTI7aMm2nTWQpKhNurHx8zBF3nTw2hAx0aipw6zn5Ho6sU46BpCOitSGt3qS1UxT9oGcdpIND07avo2rIvubHD/0P6A7pcGMSfZx/ZUVaDANMZdXCnCQYLgfDaAt+QKOQRScQixTwPXlU5GuvtjWySjvpGQ9NEjpHc+p8xLu5r30bSN4+8MZbNocgk4s2fJ9WxaUcB3DKIO6zwLUEN9GgMI8EgdoiBFYVNehxc09OEgBZ/UCVBlxVqegx7UpYOAkpyt7Gb9zwXYh1IgkE7D9sFjFZ18FrdJyi4JxrbVZ2BY9R98NWdWUo3wCbs6jiC88Q4ZhMq+OXYtTt7UM6SikwlNrpQzZ5D/lkL0ua42NqZDaZzyjKRRcHT2axjv6Enk5AW2WTm0NJkJ5nraKPQBUMCpDVuENiNKB5ykhm6qGIk7sCMfXHd/ozbK8S0PDJTYmGd5F3O/GJE96SzQEvBUS7XQZeDVALPD5t0lUWePVQ5bssI7M+3anYCggQJfOYXYzg0f/Lzy2HP11qoIUzpf1C0EKrZj3/lSIwIsAe9giagNbqm64OA1My4M7spQFC0hFkT/Sy4STqVLVkYBc1HIuL7Q+j4Zq5AFkyrYhXSVWNUFzy/cxLcprQxGuhcII2B+mrevJwmT3JaGJvPjmS1s50dz+lRElRsq08bspZJshd/Y+QJh26T0DSu1Ja4fxC6fPQ1bGGMoEMeXC86tNgixzq5gJ/pEPNe/BgO7BCU0kpHJefQONg3RNVnX/ilxHIjVPr80uxgiuAYWNab+vqF7N94G7Z1X9eiS+6FV/uxDsgQTMSbtMuI8UD6Vo6pVPC74/Qo+sWH/dshemEeKtUvWoAkmZ/2SFqlDi5yhrsOnJWI/ZK7GsbyFLQxnfuRfnBU8Y8d0JGxirC8yB/0wtOA3+nmI1Gk8hnhHv5t57iYRMr/IocnctYTqDRYzHk6rHqQjRN3oxFAmUu978TWE574KVIXsPoWm95VUFqAIQqYYwSkHgAAwKb3XYfOgqRxWXB095w2sZJ3ZboWcmncRqznWeCzGnMJe0k0DsVgzlDbvWT+fQkgi6pkgbvfPyCW0lUONUXv2tsXKs7nhQ1kb157d5Km9QkrU7da6r2duij9yAOpifcOJ2HiNvEfF93Khp0ufIRIxm1kl6D3xtFf+2X01QOCQOdtbRGv/x9FB4/uEU68C7XgKqvpLNdpLXT0chRls3dEdJx/gF8kgYZuf75R1Vddxj00LjPMakZJJ+NGg8nrzzwpmtFquJwmdmdeh7M6oLs5TpjB1tGCn3j81HPo6IxjMbcqXdmXO0tZjlELfSgHQ0m2uA+ceG7P0m+w1xvsnDaphsRkzA2y+NjsskJIvdShtxL/UmWmxMTExc4ajMoLNdTWE++QcVpO6PkvWJs9I3e+kGHkdx+WeQDbOUrqZ3J2CbBDIJM1TM6YzmJnrxuTqOitx7lZGSunGsHrWLCfAp8awqiIM2RXFXAUQYTldC9fWzO0PrBhPepSmX85hmFMd6M1TVbMc64/rT65b4Xdqqb6tk9ZgN8Wi3lXL0BtJ2TVOovImw9vrM7Vf11NiWCS4E/H7HLQ14dbhy7YS6OpUmyK3Enmm8+hIy+PK+5ZKG+2//1XsAtrA5HwbNdymWucvE+C1uBjIsAZDgnOBY2jBMU7aghqQUWqC4T+P4YbOyGITRcktihD1cUir8kiqzsJdRGsjlgfy4wCm7F/sBAY3WSWSX1nee2KINCAez/HMZQmhFizbLBYqKOV5eaSUnOGyWrYoswEs+5QoK0FZxfdgvT/iUPPiPH6JOoRYe8dI/ss0fBOF9QyGsKZeHgF1TsiwaviTlJZjzXC5v13sw4dmfnxcOm0hmWMC4zYYaBeznMP+bNaAxPr53BJNQNRFoNqn8TiH+PYVRLiFCvDO0GNbJvLaAWb7OZmd2PaXqHzrlLkohA2OnZ45gWKA+0jDGGsr5QPfmuR0rivkBKbtB37QpxyIJ64datHdl4UVTH8SHXj1MmAlXFLn0fDPgWjZNtKz1NbHlphpvLmOVJdgaxLTRI/m6WQNJENPZKcQ6Z5H8YGZemwDxASUOa4TEpSd0WnT4LNBjxMtRovYkVlGefOxRnKLqqoI2xOpRgxu+yFGCu2WDKwkVn5jhUhv2ATkDUz2+7xwUKKXR2GQeYn90m+VqqWA3iQqmEtBOzwfLeGiOcpMqXupEHipuQRO8TpgbHOTxDmMEKSXUI0Ii5CUBQkqD4MXwm5ZogZk4of8MsRs3LzyoROZ05DrymJdnyR86JpXZZOJdE8dCzPFuVRQJJJnNZ2pZzvgh7MU0sGz0QyuGBnr8HD4/FY0vjwPv8DTjz+XZPLZ1WGH/HW40py8aKULHJ6xCyM6w2yYVTkfIw9xnw6cJh2CX69vhGeDby57bxdH456bB24yLOw8HnNn4JrkM9GZp3Tnz0nD4Y4xZ7CH2i8yscDfYSUfZgD6AdkY00wZnk42SWr1eWhFy0LV7pEyGlsDTtHepu1pbzPmKMR+WRyFRpk2EQw4DsfwgZxyDq9wlNkIjef4BI/9w5T1pWlKgL5yR5e2iMxSVwpzOy35YyJNE1FKQU2D9+WBR7tWGozCjMOAF4PECz5i1kyWUnUsmQUbxjSZ78UO/hy8u9O6bxDMzuAUkUySZ5otrsITIeC1mQkQDyVS1VsZW/VVIUu0pk0ba6ou1RNYC1Yr9ilyBuP2jSZK2NZyN6VhuDRV9GDqjdwRS2DnBnQp5PqDJTquYsdskI3hGvRiZDQZuYIOiHNeeg6P0CaUSQVfGNuSOHe/6sgN9Q+YBaP4ocMwtWcg0ia4tLDGN6QlqHcCzMRac67EeWkkOeyqMdRIAnKw6mex5OnM+nuKfnaFFDankcKoGOgrH9Wx4TBDQto6tMpOx5M/opNjn7xbSG2qibI6pKR3rKEPvwsZqlTuyZX2rQj/KamRGd92Oh5q+o7KH3zENhVD1r/nZLmsdXrpUiKsu4WlO1Vn44nHTCaNu3l6ajpLL79sjmplP730wx9dnmIgfLQ2eFcsqXq48geUqkPhHYlL4jSoGNclTNbRgaUxyi6dPLTdV5O9P9McK7PSJQD5zUS4HXpo13Z5+eoeJY1fM1gWGbM3riuUiDpTYMdODsyZepGTUMe1+vvpReFwfXNfBhojmm5IK/1cJeSm85t9oIQARlg3+pVcNUqcATnQQHPYHWrkOZe2Pu7/AUHlHCLq0ETAMAhwJp7EHVuWfEnmcnggHhDDaFFTC3pwbfvU7bafp+btlAK82HRwFMKNrr1V1bwNww/Zcw1oKf7FIangjmOb61MoTCoPqJ4ehUF2rWdwpBjkkArD4VnEvhlENYC46rC11P0AfmAJXx20Qb77O5R0rF81ilRk7SxSwJTbrWdS3ARDGYJOpVz8ewuE2dDqxAkHXSlmb5ofXjeIgS9pVao2h4EUngAi6oDnhk7OkDNd85MC1cx6JmRQPcFh+tUKgz5QL6hNdfWSck5c/ljcDSFDyoYHm9/gd0mB8FDfeska7W+j/YMiNoYbPnnBfzno22L317gjWgyng6xJa/Ar8rQTbC/1R2XefBghMm1bpKdvM0kW0yXd7+UYKtM9YZ3xN/p1DFNUr0bMrz0HERubvvRjZzT7Ve/H5v4EYbw/l1YH553qjP28eso4IikaMDYHN3AhRXkuxLjH86skQ6oitiL4S/8eWk+ru2oB/xsmM36D2CZZDMjkMbCJzlMY2SngvZE3jfzP1QsacvO4vhBwbdjJcJ17cRnUSGlRIdO79pKUCx//7SiJ2wIhTUBmdrQlvCo971Kg0IeDJDh0a2Os156yCES6WjvGS1bncdyidd5M7XL2eMaSssiCD09kOPpk+vJ055vXClz+EEnVjySnu99MPE58SZKVD7R9ihPgTZKUmlhaGdUzZcT4I614I/F3ePWC7AnuP09hYymp9TpD6P5ch6VNdY5nz90IwmqZToBnN2HGCMCY0kVJUtVF+f0iBsMTsQCVnpvoNCmYmsroA5WtoDw1hMxf5MthaLS2YiEKnDZNjoXNsYP9rcS8Pv4yMKsW550Cu3czsnsdI+cG+0SLqoTbIeXntzAQ5yu3NVfbQSOoEwBnT4wBcdUHC3gWz00bwcn7m5OSDQ6Xv6dZHLT40l3IzhLTp48QqER20hkLk2WK78n7i0XIQVAgo6LKbIQGlugfJe8x6dok4hJVMxdgShZrbASUdG97gk6Lxr54Q0R1KaulRadU6sfq8PDduj8G8jiLEX8GVub1aaQDHkG0KaUviAwrHaYomae3T3ZECbK7a7rTPIfLKABVMbx7mTyGg7xQl9bcyx1lgui+TknlW4r6qKRHDqn90PxFaGdzc2vkjZepRHbWUjuM+UnnfwmVgZg6Nr9MvXdl2ahB36f3SwxUsu4dPvR3ytUc7XQ57E4Ukaco0Cp3kpinPJUQ0nF92Csqo7dmlbhJ3GNs3AhPmIpCf9Kg+j+4aAH/qPBAsRmgE9kJfjBeno1yWPpYr6rSPx9DLqBQWhAMRk9b/Xq7bVQ8v0l55U6PxHDJjPXQ9HNd7pxqOwdqe4IRdGiybTddDY7TTli2gxpXip/+EAmejeJtZGYto5mthIIg9bWge1wRoRRQLJq6MVpEWVCE5M2f55BYxs7RFJ0tlyoenE5dpbMQGc+fZBpwyjzN56AMSXd+V4C92kAbbNVCSl8CHpEOAKhC6FnJN8kZ6ClYK8gsPyzBR/CumzpXw+Dv48Yee6ZXkHPlfb1b7z/PlExZtn+HtZMkQ+T8GNf3wPfC9RSv5sH42KR04gPcWeagusB/hs5xrjTeDe2qWOrc++VYpxJvJLuOC7wDn4Es0cw4lhrOcaaBdqF6wQdDv4r0lLHHf2gK3/vNDyJ1DgPYcbeHevHHIMasnBFFR02LcT+iL4TO0EBZe+u0g1eqUn7eD+Q3wF8mjn+HnQJ+TIV84I+jJCgiNmxdZtI0cny2CF2SzZMYm/JccI38BA/LF99TPMGJmrerSnRyGGnNKhJv5r4tAs6p4B0KfGSQprbmHqUEKdOUjKYLJIRF6SJRb+0ArhF0itLI6J9r6ukILALLYlAQ+He4MIdCdGO1WbTrUKS26KHOERoKcoreCREC3HUSy6hS2i9VkjS0wUCNI2JHMhz4EjVSEGtUG5Pk5Rt4pJ5zGXhpmk4hrZ4dPZZHiRxbsaI7PIF2mllQSIGZEYtrfQkIDKipOWR0HbSQj4N1hbZSdcdzcRvaiFWqDTTYOku+A6D+OFgF9AhloQuKPM5TWmvhop7KPpw0VVEbwA9RSPGC6CPGjiso/KpgQOwGwjkaCEbo1EinlXLjOtb1gdIuLt2zEcCjXI0x9npYq3g4RBS1vSONyO5doGfO1hkr9YQ02/DuOp5dG/UMsmBp37jr8Kqu2OgCMAdlnDu4+NSHhbg2KpXAGmcdAocSf1V7/NY4PeYMC69EMzx2wlXSaAme5cCs0LgpQALpNHoQhjl4vSFhub1UgZUR/KhT0lLedwg51nojR/ze+oCZCcLOpnLw5lJJ8mY9vMnNalY14Mg/lWXyKgysowEEdjtIk+9g6Y/r0+rM1o218Xj66gX07ZX7M00088UT5DnyWXAikq6Sn/miGrX9ejg0ee/tCIkbluQp4jc9FtuP5UHcMNJh8oX9BPrcE78U/RQgEul736nc9IW/JPoILgr1NlvZIOywzWpu9JxXuelp/HdnpgNkfdPZo/fXSn5W06vvpZ9gaK0SQYpZ6XfnVFQ2aY/ZU5PSS8/l2cljKVo4hfoAWeh89REjiSoMeE2Efe3nDA0iKZ7O7LDxBsywp6YrHwxNn/K+LW3X69sX3RztoSQTFHG7niQD7rTnCWFMBuS3RV7Id2PB4+R7CbvQHokGLdCDWT4wf0y2lb0IfW9pNsmkHVAsXYnIZHr7P6JFc8EbxCdvtKJVi7ky4U+j2RVT7ib0DfTH/5oaJnHCzv53UZVD9HTqh22gJbAKv7j46XLdCTkBeQ8SW/ofjspBUgB3sV7hcOxMnr7CfacLl9yDz79v2mRtkwI3B+gmWQJYiSP0BSEkb7dOPNk8QRPbFOjnyCOLAxEJwTiQ3kVW3Cu+sTjLMvzK1Fjoxmdz7FZrkiP4maV9DJq2ccMfAXpZMYj14pamjKE9a6mpX9w7r1dgHkpwBWFsxOEATcVF3qFIrsj63apWIRHm2/qfF8XN2CCb/xwmZIOQn7Y/HibxtoQOaOhDMfkFFA82uZJXSTauoXLwEjCLOC2h+H+n2tHy2jA+uC1b8iQxUnvKxY9zBebY0D1gh0mMlyxRAhVGW71QhPL0OVHTxP9pBNRHuerBxp1GPNbysysrpdl0+ureYTW4/HUx9dF+whyo3y6GDsjt/1a/FCyF8OrDJtIkvL2i3Z9YDyQWTx+rywxBetvPDV+Hu0rpDU2tAUSQOLHj4wQEcSBcdDoxdxxTwVVA9JiPekoxoIyw2B3QFofXsicc/ImQ7v5zKZ6hoSIS1AuQtwhkMQ2YNIkchkfAvyJ15FzXIUDeEdBYM3RrNXCRyesq9JOa0PX6SlyPr2sqGRnRwQzio+J5gWtFy/LMFnWwwqVsPbrL7OpCjLmwbqyEFIUuwTkdbUzJyCF5ZVPMr4tMV3fXsI+onDRAIesPg9JIr947bdqrHCodg9AYVj/UMd7+gSDKgPp0rrmc+Fj7v4+lhgdocH0goaraVP54bG2mweun6LV4A1Yhk+BM+VikfuM+zN5DW5NLFDmPmgvHcFwfixyEA5JHNYYgHJceSXpXw7cq3cPPu47D30q7h5jrA2uoxSmIhR4uLv0oA6X/fpQkoUCPx7rO5149oeQnf94bUVibKHnqPwBkZzG0LEJunGnfiWQAyegWzFH13ke+lzIJeRmHelRgCuW0nQjTS9j5fGlXEQiQZ3wA3+xzThTaLFzrIqlg7Mx3sfjLP5ToH3XE11rNbdcEdFa0RdBPgv7Ey9E52/4A/bMZlETayZug7GYY76gbVLmB3+yTwu7uholCvWp4GNPtTFSSK5Vxypdt8BUR8YLBuHoyprCS/Qx2rnCis9L231u0mI/v9JOHwkELz0KI7KkMPBbtv71hYk2jNW7nfIsyfmdFbaggXc9NYI/NI+NtMrtX1XixeSMs5ktuOLzsAe+3LrUavFqRYlbrSKz9FZBOXafhhbexrAtoL359l48Y1W0BwcA01PxpVp/SvAYwJ7yNXYAr3u7YYaIr1ieiqFlKMjhCcmtBKeW+pP6WWn1pZjTp5kyJ08ynB62HOIYrZWe5CK5kOjPkfPztk9bVGg8x1ZEI/2tjRMv9w7pCmPNhWKaVWc8Yo8ahAiJj82V+ZT/SKD1+BppMJPmPFIxotQhf1T3XC8xo1PIpeEzOTGjKujuelfYZY1SCU8LmlzFCr5XLJRLjrOyBYiBNK1ilADQjXQ09iEQcDEBpEg9XGQT3kfEFpFu1bBgS/iNB+gxh883+Z6JAry8gBdQFLmFkKO4jKcmfQ7mZPNRXp9Z/vvZuOne7l/a4yW4fGKIbjBQe/IIRDArb9TA1ZM8M4o8JWs/KDwW42meYlxcXGe4kjFuLFswhrFoI7hM2u3t93lAVcnz3SDQd5ijw3yF0JtLZFFYptmoi8R8FvzAM22AaoX5DeKYRAu1WJzbVyet6T0zG82WD0oHspe5aqA8RKJPJKKOh4Z9NmfolMxNxC2MvZMiB3tpa4QXfp/fOQAl1ma8arHC6GPEg3xi28lExx5abV/33jwtSx79rmV3xPn1/FA3pKsegIqFUBega/G73POrxG0+2rqAgpK63nG8ofW9iA83NXRR0u3AQOA1FVwem09X3grtoCVHgAa3BUw4WMiUSg1yPDvWB65AQD3pDvkWQybjgTIvBp6KORgnByood4ggTQl4KKTQtTzuRLgW8dXFuGLWbldVMOsRPHhezflvd0AcXDBnz1OSQTJKrSnbvy5ztSF5g/KZNStLGRnjkKOyr6+lr5APCmj4SvCtZK+imiramSphICzv4fpCk4iQY2soQhHqjEh9iYw2jJPhCjOYEjN4w7j+qDqaoWc05ZWH9OtuRs9XILV1XaSW95thoe9OlKEfBHSc2ooGyldj3ijZOBPTeXfxUsgfxGFgretLmm3gsOnj9ACbfoEFyqEC7R76E5yuUXgDEDQkvuDU5oMK+LAxPZPm/Ng8mtpEkq7yR+caI0Ck7ggXsugQ2H2Pox1+MTvYJDmqVMLxs7DOrWLnmKVzpMAJ7ws7JMW7ebwt84Q9ASsHmyDyelMmOeSMKdJPV4t12DgWfM8rCdMd2EaT1fOjMMMJabPT2DgWHrp/2/RE63jZeD33on1V6DCsVQP8QeDRjBRJMIMXQjtXRecBacdH0UypwRLdMs4XJzrfJJK/uJDUiuMSO7aBcuu46SKWYMHmpSS24zYgzJmLUvxMqhbnqSNJ2M6s6NhOFTMOb46JfoVzHsmIzuggTY8+XMfyU+IEluDbLWm0HwORSyWzAviEYPpT+JocO47BctaG4lpSImmRF9nKUbhDFFNwARiWuhMkw7jrGiFqf1DpgInO3gjalqR//C3Ld0c44dQIi9Exru+n3X4HosRyMUV0NH3HODJsNDxvnHi2mvw85P9J3N/zNOF7CEA3NUmuUov0WyOtQ+usytMMSWxUQiEutYL/GrnVk1rLu3Db8Uc3XyJ8AaE2b0jTar62rHAFSULcOUQHTchv00sAcQ1y0v2PA0EdNpGLl08Oa1yIbD1P9MdxN/3/Z7IufSWl2lwALcbjReXHqnHP0ddPwkGdkSbwMW95cRgsAP59p14i8FHvEeQe0IL5Z6Cq25gOGjgNlKV/YkJhqqKBklJf5EQChPxPckSAi3zRz9Xdhfc9CAP40vhoLS7at4gBkEw71FV3L0uQCNUHqfVdc0uwmrme1YWktFw/HArj6RzeGGb2aD4r7DtoQwNBBWGEr8GJj8YuChFRlYQoHoUBcxUlUYjHUrkN/2Qkz64eAKg7kBXxAilvmsfa3rScw5ksJmoNaUC1Q6VMYCi2bFYwhHqI4c7PdUFWvQqBckBPeOOeOvqdziitlw8RO+DCtI5/VjQ7u6B9y8Jip2Gz+RVFtIdXlT1MYWRxAv2Otnc6LgMCgErx5IY6GOzIQUwQAsAfFKFUwnPyGD7twNPGePWIDQTGcmC9KJsVKJef8qiGhIUvULwAnIA0n3te3v18lPcNpij8fnpOMD0daIIgCplKHJ/ck0Q50swwSlM6yNMhw93qAmVYEkClkUxVT/SKNQkQnLPOCp+Y0rwueD7sB0wHOQNlwwzuc8ZC5arqGciGEQl/E+VjImwNv/jHJO/a/kq/CDu4FNWxxXXz5uhiQJ7OB3jxAjYwRBQH8tWr7lBiHX0YLkfr9E51QiigkjHA6xctTSAJTu2uV8kPdZvd+TCFjLR8WlD2XACCkg6UVU+P9L7/U56eaSgDH04F+Uv7EK9fCetT7RBkUwF5lZUe/ylc679roDJRaCc7xEBbMGMpt5oSnP8kwqHXWKeUM2RJL46FyhYeJi8WGQyHNub4+3Kxo/KbTmnlyONV63/a3YFkCe9uOSKzGs7gEo8k2hP5fEJc2Iu1hhaRta2DDQD5wWXi+6wIR6QlbQVaMl8wiWG0N8hg4dJDIRUxnQyzfChNUcloyQn0FFGG6ZINmJWiszkC88v5RsYOxt48WlDnPAtpIPjpa1NEFrYe80W7sBTuBZZoIu+P6ipqiURHZd8ZP5Fgxyac8hYG6UFfuseDF12I1SU+3DOMiRFZ6DWCvCfjsEc5Cwr2YJ9E0blzezYrVxiEI8WfKvcUwgEPyVPZu0vw4DIQyx2BpnbK3oTK9xy6uZNxP3lCBrQVd0FA7O2n7iAX80qO6vM9UbOhL/gYjlEClmqLq3GRmrKz6MIo8sBano19/rS3IIGZ+ZixT4Zmzo7EaubMSFFoHsbBqeubtEwUy+czO4K65jpECatdLT3xmbsMXAWHHiIGAZgejYPC5EA7aq35zXq3LoHSqMlQOQ5q/HkL4iiIQLdKqJkp7h6x+uUcHTqx7R/9fMY0IYuiX0ppYro4ipS54nmZ/R25AURxr5i5O1lo3AmIrfuEWJQ3ZP30fAqzlfYgSg9bATl8u1Dcgj4auGYkdkUnPj9CbjW9nq3kaBmyZ8/0JR+RgvZcMQ5VFGFi+FbHdKmqOhRXQDfhN3f/FeNzBLrmcaQuNWdGulNzxUjWas6MRNXmipHW2nx0LNC+mitGklpzZqSINVeMBLbmipFO1tyNnhYgPi7XjoTJpbFJ5ZZtT60cUGjt9fc+OjRg0MniBSPvavuGsS7Ge816JgFQk1X/wYIVLCi4ZhV3n/slWGDYyUrqRJzU1wtkWNZ1djKLzBRtiSfVOC0AsTcrtPzmeKSfNMcjYaWZaqcXo6aHutMjAEz6hiDcEljJNev0ESzRdTl3a7dyWaOHOgrIscWPD+9OlDAmLPpzxBoWtJbFIWKlx/hrXPKIU/P6LXnBSdfuqoQYjs7MN7XK69qCkM1avyDWQHaQ4qk/cHqp+vq+NMzI7c6mLGIxpGq0fpYIGtYObjrAqhuWMu5jYgbg71OPKRY6xY+KyxrwtB6mbViCFO7eVcXBdfe5ieNpJgwlIlwWJowTtTicLxGIxPELNhu9zbdOZq7LA4ec+zEjbIksFb0cNuMyeeuOlVAOrRkAivP30QNg8HUFM+89otsMv0msj6nKz57v6bEBy1EpgxfkiMDpMn++m0NAH4UjPjZJ0efICYbnLy2xjyQzEo15iuj+MAtMr/NeE5lsz5JZYc9/9keH5lhweH1Xn89YuCK0e4j0lYJnJAnatLbwTpF0Cmygq04lhlln1kx+wq1zxXHgDlcnh2c4esle3eH4YG5V6KF1VR4sd55LV36AqvHqeJyvaCh1jxt3XaOHJ/qUwBArLSAV0bFCLjPyRkkceSaO34Bf6XgXHVPGI7IzdY6zZ3d1AiurITqi1cbVbI8RPD7tijzQotlhXSzQNe/VeRKIjNsQ3wCNY9j8Va0gX9+iikE+WhK9E1Gx6xwgf0pLzflBvlJNCMnawYARbucnlGCISk2LBoqaFHTcRCEwo9wAncsIfSeQkzwrh+M3UcIsn4CRv8ou8viDnphyPPAFXzrQcDBtFJu+sw1uQSF5UBKMzmyD7YR3J05ml4KLbkbxWY8fb67o6SKpo1uwPHbiEXugzXt0imU///tebAHPXgThoCiynlou4PuDkuAiwNkHgviPNNMGPG0TT3HIGDot3wZk0CsPh3A3XNaNask8SK8xuPLv6gadLhUzWASWXT68Hid5QoLAJBMah60wl2yewUWLLZjhfryMfB1Aet2YjCPHUjvbWIpr5ZqVxrCCTkxAXxo1YZxosEkYEvAFF23IbILkaV8Lkj3oLvGa46uLwsEE7/QjKuVPUSfXUDjPoHnUaTIKo8RONKKpBZVwxRtUIbbLohfPDbrwxkbhGlou0DhBcItPM8YAm7NusFB5zJZM3q9UlrShe2zsymhmFombTqUQNOXGq1K+9Oz1yN8vmtWkD6C/RWpCCm8JohEI7GA2pS+xja0w5v+KlJJYWQkGFE6XTQM4/cOActNkhdSmJB+b0yi2N15c9NFX0SVlfnvPhlUWQXST8M1wzHbEXZ+UOOA2r0FjRS6cVsh4KnonnPABUiBmRxPeBn2hzcdMNJwHT2V+Bj1thVpF1dYIB2Eb6tyxeoWAL+c84/9uk6++wqKJ0jUh+aVS1KxcttNECmufQ3/QmACjbqqXD8QDoL7MFbL2udBaBzwEM6P5/IW82ZJLpA3JgCNDOWWyx0/ELvqm3GpFgW2pGhNoXELa8YRdptaOJ2d+75fEUENGFTGcuEegkzpI3tI2Jmqw/7VGshJyBkhQRb7P/BjQRKvMpbtFQAPljZumALaYm1dLhnZ8cGnKZT58VKDb767R/ZjXzV0PC5/F7cldyNkm7NBBg+XpM4sayGIsuRz61L2Ye02IuT2OIvSXykFb4v3s+DT/k1kJvtIsZf51VaSypakcJegK2PHCv6OP+vVw48tx1evFDE+Cpi6OdiEM89VLvzMQlOnXzVS+sMVVxz5FVkSUjx3rPIcWrvFFfwz5d3DwM858SzCoioO44lmSrjTlzQkZB6yXvmOrdg5XsDp6yVCJVDp8V55Yj1f1r77bef0rkZjqMl6v693U0TsANiSCTHQONI2llP8zgJEFKbZItfEEUjBQ+/yaSm8Gn2GYQriTHLX7tlS2qxbR6UsZqrrIIo2PG6s1S7heNlsb+bAbXurDzgvHP5J75QQK4ad4cIjd7NcVK9tud2lmU4sZwjMWUZ87FBE5F2nFd86St1hzDoyKXSlTcSbQ1WSCSjB3gtfaIxUd90XJMdp842lWMPZGmHQgcc5h0gHNWpFbB4RsSIkDVAz4b06HCIb9/gFxYQGIyMjW1so8KjYax0Qav3WOhDR21iJDXcGtdELhnIfhTgouXIuxueRf9ehr0Tmt5Tj4f6erteRbWBketCCkcprM6GE2lThSQAzuPPSYCUQ/dGpOqgOaAbDpH2QVrjK5pT9Ee6A+es0A2kmsv/5jH/l4/TG6NhbPWlP2zGW7I3hviIfnY6KNtqqKXO/6+W6d9N/PwXw/Oq1Bf2RT/osVuR295BWIMvz0civ86Kqbd9j92H1VQpcP1kSA+QQivaTRakCrhvQU7nMlNHfBfE+l2/Kjq34/1Px9bL4zt7rWQr+F5mO/aChq9Qm+ToCeSWziyhLXcoKdSOZUqjCVC62D6pNcklkRy3H8MjycTT90754v5avTRwua7L9zuP2Njw9Fltzq0gbPt57vrdbF3ySaY3z//wPtLfixjzzBq/2TdC5zasjqPkzJwn2usZSRWYCGU3F3KwBmdU53sgORNKerKHvifekyXCPU7gcEewLjn0dLgnpj8pJYJzkpomiYOWONio22DUOHQyyi1kR00tP9bWfXY6vSJvOxkA3Cjk5Q/pCwfQyk7qCPpzvurAIu8e6iqAsOCXSUxPkz/paOhEQPyHtyduvo739GtOHTLGruHINORLT5Hrp6dpJEJ+lt5qc8QabVHlyZF7+1fGNUdrDwi7Y3/Xq5z17Rs5f7/GU/X3uU+ivil3t8Rfxyjy+Lgb3xPolVSX00YSRfXQvmYeKqwGkszkuhEW5+XN+C0DCcYCUbiHaPRp173E6EPZLBR+NTIG7Cx8hPpcScrQm2TArBk2GpRPV+5CMYzwvNG52KBxP2c/kVyYxdZQc66whqhj6+17rPthoqaZbtue+Vx7EhRCmQXImo6InnKcGrJ7FCLvZf15PXj5booEifLGfqBIgTazSkp7xdVOWDMrQT5r2+rfWdgoYKKNIylDElWl0fCI6UN2wGC5kYwctOKIHMsSSSBGLopqH03WuZvTtDJEEGUOO4p5RtiX5M0PDDmMfxWkImOVER2YUfjsKphyDfs8xnUO519kl0rfyoFu6YRvNTarSk3W/hvz1DrjuVpeQIvR+CHfSyf38wv4WUFJ020UwgNyWszNU1cf3g/CKUfO6UBsSP/W2ryCpXeSILIhOD3R4nJT/OMflxM/sVRW9e9C9IV18RvYn0ZtpaEKK6NXhG9f4lZv8s4/OLSp/VQnaeaO1wWGqVJe0tdXriEoN+AW6d6rdwUf1KxLulzh9dg9pCjxLYXGnhn0UBlAh4i0PrHH3z1hP9vUJJWy6jKZ164JOk+Mzp8VA6tqNf2JJMFk5zS+K6ORwhDdNje5NW60rJT/tkd7NmUVdAMmpL39UxqY+17Ep5by5hpsOXgNC9mn5FMsEw3q9BfW2YKiTnQRJF1dovcOhKXsCUOP37cw8bO1+/RB2ZFgUORItVzLz6HZlvDCbl5+4KT4mRxwv3B8/6Y/+AnC9tmjoWGnQQihzK30533nf8sqvQxYMKllXQPPMsWBlufwgI3yQnB5aNBCEBawYS3v8D5mJGup4X94TCr5ABz1RZOgkyEm4GFixYyZLk6KMd6isuo+8016AtkRnSo5igZho9bpCY1bPq7Qc7m9f5y1muAb4aUbgG7Aip3MbBiCEOCUoHnE7dFzEatyzVj2wMzbn4Gvr+7EEbvmZMJcOpH+7rTUt30CbTToXSmiTAOMZE/nCUTLL3IHf4zctuiiKEt7ViLH/wTzxe+3B/TenXT+udYrx1fyVoU1tE5vyBHvuPe+6veL6pd7h+eOT5rYb9oJMrOaGm01EYxfP72k6G5ML9lnwd9nD9D6fcywyq4euyODXz3/3DmzCps4fckCTjVJAFax3YcqevKcr1D+eA1PzvWgrxdn/8ZaNd7MrFyDOL6QwtQ/+vfSwdVXXuOzB+XRw5r4V8mmywcxjoc/3O8s5W2hLCFAa37dfppGgvTxr5Yksemuwdrczm/WxL4DFTh3x22/bEqATcgrAAoP5T9A9tdp/nxQv4KIDlDPfovO0zHipQ+FRbbPz0UIJJ5iANP7HWtzPjE+1ruqXbMjOfrR6U4a5WR6UG+bekB43bQzt3zF4WRJaCZmuJJ+iqWbgLonQB6I782FVa4FimiCYQPUDwUiPVozFI1npV/Q95hDKrH86YGHSHIxvVNE8U/mgJg4jFzvY0aqp2YZxKQNNIAvIsY0rBQQ39CVztQJ8P5mCZjLsypVfgldH1Jl0U72VoDTSfq4PehUvf09DUPmourOpm0588Bd3CJ4n6/YBoDjSldd0kfzNAIIuvMddBq91bStM+eq+AaVpnTBxMev++PMU9OsW00OHDnzmZvunDTvRzYp0jkzyIekWwcd6ST1gBZynp4NBLyjr7diFLYt3vk0hgKhUHm/54aI9FVB4COnfWYqUWQYupecmrtBOG4Bnlvzk7v/wN1ZyRxf5jU6A1EBDJyqfwMYEPBvDYoU0CouknBsTc93KBwKNK3EEnoLg9H9x9J+WKl4xnfnJARxRhJqFrgyiJh+aZn+yevw7nhNPbddI+0bn6p8F32RrxNW3uBldtuDNHOF3uiZ/evNPJ3Bg5YF4XD5Od94BZaeL21Yb8nte/LVncWlylNAtO49zjPPr4MPJR3G42zOdHaq9+e/oTlgabHz2LITnVIxLuBYvUEpnFJzo2f6RinBOdQrjlbaa5Pc3t5L8pvN85QZs0g2bO0MDKHJhdiuxMXen/KMivj5KMfzDWbN0C5rNJC7GrUQ135r9P3b/bNxPEmO2DDCVzdFs0aI4OGm2PztlBr0eb8pWP+0733DpaI3gZ5eC+57i/hdvu2p//Tgf9fjVpwLOiEcOf7A/ry3vwvsvly19OVTrZnBmgD41tEC2eS4HtmQ3p+Ps9FCyaESZJIVtAk0+6f/m0QtqSOW6J7VvDYGzTNu5Le/vGlujjEA+T6X284es7CzEU8cacRdyvdvxo9gHm1ycxZRd/L7jCVyj1cCT2/yeyDR3a4h/zmDjTqUIr2aQizx/jQoboSj8G8ORIB9WiuqHXk3w5wSGUs/59oqk4THkv/av0ArKDB+4FMnbEz3vWbxRJ7xz346bbLRsl20eKavzE46spwgCDd6x0jL68eCZCsa9p3+L7m+7JuibsAahX4YLDmHjsr8VJl4YjX4SkzmaP6MjrDBNgxrcSzRPxgzdEKImkxLH17PMf50IidqS+RxvT86YcHbvYtQ1m7DpQ4NgUNc1y5OgIw2q5XW5t+NLU0lYhYx3OjvDmHfR9kG7QZwRWyvCBdv39B3SU86GeoD845t9I+W89qfU/wLK62lfqDSiwt6SBP7nhBdz1AkYWRZBgaHAs2bLckGGH83+wNofzMbC+axEkb2k0Km0d8SRDiEpzIsD1OruBiBmE631OAyf8GORSExjqcfdTGJj5HAFURcyrX5ruYP/bzwKQ/QzLNBbEtbo/DO8ucobUkMdchEY5qkkpMn2ssGzV0VC+qPDv/+nEq/5Y+19vfBnZO6ezGuLd777nsUqOwG/zsFZ+lLjRsfSl2cFG4lGjlXSAWfrY2+fuCsA09ujMJ8Mb/13uPrJxB9dRkGlvswSXvMY2n+F0vQ5N2FmeVCbx7OJARftH04f66fekfkQ7eVfgQ3ZI0Ll2TOVAPqhPg10Hj1fp5Hkxf9FY2eAMIt/oD2YeV1qr/wB8h60+NBDQ49kjbMXTbosDhw24jioacIK6OBHnrdJUFDMLy8PvzYx0qkvAa/csn+GfCNzVfQLPJHuy72OEowKHve4YynrKYtOiIciHu9gYm58GrNdXcpqFKHDnMJhJ5AhyekYMf0FnMMgzm+WXdX+oIQ3zxWpqf5p4Q3y5GVJJxQeTZKljv4FhR6nzXOtDII3fPlwAgPj10R7FOQ9P0xVsyJJ6vuQpHKAzb9nMMONeASxk6bo+N6fxEMz1+HzVTn74zF+FoJaUL/Za1PtO4arks8szJ0ajf/V7tVzS0udny3ZmBJoN4Gi3Z7Giq4o8P5wE2im7IqSHRbd7kqA5SeIZo9/dYXkzYqqE7+lbqgPR3KjzCMn/1HcngZ6p5WVcBEkXvxIoQxdqRySvCHaI3AXJGrKCsX15/RXEydCw83xGHaBOuxBwq+w4ArIYYvXRIQNalx2irWDauZ8/BNVYIZYOMZds4OHWl6ppyiwH5kL/Xv1JsuTPn5dQjYnNLlsX6QZIb/4TKoEWKzEOVmxIo86EHFJSaDQfMdMtJzhE4wH+5TxeFmZPXG9xX4V/jLWQGGVLEHUaCuDkl7z8KRc42jlIyp8fF5RBqO1eZyWPX1cHevYg3uwSzGjt0KTJg3lns3JHd6mmQk4eXQL5VbYcIMMYC6SQLoDkpZ3ZvvnZjHgtzMdsO02veAbhlVE4dazMR8L2b8FYoJkfJXhGTxFJWW3n0wEep269ErHFXb1ftahtVNqKzKGVvvy9U6oJk1RII5CSANpRFLicJ0LmAdyEcErqsSm7WWmMuBYPKWtCNeeTwcxXBInuq8uRS9ncjdT5YN/UyJFY/SVyWJIJCjgxQQ/tHe1RqYCHtShkSgVkp9N3qcrPAchmAF8yzNdRT2HYQwtuAgarKt8sVHAnDlieZpCxGMB50U8oT4DCpgAp8XiVqqXH8eB7JDT0UFAa74aalcaroYTjawVJ1TWlKJfipZOg7syCUdQSQayF+71rMG2q9Ce5Ss04CpOD8VzORxntjd7Z16VFZ8n9UhMgUzNLHWi1wdgrtaQpbaR9Sr/9O2LazQ9rVJMPk/4A+EMCSMZ2/vAfOXQ2mpzmthp9eifNryWS8fzDFHFclv1JXdYahgYpyTuqOhJDt1vT1IANItr8s1BiQI8rugS/Lo9qrEzOHDGXARybRXBVxn5Kwy2SZORKTr5NOcUeQZYqaNZwx2x3RBnHRkDpEeB1UYIPqBfkQd4kzCJGfJ21QN6otzCzBUdbmyVhS+VlKFqh7HNdYM+gQJRqKhvoR+ZYBishiBZmdr1sYzWfIjrihdDSE1S31rCaXkpAxf2Jrb7UNgkrETAxtCkBPVgBpqKVTUouhdmSfA8g7zSHb1BqJTHYqVzKV5PIzC3ekkjZqAIg2BjwS+xgApjxkbXPBE2cTbC7CwUHNY8g0NpCXoWcdelGFLyCJGfCrEzecsn/DZPeznihsSvh4zqf9jzTZLhzO5utY3ZpLRwQDdo3SklaPjR++JgDs3apn1KcAsWqqhc1KscCOiTxbCBikJ/1ASkNFfLHMSCNglEASpnApUfDWoy4JiV+/iPKg8GFBBPXu7uquf/vVEUzOqY1TxnjEKa0mVtIiYpqImA1JG2SCeuv3EA6zlKvNysvwQQfLtoJKZr+dScFqKuJqz+zjS8C+dweK8uJUleb+hjWP7TalZVNoM8IyJP+MGoCk+ezQdQ2tQHtOk273aVpUsTvpTqEwJraecUNUdJkkg8hHAE81mmogAie0BXlvJhyJ37qqayBj3kszhW/xvro7lLvIiltbFGpnXEU+QVXrETdf8zp+z9EAsii1vH89P9Z4MkKSp+6m4YP8CrqifUdrWeA5+FYSVUr9UYmpUWcIm3Hgce9WbkVuq/6iMyPzwJPVVL1Cf1W7ivGY6k8BKImZloYer1M/x/hn/f3bVMT08rH2U0gqdLDxRM5WwCcLOrtyyAppyiNEhUXYD4ERsWIeupETrlZ0Ui1nKi9alaqb9A0pObCNH4Y3uj3rxEbTeBS/DFWp1Y1x839Z+85079m+SJcfAmhbO7vgg7AAhNg9tAFTfUwLD4pQahQslWsi/EBD1mkWInSmVIFwUU5bdZA7ZoF9RJTddWBJeiFipUqxSjTCSytaiuu6qi9a/hWCepUqzaDFaQVFtj/ClIoLzkhvNaYd4kCE3EzL1DJoXqI007VPg5frE6Yj/HXjdzctyHfFvO1l3zTbDjKSVIRWWTOUOSFe6GCl/kuHm8XiSXP8YdqiLvI7EqPW4hROw0V8iCkofjbxrkJ8333DJzILaxH7DWmN1GiGRIciMsjVZyT2VC48JDWNbxvQqtCzlzBITI5CjK1Aj8E0rSN0KfwA9Lb0DhrPelh0GZXi0bM7RTT8GCNO2n7VFm3P3zMgcfogWjGc8Ts+HRApCAsamSumOVnnittRmbumK0VmZp5MgfTc7TvValkGUvBwRZAa5S1syIV+8Jr6tAi+lUEmqxdeg6HXIAUXSeEvpTxS4LRNc9WGILHYS7KytPqfhorfGQ41z72w3aatac53Aq7uhDzHR2Xb0E655xVRFgLuMqFBVErWBgOBzzXgOWht8H4OCpfpBu64rqkkJVhR1jQ1a6fpPXLrwB3czhFMEfUUBKq0z/xFCCBNdR5OI7z1FIxg6JwSk5RMoRmrYvmyTBK/IJu+UbPkObgQa/BO+1oEQvP0S8wrRS10wbZFXYU8jW8YgWI4nmwIxEDoxeTOLF56kTlMMnd88tm1UzoeAvPgToord50CRXFRmKKhPEN4OXFDWjpFtE1yB2korJpzLuZ7RVbCYiTTUsB8v1PifoiexOK9WNBopAOqTYV8KeOE/hNjwtARtCzqmrsXkCd2oHMzRI1fL0AasKOQGoFnK9rUts6kDdmPkvlVGD1yoCFnINQ7Eo1dHZASyMx3FiT9VJ46DEi1Y75QDNNt8tyd3YFXkgLjPDWORzNRPyPQFK1pDjXuLsVukSD3vddFimDedtRT5M+8LgfyUO2K4EheFy7Kbs/BSH+pN3GRQnLjOtvKTb86U+k9h+T8g7UhUGhc0ID6axikirTEEkZhAmMoMJBptPyxG907w1mQgNaNGwu5wthRKOBSigEFm6PwhmazoUTeS/0m0N19Q/V5m/iCFXATiNshfAbN2ROQJwoENz6UhPXn3uUbGocsB2J1oZpAg2DDwDJXpF0DTSgSZZuUAcRLds1qMBZCaRCoA3vOHEWWywjRKxM7llxNOyw0Y1HpdWucV4NdVaIGztMiQ993D2RCKZ2P87QfLTvNV2G2dGQ1n/jDkcHQbqSn/uUlWyIHWChfvwozroaO9dj3yIadvHB8dLBInAC3uDO9CaK1dOWyRobwSNeFk24+X6Z29/LzZ2L0j2FC3fDl0LzgxLXFxczWNmC1OPeg7Nc+tG9VcfThWiqNUflzxBn9kFpH5XJ2B/zW4k5aQsDGciJ7DOf1uBAjone+0i/c4PXkGsmMI9lHAScbZ5rQB1pbp0phnMwy+pUBeiBRMf7Ru37tjBlB268KGplQ5SgcOfqFrjzcosWC0ajOWVBcjYpjP+dufv6zWJ57LxYxjshIRzFGy9m3S0BgB56/GRaUf3U8s7W+xJoRMx0TcyCdrXOVjL4QU6RKuWX4nPR8SsU2CglhqlljASpw7RlAUPcn3sCfC6gsKXOnOSDBMZqelEBfDklUISgM/SccRztSfkKuZSWye0zJQglFOQD5o9MJTm8+PJv7dOOwofzkQx/4PD0d5berXlpGk/W8B2BiYG916/xOOb5xThxbWSSB8CPEll9z3b+1QDv+i9V4GFv8UbPoY0skmTUoKGPCKyMxTyDOdobSwohXvnMTk67LgVo/QWnZsNIVno3OSelPyFTlu/qn+SO9D8FxRL9w7GaDcZMuZiMlJpnDl0cuEaS7+lKRuwpEb3MhYgJSixv88rXvM2rFLGI4tDmxxHp3coEFAOa2+AN5kHi5ZPT2OC3xllzP/H58z42N9zGeSqMe4huTTA+RAgYsRkuVEOWhAucMF+wfcEFW5/QeAL+PaM4DAo4AfeCy5dz+fIu3WZysgmPKbM3PHSU7TJe0R7Yx5mB249dvHC4QbzU9scuX7jFdJ6Tk83/P91G4A8Cf7uJ+80OW5W0tgCQ3QJPkNwZ4P4XO1gf+LA/ditSBezvTGqwTxCWrIh4k0Qmz9gYJEps8v1UtcKHU3Z6/NQUYUsanJMT7Sdhy1SZpwJTiKQOIOd277HUKB3c/RGBTO2XXpOftXZYhc1qFTw1kNg0yuoGTyUJhIYkBUzfTrxQwpYiXhr9p/kD4v4cpoOG714IbCl1GcoFR3Tbe+66QyOORbicz09vs8qgutRPOgrBj4h5tH9HLI1PW6qmGjBt6AkSMTs3m9F2jhAbQHxdogGDHkfjr4+JC7dhnj3HkRDkMB0Rr7UTi+gvfhikh7wYeHojvE4Z4kw7T4LS+6Xj6UeCESsVrLbC2eAYLs3cwzxD1tDdTltLCCOX5BH6edwYJX+BeUJ0VLr6WxDIkZ0tQibd0Ow9+qZHV91835aHiNoIK9nNpW2T5Wp267JhN2HYsRdLziWK99eZXvDeVimk5Ex4X5KQewlJXkWiIHqLi27USU2pQfCm/4wt0v1IsyX5gFHzLVdj3Rm+sV/HWekroyje2UCljKsGZlLDlndgQCuCNTflJnXcwCIJE2eBVL/hvNg7ybbpYJVGPYmaMqjUeM5HF1VMxHZAg7owL2VxTE7I5I9vIpTchEyAA4Gd8jwmWupYqZHgp37EHJGz2q+YidMZjjZ0H4asFTvwJtFZ29gxmEm360PXdkV4IjkMhnbEBE//mqY7nGzD/n8HW5BtCsqm6PLuhS0z3kRVjK+HdHv5/kDetqVB39kn6fTzBNkcbe7kPMueYlJwGMgG8k6IgZzWhOkx3ER09YFJquLE0OCHOCAGO8TkQmgo7HQjySCE7K/TG8PdsvB05ZIKdrKA1QWOeaKNrlE/0xqoljIVxqCNtLm/C3HqhXEchavxvRnD2xRtGBO+do62cynJ5ZsZc6ohGl1i8FTYQ/EV+gfe9ZPMCXrHu8Eo8FbN9c6k5iW6nGzu3AFrz4Tleh4aAazsfHokMyoyq7cHbplFKAc0faXywa26IX7J6Fdp7V9F/o1l1Xt45RxgcfqGswm4nxIrlkgDh2fWQBw2POMTdKqN+ew1pVj0zPONd8O2wSxKNhtZDH5w7c503GMSUB2beXerZdKk2CsRihxu0Y6Z3F8uuva7k9TnSxtRMR58fUIzmKWBYQPvIstgPLW71a7RcwVJ4t4dp8rzUt5tuaFMdP+S6D6axu5/yUUbnKI/d3Y38ox+n6nolI0UKk2lVYzpDj1rDQfAukd1W77F2MRIv5DhGypfdS1sn6c8FN/mUYvfnBKfTmKq3gWcKKKtzjvkDbQFjWI6qbONFhBC1ogGBQ19H1kKTYM0NsLDlqkZcfUjzZkFVlATGcFFVhFSj3kycCB/n9a3EwCMfh4SfQHmTMAOcvX188upkmIKCSRLZ4YWVlCxWm5MICXqzMFBqiZx39k/l0ERietB706LdEwOutbCqH3wJMgKJUV2Uypd6kSalHe0aQWuTwcpb13J8enMaRok0WmumSSBEOyiJxDtWFnUbujXcmtKPDF6d3Wh4iFgck8dfAgd2i8HYeSnuoI7gfRtmPBs6Uj53A+63IhgnhxVmKg1lcSQobM/fWniAK/8VFNq/9leYhzcmZHiKOL+kdU5FR+O7WgOqL+nHthbRv7vtGZltZaH9iQlJurMMLvZyZpMHoCp8YhuUcZq9xTwCSO379GSmb5YcZ6OfN9dZVm9g2EuVKIn4vJiHYZycEGBsUX1xVdQW9gNWkaCMy/VOKKhmNC2imKIHqJdwlVYKnOWT7y/6W5B1lR/38wsHs7yhiWUFOCAED+EOhxHKUGc5cUmMqxNVyDrKFY/6qMyeK9JqnUHr9WYkBa7JsQo7rJ7yujnaGpoFy51N0PQB00zPugvRItfDBb9ro4ZEnZrHbrZke+HpJelWHXWiVaGtaMrBjiJ95YkT2MPYo8FliKOh3qGWJUx8fGGNW9OsommFLEKrYiuTdbECYWHYmUcJV22CoN6TdaT7uOczCtIb9zF0NKSTRt8w22IJ/CsIJGZmNt6G6ayOTW37tsxr48tVqEiwuh69RE+KPWJbLTSRJ41yQj8kQxi+ISaZc4fKLbe4sLOO4iw6tsMccgeL6UC6u5M1HXjaiySYUzHyQ4ZJZ+she1DOZYRFZ/RFWwj6npy8G/fCk2gMbQ3ZnIazC5lCEik88AMC6qPjR/sedBieFQeHqU0ERKimBaCcomqk6n9KlXEC3xO/Cru1IjLnlY4JgBti6+Ych+FJL0IKgdKdtgEK4QIAzkIEGzgcAK5g7BG6RU98CJ8VR0BEKAJ0U+T4205QH2cwU756uRpkx7saNx/Mt/lAenco9t5iBONlX2GEQBJ1KMwIzq/xJ+yOEmqtYiBFqd0SY01K7gpE155fySw3ui9okUVX2FR8AoplIxgrDj9WtI4jPDgmFWwMhCLUHbCaLay4RAObHYNd0szV4ZfWyBApc0+ObwsAtsofurRsB8tgbe+MaVStKQUibOV3elHVjLlljibN4FA+DFjsHx2TULx4hVO7Axl5d3LziXZH35SVAiLIBfN98ghzgjUKQsA1uINSRoE/DkWYS+9Xea7fSLOMjGvELmVg6nLbAfCNmL8xXUtgSXjWfk168P7Hvsk5uLuM39d69aHFyPhrGIq5yKapbl+3lVTbIPSoUozrYC4sLfbYwK0gNIHBwF84quwOPbrCV4PGEIUYF3UTgQTM8kSxaC90Ti6Fh2WpZy7FVvu5kgbnDjIgR4c6/jxD7Rkq6tN1yRNNbhOyZ3ew6UDSZzkIlCracvSPf7loFWEnBnvZGXxTfTpYiGEVUwiFNWKIv3LanFPO6omcPGo4CU7WBNeNTG2fGk2/GNWmMAZE1hWqErWyH8nF3JiSwYykwXV6HIOkjWtyFQIWRY9MdsV/hIANr7eX/uKGBL2a4+yYun058oZ7GcEUQI9mEkVDfBcR1/CoOLaks/qWzjbnyYvLf1tCfy7yH9riDuOB84bwfnwiAnWvKXXiEwtCvtzH+hWA9q88f/vzNoyGPnyRGVmNDTkUj1f/rCYT2vZGxoVn+YAThDeIhMbuDxgMx4IKX9X4EzfyrWwE4diS2j5RF18+vLiJQaFWUvqKmrBzuMdANflYNdtVV77L7S9hw8L5fS1653NtOYyExoxI4ZIVNG6upgbc963blWOrqMi3yuKV5h6Uew4jgbB7+2a/DsgQFFVcWiyQm+R672Q1Gk66P5ogilKM5d50eNAAwej58qlXKggt/zCLcNRkCW1Yw3fNpaaiZUujliL47j/K49ye6+yq4eOG72J4QKvXNUnVxHk3cNpxWo3ir/CIBOf5NrvloRLZRYH65EC3NFTA5YMaaf61TgssFsw67BYqyLnUmNLLWSFGQoc1UjKUr0gttiWosXig7zDBenWF0ROuXr5hELSFfCkJdWTzBymyEIaC5eRwwI0TO8lcfGuZI4rmRJM0BIgzZik2cMq41MBWVrZ+kKFzyyZEs7hnN6VzmLJWTyPI0vRrLxPeJUV5nj3AmJ7T5+Gb991y/mgZMxOSm7YV3QsPq6ebTDKSMpWy86bdSVayRN6lFVKb2fSFFIR0YSCgRat1juiRgtOnWvdnqG7W1v4j6itenqrZfci3qpazbwnS2V0Dg3nDchy78SSIipH2FnRMbuNdibAJl4hK31hj5fbmRQ0IYeSvR70kv5KpwCbc0mVHICMNucq60QdMqPr9p99nvX8rVfgeXWeW35u+rmtEqLbesYqHyLEQBeCtYfx6DKgdEyrkxMguwghY9ULOWL9elEevlD6/4DVUOrOY6tb07C2zMhDJWqZ5EsgWB2XilMT6edOOaSFgzbF9iC1Xekrf0gWp/8SCwBZ4bHswhfub2U+tGILYB0F/tXSB9/Uf6uG5rjzYsTzYRbYV21UI7/zX3prO+guNE+rTZaIL2JyxmUxpzdJWySGAjDNd5bJGTWtZ9uAz45v+NbUqVCfTJ0BliduIfLOXcI/zFEvlTbQIKU1V8yA+I5JeNdlqUX6vfvTRfqmndUV2YwsKZXkea/pxIW0O5cOXDb+xUx04quCC1KTBPVmOEQKxw4sZLbqQMpgaxcWCtjeETWjIWGkvlq7+oelVla6Y9nWqYcMjIUqxAYN+7JEAksJARs8PO3DNIlmPpScs81GNbbfg5XkRryAaOJM83PDdJGG6cTxyFfEjkRrR3cFfZ169LpdcWa0bBpA2Dz/ynx8aXntq6nIovpoeqOHP17nbiWCe7HmTROfX8WZaX0QQiDsVif+1LnGfDZvgZltJShoU09ebACHFT7rHmxmScizZ+GPhEQVFV71hp+YQ2k4yDHjyMRXIb3HhOospkAohLCdYhqqS1ZAnKNE6TlbhZLWzfPIrirRzbqRoO1lvDTpr4BsT5/IUcPazdMrbt0T8KzGo0Zlk3xvjfLAHQOq5TelK9CFESFwIlFtNywuTOCLeij6Ynt8AdAHuY4cAbUzT5ODhS42SsavKknNvJtyZ1MFU4Wr6blwYTy//dFj+hhV+gzJzZu6Y2BKpTTWdkAnML6DJmjK8ny7GDMVG6NQz1O19srI3UIhlpzqcCDSt4Z8uUk56ZLwUXQlH9gFCjOw52Fxqjca3mq6d4+nROZmdexwDcfQsc5IFr1szVgnvrkEMvPPUN+xxNm9RWk3XlQe5TEntb9rRtYX9Q1naoo6Tz7D05/XAyMnBrcg+7/P4iwuV3jSKfIOf6OU5zcGm/RuS05t7U1DXj4wWq8JtlwxD53FGbJXGnVx7uL4zW+35x9PtocYlt4op1z8gnh7s2yMdWmS2AIf6ktrx5bDlWht2PyWx5dPmmXvRGDdEtsIrVOCTCaJqgTBXO1FMuHh/IjCsQL1fmjzKy5r1eb+U2XWCoTqseLKRUCbfYqttAkfk+qk8+WtOctDD/rAck328XUbUFiGt7D8X1ENpa+Y4zeVEXmu8FYue9gkoO6rYexYdSxyU/IFIl8VxYNNKk1H//QK4PNaN6DsMUctVYPPhRt0E3VUavSqObAc1cRLVNS9AZqAfpm1E9UCCfFfmLfrW9VdqyD7wF9KT6hi85lDKU8jlLlfuUDfZJluXnHUhC1ehdfFsORKTycw8u/raSqBcfHA4IkLULfmpyWoyK/Insw/w0A59d1+96EMj2uqDR5Hb0PAkg+5uaaCYM7FWxDsLul835oynsNHDcmxYDujOVc0/vt2NPp9EhcvQtn+pc0c+0nqcWlRsNY8iq/x4YDoBP4ZXSmWaJ2I5YTse6/bCWeDr+EngaOgnIi7ZwcWFiagv0ALCxbgLMjbYYIZUkLDtxUmrH4vM/h2xRmBcueFrEigprC1aKelGnbSQI03MssRGLQ4aXpZn+vmBsVhA6X8I2YaKlYOzauwp1SdwR3grnO+Vr3l8J50qx3sVkMbvB4wrGFoH348ZOaDwuYzlSZAAIfSpdKvLreUA0ulLPG6+8Gx26KKhKMQ63SuY2mGbSU38Uqq2CDDWcVX1nMl0IAZVqDuesozAcoVFa6q6GCkqa8IJY1w/M6bOypYUPRlqIQ96rSX7jnNGhxGCUECC+FCkq4VEgLn59MKHD3ijZogZIc/A6aOGgGlGAlSpl1UQhGEGmIzoCIKqyRhxTYgc69Uj/esC7NzonI5KOxHDuUwj7xGoJs5f4Wi8mKcVS9chlXz7CZxuB670E4KAVUvXp5Sx3Da6otzZ5mZJs3h5hM2DkjdopDPSaObtQ0yZFvpO8/UOD2qDPXbUEXhp93cAqh5m371eVqQ5S0Axu0GY7c//Wbaboee6bXl9pp5bum57WcenQE5fUbpDplgUfArBbIm6FNMsr2V0IaXtaPZRtl1ozvb+GUMPcj9AzenBPdnFd2VQMix7WhERmrCoewiyJRoCSnM0rAjCHHcs3HRYVFP1o5G5RTh6wqlPy4lRS7E3eHkMSoeDu7ueQMp3ywRDfNoQkimzDK2nr9e++c4ZxW7IF1f5pGUEQ3Fiymf0c8884eHi7ScOx5cllVrBcTYfQygmwWUSA+bxZrCEni9ZgII1z8BC6cAbqyih9EUj+Fvpl+x5Sk6d+G3H/1Rnyrn2hCN1uaJnpJ+qlzhlM1XnCrguPBHsKqgQZijMuQS9BCu4CPHhwrau4BkZvu2J/o2n+s2EagXlsluN0JyU9MWwVuECxXZ6ChwiyZM3oJeFhXKgqOVPTwBjUqpWDrLkujAbU62immQ6IDrpQMf63F24SEFPSTr5aUr4K3gU0guDK6yUncdodSPbSbWz51Tz9+2AxQISbpAkBute0PLAFdmP/rBwr+CcSQAJT9sNScRzp5MeV1CCbiTpg2Iv4lSJITJlMUjamcfhT4gwd8GnxiTdAEbjxRbEaGOnYGOyEK+VX9e+1Hb7MSVS/n3Ucz/Tu7YFRNI7sI20Fyac9cEgRi0kiw+bufmL0w6cVs6wJw2aoBXBkiBeXjYlBWN0zonBFx4HFAAADkqWrwATK/lVzColfMbmDF2hD0Dl2z/wvMTOrwwF5u+EvRbDV5TjyHoiOb1M1ZYIxol8Ah038vDLlZp0T0JuQaIYeYFuZQtkWBJNB6V1/3zU605eBLlHIPd5Z81VfVdq1bCvWx4ZZ68evvjpq3rmjw/8djzbPaFx81cvTw4PzT6hsfNXGeatH+9Rzm4y8ORg3g7iqFq85Ta7sYKajyJWCrsyVUU67P6leiXf/js+yt5f/nps/RyyPlbvQHV8oEil894QrGENhUT351oCtlF6ZpOcVHGP5holC5mcvKHkTZ6fgZo+zidO4ecROqT3lbKI03PEdF7T2VVLBw0P16BjqDzljrdhKs8Ig9GcTOSV9gT8P4PaRLu738F7797w2+T9WT6FfrnDVLFzS/LkzkLUypFcvz4CT7C9OZMDc/9SSD1MIWz/Kq76sgTXfsc3TGP+/Vw/viv+RJvYQQCR/H9T9pX0y+R9Bqe6SskEs1NwlL68Qw5mSfKDbC3zfQg1M7IwA/QTBQ0w/tyQ4/PBA5H6OdfCWyEM0yNNrNydsI+Rsz6itv9BtU4YhencOyL3O1JVvul+fpz8rBhXytvXh60o4Rvr/dpIgzCf2MwstKx1pHUIBXRGeGvVtSFOpzP8eRNCyQRSX5Kx83g/+7yIo28vreTY+nmr718WG/ku5bPMEoTPkj6CfquqyESOKOSPZyXWz84it6Bl5RHsikmRsZCo9V8g6RBFMxrWZ0Zf83TxjgXGo7FDVpTH/P7RihNMDUI7qcsE+mG7UH4tK+WeLkqZhWEvypJjBnmOptzXdLA8K0yXrluSPGXJmrIEX8I9i/a55jaO4sHectfG2jaZigKF3pOVYst79nv2dx0432lCE5XawZ14sw9PtLJxK8iyIIznFgyU78Frqfa+fIdL8Anl6sccrOA3oeqhqQiW20h09EddeRLRLV2IYtin4ROActdtePKskW85SjKsRChzdbx3Zv1LbwNJ5G4FelsRDwOadSPBNYsVRqVIiZ5m/D+Ni3yRgq88ISH1focstjgslh1Q+dPV62FHgbGRfbgm4p2AcgtbzpB7L+R3obRRvDnhoa30MblFLSNPDTXWX2Msnz0wb9vDA25p8VHSodq2yOdgU2FbndC2WkiiZ2S0mYGjU391UiWmcvIdMY6US+a1wL+1D/ccAZbD/8uOHP7z0HRTk6S4LaTwZHwUBE9WGncWDCmAqPRnmAUAxEdUyAoAaCgLZGH6HRosiEikGHUOYXsobJ4J2qYhKykJ/mO9XpwwOixZ9/s62A0Tct/a1RrW4jNYwYYjaZF14Ybkdr6AEKJMobd3EgVeCAaduzheHyx0JHfIZQes29e1cV9ZZGW4hTct1NZbe1CRjd3Mq7o4ju20EKp4KSrtd8BbanVn/sN+3uAw2cWqhP1Q2TZiNx5C4DCfkkVboSdlTmncaUNmOa1pMyQa5hgJLJc5ZoAKK+Dx9EERuZGyHkNqUEH3oRWPO6LM4Ltscy+10BXrzpf1pr3MzQu94KYYjmWh1MlvfWZbo6oolslL6A7HrSS7TEJsCGwkLiCuG+VTZwfynVZvIEKYyA1mxX3SrzpAUMaLVOMKsAgG1AJ8kL5Nlz1NsazXOlJw1xgPgcUlf4hoclFwzH9rwETUiEmWYOtkmxAc7Wiq4EabnjXpKyE8pEfkhyjuCiP1V0VWFd9DgTlom05M5Gt3nIMcG9WFg7UDc21b0pjcwByxu6gpH6CNFlguy81284TmOHMuyDNBFNFLphrTwISUCo9YTFuW4vEjT2PPG9TbJShB6rEIc9LMbLC4z62xPP/fVu0Gv6O5oDublQ0ktKiCN3mrgQ2iRc5CSgpQR3uVAkMq2cvHnkiiInaJSHRKZs59JuHbjqlY+pboouTbapCUBuKm+xw1Y+BWMMY2+vveu1XuQ4YmRtfVHGymSOOtbSZkhQZ+GpP1P6xrQ9UEUrp+tYA8b0T/mthQyOBtYUa61HRocFedNrlggaR404fi+QhDVceetPm+KoHjgP3oHtEbCyFO4FltilF6ksoPhErsrF6tM54F4t9lQd74ICZ5kpZCMQF1Afenz6u4/QTjlkvfNsKxfcn9+i4A+M1JujCr1mM33HKUtXFyQ2SALGunUmpG5ZIru5Htm2ZVN5FaKrsln9i9N6a+LrQ49rgAOqlKDhJsSh+v2ZiaS1+I4m6nTbD9h/cQ/RWQF6C3wOB7d8kA6y7wuh6R7dGD3A0vRg14UiFXMEkP59/G9NbfA5BwCEfXkbe+iacYou45f29LZCTtbRpZ94kkC+mkgxEsIOl1BI77ylhYwXnrAWun01CrO1gzwM/6xjnrHSd77hkWtH34bIWdDJZpJ+hsjuvZtCZM0lmV9A9yvWOUW6W0q0Es7OK89PehE0dWwWEiLxhy7xUKSNO2/EzDglH3eL3kMsWNNTrL9vwOaW41TOXF7K00Q8uHiVXtwm/xO+cG3qpMxQWjwjtrOJfqhoP3MHCFE4Pe4Yj0nOCaOt0baIOzIcpt6P5mJMwA6k0glBBRzuNeaju7PyVDUkiGMlW0/y73ATQwMaVSVePCmokraxZGrlNzvv0L2eyPn3ONqN4YXphEIj28WbMfFyIuCXVsP5oczCaycFQyyez19aPsrx39v7R/RWVirPcxjef+lzvo+/+A5jEwKyW/mTSLW90//B8FrwuAXyllRs5dc1gSmyZLERKH4wfyoPbu9PDBCd/mwGA58osnspXavpb1TnvCCfW8fHXJk5BQg7Jqs2ZnnvR7kUv6aDGYFTWaLhXkUNEstdzA79B4tU10Esz9rS8MubfzglkOV5qBhAd7mCGETOpnCqI+iPFRuIjXAFv7iN/ZUKCbz2H8C+20rvZgAdswJ1bY1q+wGf5AHW6Uhy3XekJFW3W2oP/tmt9rtuNdc+bRa172xZMIw9pbR5thm6IO7lkQCgAvhI4L3bY4sO5gD76MTosET/GlUfBRh6KrE40bdI5kmZue5INRxQx+djA34wXOEfN/AWr3Ph7aNoveFBOOQqYzkyo+mUV+NsUyetnvBrCf4En38YqnnjuNIfffolsRnCrOYZuR8IHz1xmPAehUKVbHoaNvT3k2T98vZlDXso4CY7EPiQCAmynYC2Ohc7RmmyiXbuqc2RZwLzmrCmb+gwaLFmKPOedJ9BWSzNwokNX2JwI2g3fR8AnFt3D2BKAJFAqSARpFUa0kHg6GpWIAbTguwDU89eqU6A/bA+R0FfiIK0Sn9UZi7vJGYHFCynEo2qW+QFoWYvl1CtqU2CG/btG00mNENqyRk357Z+N4lqMhlVL5PQlh6NpzFf+0cb9ioOabEkjdmNoUM7hQ9/ss7n1c3JJDxgUu7oBevB1iOayIooPyD9s+0MKRKCAopDQHrTO9/Cv4GURxwg7f51AAkhu8XugGeb8gZy/eyATuS6u093GNxla6ovRjGsQC9wYfbItEdWWSigRK1Lf1gOZyxE/jpmQ171EIxQGuANL0iJU4urzaBCFG6/PbPPZIanIzwxzg6EcorufD5npmSubYbabiwxmb6PlGjRbxrceZtLKdDS45lWxjRqimjTy8JFJrf0/WHPnFWJadBaFqZ/dlZ4XWVG2oJUiOSn1HwIO5hfquq+j80OT2HqvS87Qqc7qXfejcMWxEXWKSa6zw7QNYQLIVQp2kA0qEawebXKL7gbFTcix6nAHaUvoRWFLEH4Qgf4Cc7Ylwcb/j7cB1ZQQ42UvkhNbQzqBAQNxNBKVfmOaDiY0U52B7ZSx0/E3h5T+FpVzH5wX50yZcedinn3NL0a+RcgTpHbuQxHtxSFr/dBM7Yna4wzx2N6z/ftovnxp3uzT+2V1ezrHWfGpLS0mFjvXIhDAk0a6XJpkXAmdJVU8xSX3fYHwE5Db9ZFpEpTUfyoJ/yZSCnX3SC8vZR48Sbq/IW+sKM9S5+Bxt1vdOzu1TF9u8J4hy1JuTyIAq87F8GB5Dk7qZL9t9AQcauytKFnsoFA8BNdyPAxgYS3YHmX/pUwfYVcfMuJ8ewnULJI1fB46IPS/TJu8+WnY/HwDO15hzruINofjWni7hCVx0mgjiJjnfhlr8a+JA+c4UQjPgcLFfwOdDCC0AQgDRdhj6tlV4x8Y+JHFTsyVuB44BrnG+GlK8puFHeB9uLJHi8+C39m7RaPbFO1q7nTvagfO1sGS+64o7A7x5Tp69kNzovmxVbMljU4XHuCEPRiOWCtPaAKtccCG88IVM+HrV+Qve/RLcOABEjHdTbAWLw3SPuJ3rX4MQZJTQMlqE/MexIWEuBiaasmpt38WAVJsC3xk0ZKhSheFv4OYgRa4WWTAIDYEEKZfiqW7lce3clL45kJLE+6vBpoKihepPzKhGyT0lKF3+pExu+2XGfxAfSbg1qBt/tkhCLUg3a+zEEGRhA3JMbiMByoRIlahYFzhWtb3JhXaegYzS/qE0HRUAQ0EHDqlYCKuiHhgK6/jJ8s6MvLcK/pfiq7XZH6Q2wgq1ee55oBS/rEY8XGYeY1I9YXjc3WpMiQqR6ZdBzSqj449+Tb+1HfJ5IsTUnaqPWtjD8dFZIrvBwjbMKgqX9G87Gff7WaEpq6TzggM3xWw5LK5UnahZNWtUPshptQ5wJByZyW30biP5k/PI9INjsz75eTE1nU+V8IjsyZ7hyf3NE92PGqGOWVAwSVVz3dppCmAYx6GBpEwpFHPmCkssRtwNKwyJtur9M/wCS8dbTPvaKLxrYPWI+NUH7cM99QMu5S5Z9A3+pZtK7L6c5ClAc49LnJPX/XFiPKpSVlixJ/Y7HwQwiA8Q1FUZfBbaTi+8Bn5sZ22pAYB8XTp9kbH48/o7SasPF36braBrSLMNjfjhYf8S0ye9/WUSvBViH6uMew0tdZ5M8QUrp+gHNMlV2bBeTmAu3IgB2WjQzIuTPKxMVhxPSZec1vGOgNbnIqWz4hQaooOLkZ0lcVraOvmVBw0TuaByLtGMXfn/30um6LjGdgHQi81YBcZyKD0rWZQx7McXeNR7Tv7lC2CTf1l1uy2d/CWBL39M/RWYdpXJGuPbuP/EXjRivVacGGlb6rXV6Ck1nkFPiYhAimhNcVY9tUGBQ8FV/TMySjq6XZ9MBh/5KdcOJFijwCH/RSLIVRjwutOHgfh4CwYvW6G7ZEPaet50t/TpIt1cQM+JT3RPEDzCal6gZh1oez30dPTo5nTw1eWhtce3s57/aPsvO9kU1/ehU19eVYX624CJnFdyk41JIBxZ4Qd9S6UFTSRYM0WLqKmEztXGy6U8iKR8xqDFo3D7padfu0fDgtRaJqOK+Ci1dWdYi7l2dVDxzgqtHAbjpEX74Rr7NJ2o25PSDDQuc0hf26teDuSHgJHBxyl2DjDZLdArTL7pgTgUWLmDsuOszE1kL04dJizcAXjcHEcPVoLyzxKMKnADrXs+VhG0txokUNUlsQ5ZRkOrTfXMfadiXSIPwIb7BEP6fUjAOz0cGwpPtRuP6NO27D/orwyoMACk9BLBoE1w9BdrSrx82hGk51w4mnIL18E5VA6PJVJphlkpX7zSofVSoaJPaB9QxfJW8D7bQqsK7NDs1fYFP5V80NS8LHmGP30GFvxQWo4tOCNNDx/GMQt8Et+briIaeI9VbvoheM0/Wez74S8D0uAb6TFVMvwqEcg6g8GNtvJEWCoHRcUU2eU1i6g3JLYOaAyv0FsA52+UfoS2bUzfoabvQgwRL05FA1wRoER0d/5gLWuDvYW1XN8dfYPoJMN/zn9bPx5fWhf0QYVDBvw9CpR28Yg16H0ahEBWv0gH6x79ssC1SFrQ4RhKV1BwVlpqCayZfmeI/0N4ALiXUwCBIaS3dYiCMIM+cVyCApgGptN/5g2wsea6XS4iT7SpZv1VlKQgdtiT5PGg6a/Ha9l/WhFdRxk4wErl0/B8iWoa2dMjZPIXlnkpfyeJ9eV6Nm7RfeJ3RZRCm2vXFMIaMpqSiygIZaD/QywOllipLILuPFHyqE1iTnakmih047TK4xXTC9JX6zXE187v5rfsGQYZzGJzNkZuT7wr7+e9KfBA2ZR9X7lzbGhuojKrjJkeRxCa9eZVCVKStsCIjpMjumhKlI0obtO8ogJX3iHT26WUr8bjImd1MTgDFvYK5KuSfoNxuBpRN40N8pUzk9qrAGGFvErpnQhFXqVCxW2maq2VcqRBAZVyM0YFKqFDCa0ZmbaH/zvEnO1XEumj1g3QBCOq545+nw8R/dk6eULr54Cm4j1anF7DqBXpsZkjwHFKM8JmDmIBrQBdIjG6w8By4ypayRyZp+uvtwzXDMmZZ9XdzCLWcRfYokzFEOpUEYbt7/VkUwNZZDJ3uyzQU6+FLWacoNGtwNJkskt/sy0ALylX0S6DkABATDIo8T6ETbCS6LOqf/pVItQ0YmbQIEs6gWDqFR6bx/mtHL6YAyoC0hkSS7lK9QaWCgaadjvZ91oj1pNsxJzVK2bpU2wq3y+ZjaAkDeSWrqvqHkkS17l5R8zqGrOrNQwX86S4YuGJCpjNzs/Obwgjo4xZpmiJPSFQeJyW6duEuhKdJYmPirF95rrZJYmYq44yDLThtbOZlaXkkrzBQXe5yQglsseLSIpAILWgWjkRp27QTFw3jPqbgZInExOpBRBYf+Es0s5t+lUIR05Ed5u06BXMOQKPyCCM6wZiEnnyWAEsS1jsWUoJ3197mGSB+wfKUNz0LbD/ENlU2qynNdKmLAsaDAL/q8KDPu8+pwfiK1MNQpVAtRom8EDznvHcTm7kPV1ZHt0bEMLkUfIdov3BiLi5g9BWpUw1O20XLCxoCW6JkD3TQbIQhApiTlpFH881KrRHWUNHhJ4Rvdt5JUENd6tWO+yw/po8cJ3vNe5FpeqMAkaOjUw6rLOKDn/4N8H/JG9bK9frSJAQcHrsd1bb32JzSf1x4Szjv6XyBoxRih4Vs8AmOsJF28rHY/Bvm4K93Bt8haNuNj4TUzCscpfCOF/wK5ueelu0NGB38xqCdUvMJcciSOB3CwJOTLOLH4l2kPUsxWetCXV0fjvMvXHkXZ/jtpLJSTcTrEkfQc8UtDe5JGhmeSuftZyR58wlxC9g0BITJqxnt6zS4fAtruRH3eAWTvZk3c0FkwWuzjc2YFWMkCO7oHEUnQ6a5gebw8HSSL/7PG0j76Q/iOesZIwO/K0iEhn1kali0rTSRLfeY7TO+PPRoGFaqIejG09C43v5/eumYp4jijoUrp8uYr7w3Ih09Df1O5Gt38fiSFXuf3BrON2JxpUoFNzTGrnh/D7WrxTHjY/d9sZ/9r6jYBo2DspOafOYuq9UIcEXldsxKJr5S5T2dqCQvOTP4YR48Yhm4qOEw4+G6BA40NnJci5fMkYR0mJv0tpWbTJ/v8PTgUdfQrnpQrG736JmogW6OZe99LdXw79Ibmjn7dMf3T28+9Zp4jz7KNSWLQHukMjiVM5hZVLV7OJmXatqBkqWbJ3RU8mk5oeLZa1niYUpdjhUNfylxuAJlm5RePgibQ3pcxoIkUTDCqxAvJGfsKpzhIDsbEZJ7sNLtU+xjo/Waan4a3kR4U2sYZQTQIgg5k6+/PvuWlAbWWMTwCKioOIS4qPgyZLGDwSo/MAA0y4d7WVJEhgEmHfcalgMjgFtfSIjdWDtuehvpS7YiOxpYWSn7c5hl2oFcLRP3UnA7EPFRYm8VnVnHSYFev8RMW+GxzfxPym5q8oZ+t3508m5p7tq6uMgVEHkpzXNdMo6q0b8x4h0QUVPeSorUNJFMzWYZs/rZkQEC1AJcAH2+SMnTxpLpef21UCCRD8vJn5K89fZu5Th/i9WdQTdyNPIGicq1e3mVj83L001XnRKlOYNpTEBMeY01LbiEPvoNsA0SEoFn5PbQ5vf6vql0ZEVOmuqDfOggL504Z9oigdQLjxvo7oqex9WCH0Okemoi3ckBs+Xzm7VMdhz8kNSsVYrBVajJCWJnAe4Xvfn5mHU0T9UUSjTa2g/jVmnshdvXicpAuJSpEvGlkUBOWEXypjjLex4G0K8uswcK8FV5DvifKEjVlMJTjYswRNKJ8hrpv0iZ17IOU21HHaQOKME16prebIrvbNN0KklUgW1Zf23iZtjYqFiS/24mO3BIIXqs2Sz6NfujRAbUmSXGzHMcdHQjG6Jtg/bDN8nmiGPMeOHcvV/NsuASw9Cpp9GmG9P0uFX3Tk/WQRmSgCOgpDB/tDPiq3MCn36nMDcZEh5EDwhyfmRMq+bvGutqtk4OFUhojR5fkgz17qs41H1xfdS9dwd+ssCG6D+JCL7w/IeyqpXmhw8HVuQLtAt+IAuxcSK73ggNwt2N+WdMhwGipedSBRc8Wb9J3LlSvatDvAI3uHwdQc+zJbI5HR/vlG+165MB2WiLj2uljPfZ9+MxCU/y7Ie9Ixmchx2T+13y58nLpCPyCA9n2cltdt7XOOsXgYEDKoGgliPztcuzWwqCMDMNcBq1NxERJKTN9jrgKv0pL1pu6N4B9jEXBQfDVtTJEZAOInuTdD6weymz1T4cgZSwSxyK/8xJluGtsvgXp5SJelJ9xOMxrLa93YAIDZMe01ygliOihtFHikwLpQbKRui28xgT5/lkDgjSUXi4MmUTAF+iBk0JEGnW5WUNvJ+PVI0TdgV1KvyjRbXF6s6hCJhY6iSKD0CFr0MUdRrHK3xRSRgsYteSZK5PwoVrBAXhcloX363Df8kHtFGeEgQJX1SrJvPKhCI3EH0gWiv/Wg5smrawY02hxwv5399afvN3tQ0ykKU1hVPdDNZ/0S9xcq/VIidI/VX0dN91/b5uJeTs77f+HhKLVzy2K2u+XEPeLr3hf2M369PmOe59ctVBmJ6C14/6+vI7z999PtJLBESJ+EA9ghmz1R40IKHumZyFYCLSTFt+Dlv24RvvjH+GZGiixUteWtz6P3H5Wr7tkKQzO0K8OsLSiT5Qethc966wj/XqTYCdr/BLyxQsSQZuCP5QxTjwoIOz0fbgZZSXo67+OWAlH1maZ9k3wO8Bj8rn1LBaW28288O+VOWg7H0pByzr+GY6gYeUVckU9xjAo8KI94xPNbrJBcVqnkoq8z4zkQ0AMZVlXP09lryzgUMZbU+DRPFX7ef6SIIe5fOXsAbX9U6eIU1tWYlsMdlauSCbJNrXpZ6aVwWQPH5SXFU6eK9xl9xCkX3Q2XVT5rSupyTiK8vyOiDKNUjPu2+EPRtklFXWJ7hOYHiN4RFdY8pyD1RJQOV2cHMTWEkjlYw96O6Tq0bg6nbrlIqKHqDxK7IzsPqX9Dll3f4Bq4CVTtwuo84RamJvAtYLaaO2cfneV+S/KjTz9XANBOtFgXnUvEvhG8jJegks+tJpj6uGy5rLzM7veRqu/0rswjzaEoqCoGKbI9uvlIA7QZMtCLJU9kJ/Sza0LIzvWF/Nwr87AAKSVcIyur907pndBYH020GyLctCrSztaA/Y7oV2JfZG+OfSXiReIVigkrsuTBre9cVSVNtySFYk32kO3sxSX31TO7B8mytobbnetNqeR24MEzCuSfkDtyBQkSbKkNJao4JCon25ScQeZEak7ZAYWak8LZtIt5P00uMzxcpG5Tnc3B9qwVcLkgzJb1h1TZ1dzNx0j2jKXBgUPFgAVdXDnuAFWjcN8ffUY8BJzyoaPdmahNlPO9RicHPmmkxGM+0rOCndIYmQANIbWCOYu9dlGEiZGbcIWA6U0JvlVmKigLcUV3oXEMWXUZTkySAieY2zkinZgQiolE+rZYmTTcE07ayTFmxFmCATHGz6WUidzafEqZk/85VhNMFNmv2qsE6o5ToWjsDWss/HMmQiAodIn5Fi46I6VbEOkSHb3CSwsxLCmnmph5Gao3PJ0LSeJFsQh1ldKGSHXHdn4UEuE8vxUURHXygTLDvBOv6ic7IRnhq0B6YoBotl/IOUDRomtQkln7Pv97mq9gLRxmOSNIFtU2n3BNDgYwHBQJ03hhBWec35MkMJqIAPgNYx5JSFnEkZy62UgT3uSFxtfH3DmXHLgP8DuaFTXljLxDp0jL0QYWtaFV0MwOUP8YJsmR75JrxkoKV7l2uxb06mygLcby7/gCZu1zAG0TkdhJsyT2N+qQmuOP3YzJ21bgUpLAbqYKlQLGI3vUmRqMeqPS5EczzVlEbGFbTfs6QUUDBdibhnA3rurGGIRdeX+kTMxp31Yc5mmM5Q2ZMOUsmgRwQonfOKdUrlCncFnSDprcBy+0UajnCjjvX35Igr9abEge2ubbqV9WokPP4WTaZY4JtmYo00VpKM0HITQ5erYvQovmDq8WndgY8McsGI+Z1sftdQAzPV2Vipm2g5iFtR4dXUxSLQjCTkdtKCP9qmtHKE0Gfg5jCHSkRah/M58laiAZSe6NUbxFKGvU77Dh/nEPa308Y41HtLuube/9tB6fD2r0ttIthrN6naxByolSLRv6ek8GEvXBa8Rwz39I73i3iENB8bSD8ALz6SSt25gzkoLB6S+0Y4lvzKRPGlcQ0u2d0Lg812dab1FOW2PVCVpoAJYJlY6c8gFAoFn7TyNQcXKOnp0WZgCc4H97cBHEtD/htmjJTpVi5L2eTLpUR2s4Z0fwZ6Dg4iwMC7auNBf0NhUKI3lhswMF6lXhnTk2bpZQZ59nwGcLJtyAtYoF8Lh1Td4ox/W55kr0bOnVhVQjitoQw9noeL2AZ8ymXgiC2/AuZPTP3Ua7HvMn7JhBqwa+xdNINFvOzCGSGINWl2YRzNS4m+zQGrNHpC7G3I4Uk3LrujOAueUbPvQDO/rOlcC3CLi1MY0gJ2uSvvN9a3JZ006va5ao1NysmBzO95+lmrnAEEY+io39Tss1S58MZY6l31a0WPblChFLH+609H34IccmDlAi8Tp7mHqMmzigFk1LGVap2oV8GYT1FE4mq97rpW+6PLuQ5TRaClKdZpWAeXTienN2+m0IKgmv82ISAGLYHR+kfASv7jNoOrz5BWsGQnHqIr/nbEYTeVZlJkYN8FaEm3g4/3j0SeSFdc9PdNy2tr5Am0Yu0KTDCeu8zAvwRJweVm0BJy3XYAFGVENp7uVV1CE7HsPVTgeT03P/qLhftHeOCvgyjyepL44rjm4hts6bfP1244nXlki6+ItR779wNwk13PDM9eX00I7DM8y8Hd41GmHNTpuWLekw+Wkm0cAdrUCpuUn6iFVfdr45oU/t0wkEpDbmxjlbSV3yUsoHuw4OGiQ8pRFlQz7cBYzq175BLzBJWYAiaw83ByvyUw0qaPmKoC0Um5K79xO3/bQIlXZlgurj2Rno6dvaXw4hteybGrM3kYNhzbHzOQBfJOeow9197lM3c7dsPsNTzQard57VbS8ajwgQWCv+nrv+AcNYNIK+QJna4yynuKzWXTqosubQm7T6g6Cg730zSeafHbS+4BulllcqbU93RSvPqiebIurBqNu6nl7C9ajS/5AcJnMiHUS6is5IzpRJGGUUS8T09bLQQSrw20HlB77m6h8BU3iqoPX5kmx2jTtKI6RX/cn6UoOUXXquGbB0toyakhA+865ACeouWEq1DYVH0uLfSywpOuKsT4frHGu2RO35ylZWSXOG78yg0lle7YKRqDElyBaaIyK0f/nyhi5+g/KhrkmU9sXCSjlucF2AEes/VwdDr1ibx8TG3RNW1Bnq65059C043rQdc3lbnBOzfv9jLbKWZWrNVzK26+3Hw1agD0RpWinNqSbN6GZmvTdqPOV+msK4SLj66VGNwhiOkfT6ZNm3itr1bgTr7wubFpz/zE/jOZ7+nBnqiNZwEzvuynkW0P6fBV76UtoFHOnNwGOcDI1s4UaEDsI0hZPgOwwcoHCWLevMtV0B8GrcdEvqRZuEoQCuAmzIJWswPku5hlwL96oKQxxvkezRaGJQDM9zo64263nyplthuo/Pv8BUpkFmB8UhacnF9x/K869zeP5rhNXUHNKfpvWGbAt13D5M7N2Y20+hv3et6GokKkqYUjYvpqG4yIyAGeqMwyrWlBKHiPpz4w9rJJq179FwQgTks6It4NFnT/wKZc5fpkn05tnOFed9LwGFDdK89W6UVh/CPm0Y51fiQ+msflwDyR/3forK6X3tV0oAhRmCPcPtlJtZOrt+1ecH9srKTRQRwCmH6YF9+aNYzHROQkPt8V/ZlYNPLVf0yU7Zb1Egi/TBF1R68+Kw+VvFW3OQu8uqLK1+LCTBnbzgvztkaL5EVD8hfW707M83wGkrhF4V2EzA8goAZGHgL36xmu6YAC5dFD9Ywma3g4JxAvYB4nWuCuiZMxXFSSJNxlXZ8j5b+MlbdPU7+It91tnvcJQoQFWD2iwGqiirqEHVUk846CWpL3W5OZe5RiK6SuQr7aaXYmzTHjg6StQMBbIpA6UUMVkYhK4z2ZV3sb0zBRYFBz+hsdoXevymvkYKU/eZDBsgtlvAPwxnXKTi1Qjpui8zB0AS1pf0+cjmohyi2la3y5REvURAsUOyFapaXT6pfsQfe7i4WLj6yhAVYtPj7vH6r0paY3irrMR6BpFvVF30kNl9qpheS2WqIiWOtXWWBU/JA9YUvcc3huZ44ynBUiSuOMzfLGRL8LcJC4HANxxsK6oBeS7pq1fGtmkGnAR9AAPh//zNTQkQPLJqN9Nv2uZlJU7PXShSZ9QEKhUIa+XqqgdcL6k/cTV1ePek2JvHjpJ1aQjz8iuAzvG+XvIzGkusQu14veofEBDcDrHBEbPoWHtNAHYPRiP4x16dm9UBdBYeRj0gwhP1WCrawBozMiDqCQ/c+JTuEWuOfEJsEUc7WrUduQ8Sw6n7qcmJhuv61tPwSWYC7pNaDV+kjwaomGLDxyooW9foscJ3i2IPpx/yvGjWUxCWAeAoPfKmb6k/9xvk8Jic0XaCC02N+HXpSEUZ+8ksDUacsPIhoF3W5wGMxzJl1KLhrTKJOvLcBDpfZm0BfqzVwF202knNpWlDIQscNdXgBOo5P/FJQ4geIoQ3eLJx/Q+uuwVK0Bg4xHiQ/tFMkPhiqgQM0hIV4gQYSCA6DiEgLBj7CUwnUhbqpxun1BiVeUIZINCUghTqfFhMsHH3TZcyRp4PXtAqHPNgq1pZCkVZr6oUZGvFiCyX8m7QLqDpeEX0jRrtjRpIjr3BdjQMBJU16v6Q5PWtglSnrdAwVwrfSIjhE6nRblBaFpqNQeuY+wKxQIRFVwAEk7yf1vLhKbmdhmWPg5v/vxEqPc6yZ4ROd0fidNkJHyuISVvOwCid8EkRfmHIPIO7g3PHJgTchy/eVxxIJzuiXKqZgtjg2Z6tQLHH1WS9jN4XmAb39GKBfv75r/Hz4nlLPOd4iAq8qcxiUlYPtdI4vVyA8Gm0Nn6GqSTKZoE0oevqwypmGnQWBF7BmgAX+RwzqEHbKkCMSbgqoOxRY9WXKd2ggVtB4e0u04qaEA6TqVnGiR4DVD8lcQJVpiBsO8FOXGE4dGRXpYOoKimgY2+0uVjDrs2ux9d7MmzHhViXwA+ySgOHbOldCELuJW8NhVtbGp245+rMCwppH7ddWyop3J1cKFkHx/YlXTzYsXS6RK2zF/cNlkWXiOLpMIAzKGMm166GwvGsWuPgRVAtti5RDuy+8m59YNnOQAKqJQg4YqiEXiXjspQXNt+RHOcd4ahIuwC/L6O2m2bxYR+b2JaKTO3hRQId1ZJgz6VCvinuj2qQWW3xsropaoSjY94rouUp3pUFpWJ7Ay2zO7TcBQ4Zw7V4B3RpQ1+fTbpoCeUk+9TB/aviSbyGMQ8nU7CeVsIB8SQ6b/nYvmFxSmQGH5JlUJo7uIRIWamgihFOS2pUs5LGkj8pzFIUWI9xsYleVbnzO0RQDjNiHncw3pK0y/SXpha60Jiye//H5lx0PQ6oWBTlO0n1h8xccuSE7wkDJ884LxC3JsUIhFN8Ycz2ekTK1Gox9KTG6DSVb9BoW+48e/EEZt729WOIvLE0FyGu6aegSfbwIZbSx2EXiMgwcbCinAtPgY/ltclHmmmaupBiAA3q0RsFtilnUhLsi8PvwvBgQOv0NkrX11jP6sLnRG8Q9bJZgriqV4CCbpiIh7NOAzECkJi2NIl8rz+9UMzy4x8JPY4fUAArHCqItpl5JVnwOvvGrBOn5IUMGdCgwHkovB7E4YLPucB0GQyE4G0ntUqRPQlrprjU+RojA23Yedx2vVX4gHeBlgt+0PnFG3kgbiJQCSmjbW37ULOySFVp3uogj0ptWTWhfhR/81Iir4EvzXo5CEdYETB5MoG/qJNQoP4eZU15pNbhND7+eXGK11usYvFOYrMxUgLZNSnQZQquohtu2nJ8XnlJTxYVXmIe1R7REIIoAUAkxoozoFNN2JtZpf3jvKix8nDVt1Hrto0J0Yyd82HX/KoOVwJZ8kT3Oyudx1S2mZPz1lKBobESqpfQUyB+8ALMioy6CsWYRX3VH4UByQx72dpnv0fwhIT7GDnzUESlFT6GijppoAv1nOpPyF+Nl61KxLrDBa2m4XYy0dHNfe6KKd6NpP731TwyH2pvDtdHn1fdyVW4gdZBoguEbAdXuMTo1Fh3URQgQry7FDloTqQh+96MSnqPDkkBHkUsclv+SzuDeqwpbsiP89D2dokeuVw0Dm9aFrELZTIc6Sx3ctPTYhw2NTnhV7xxvrZ5XD6L7HCknN/mAdWp0nTtFPSXh/Ml5EHliaHamu9hO+WlKAs6wm7MnXDj4uXULPg+z8z1Q5DVOYF2pnlq+jatDCG3w69GrV1IU8jpS2qq+fd8mPHAeE7PemT5hvHHoBJQrtQMbgE+RzYmML9ePZRV/rX1gdz6C/RclNSqLMFYAOeJR18r7/XEQTUb0wSxHhOueZEQp1BQ9teR1gJ/D/vLxEOGUZF+qmc41Q9p34JW4CR4C1/thOqX65vpB76XQvHW7w4jvAcih7sj20WXu2HdwP2qFIm3WI8WdWQk3iEksYcVmsIxiLzTyi7EBXyViPjO/p63ALr8kGgkkWN9mFAhNpDPB6ThjbfA23w7P73pcb60c9WLTI5OkT8+DPj98OF3PC2NP8/HGE5WxtGKrnLGA2mpOVUi5rIjL+sSd0dJQN7GOkgGOC9Cdk4Ej16URNvJWbDABa9uWPwEgZRc6iPMJ2/xohc9wCC0fcSK/XM8u4EsCcPD2sMtaRQ9brWYiqWv/XzN9JXkUBcMKi+RX/g0HvnhdJsDukmSrekSNfKXURhZsVx757lt66gjZln3ICqeItJuI1TnyMYUZpXnaB7STlSm7mK23mrMxF+mEPyOzudqH0kxOtFLkg0OQP9seXaXnnEVZ0R2irZYYaEl+uKrFYEw65VHUX8wIIUGwvKZW0NXOSIA9IuxJUIMZYGg49uckqH0Hhpd7JIeb3FUvkuix2XlpxxlXpolHuO6ORbcAH3kIE1ClsYKUovByclF0/L1olmg+1EPZHKGCl0vZjip6scEJzUjKk051WM8KalFF1qKhaUS648W+79qllNRnI6VqMqUQov+sbUXBwkt/iD4M+0YrY3oPASj9UUOSkQM9KfhfLKsoScGNtq1DF1yzwwAWd3XGEJuCQnEhnPtmmMC3UFMcqEzhfz3yAm6R+4i5hEB5EuJR/twWDOF0W6mbvM9NqkdgQsXK1OlQydFe6ZrbQ8pkRc2ufco9P7O+iPtZ+RFKGyeywpJ1ghDxyS1wF7axB9R30EaJjFHlub95fGv0UbaPT8IOx/CnYHj+Wc+u0muhHrBc1nprAObxOhcXpMmgRYBlcIsM2DBNjBlUH8DwUO4K7nb0UgDdzDoDVpr9AK8OTmDC8oKAE7+fyzNZ6cgFnaw61wicCFS/uRIwf/FNnsLYal3y+Mrz48nF7nuAvmLSsc6T1pfV0mwkM+Fq9S9MzKQ9aiXNaR50iHfUOm307i7TNTT/qiEmBksmO2828yvQDqP7HF37kOt24KeW5D4BfsPT8N7yvpvSQtAFLEVQOLIDADzUrZYB9XZERJl2qDaOLUI+1f7WOLtXIH7ozAA6auEiioX2v2kf0fF1yAhVmzeQfWMgzT6y7s0kAKLjks3qCsoNhbwGTjgy2efCJBkVAdbG4shsMyJ3+QlPrV7EEU5U/vOSBwKYwi/2K+fMgV6MEF3lFGbPxGiKgq9omGI/XzWabwtXhpo9xZDZfzr0+KUEln8MFE3So01TDcKpFIyIqN9ThIyiokz2VL/tOCtGGKlRDPUT72BmDLtJQ9s9qb+9kSUEeoAZecz5yskuGRcZy5NM8UVQE/D7ll2FOW41CN295buTae/e7QpYaTmkxQPJe/RhStzZUB5FQGIUicvFgA2kOfOl6DgfodYcju3t51isNp9y9iC6QLzrHrpbYHDBkcd2siUwsIgYo/4cVWW4df0PHT6uw2k8AfFL2aU1msbeWa/NO2ZSVajPUFTCwlI8uSnSYLgMFhh/aPRkqQkY0meVAimhUwyQwxGQKdqetkwe3omHe90MZT8Kdv6FAGWvjaFEWFjAZMds7gtu7PfSDJeSbeoTZ9cmC+rnKr29UON/38UrCyA42Nqld26MngZUFlNPUPagn95D2OIh2ov+X0phsexWml3hAEmpwdcZqy/Bh784ihGv8+fvY30s3WdrbIFwN31LQ1OeLzlJ2UePBF3JjN//X9ehMgKIGfly8oYQ3zynJCK5kB3ImB/MhAT3FUHN7xIyJu+ALLxXx3OOjakh8yxWTsmWEaoaD/mTRzA6E8Jkq6le0AzazL9j++V4SVKZtQs4AIDqoD1e0j0D0rOpWgV50aYSl0wQ70z/6gwLB1X+XMfpXWlz+iHGUZczZ+64U8P467m/d02SN/cBmPHXk0Fwf+qFxHb9sWpZxMsWJVolZnvo6zZrshCykzmx+FQjXxh7YagnxHWFROH+cfAUevRQcCvoXuxhIJFJgzHvXST0dcw3YREPFtoZx+/boBfDssYSEujS+vgFjvq7P2GLAKrb/o1hgY//BsN+bDIyEi3Tizg9KZYs5PnNfs09Hp9bRZ5wxu3bnY5Vqf+9ECu/NDcx/SIRvpmFPbt+wRY3W9/X23+Y2iv78szge0Nw9DdrdiZFRn/Sfx4fcztHc5kDreK7/+keTTk1FdAHOnRoe8nMe5h1edu/9kSwj4jQSz9DJSov7hAdD/TDCx3hPm2Zs4H823TELvr+j8rpAs4NFnFEkTbq92xEA4esyXuR8QEeo/vkBE0H4g/0GNCVLmf+bPhgz0lqQrbunrYrnL4vcJmAIOJzGFz/lRdC4Xw7WOe3YkSQQqMgMjqwO+11ri9SWX/8BFw1aYDvwziyAWNt2gd8vcCjeofhNF+Faj9F+cfD7hjbkdWflR4/B9hdP37E/i2RrXteiBAfS3AF+gPFM39Q/d+qzhYqP6iwC7e/r5I3wf530/pqkPuF53pjnb5edxfpNxWehCEkuZ0yntkEnKdLOwp78EE6cKrQM4gvOZdTIoXXhXqffH3UlEUigLe6mo+qgHi8dF3hfX4rDvm+3AE86GRBAcezVdk/tiMUf3sXWH9PjLU+1iEb1/CETpUaNP3kMfXvhQ+gT8WssoMEJjIT5K/RvEzgfH4Ddduj/OAw4wSUTlhk7KYg9ZPE/O3Ly+aoNSQMRX0J1d1a1g264y1T/A3MPdGp9uhWPW/0SC5t6Lu7koF/tVcUVSivKw1V//UvankHRoqhf3fjOm5S27DPFgR/ylXxUYza4sdfzIwfXBnz9DO+heHbn99GnKvr7cG0LtfIzF/41u+/jWToIZALZeZbuP0N2bSumnnbRrqyYYZaYd/9SQa3WlcNyzrif+tqFl5ILE1eojfR5e66k/c/hryfN7hrLOGfrTp76WavzoSXjx3ousKzfICcf/7uKeLV/9Q2h8/SBQxPqP9sy33x+ABNfp28t5rCfuyxnRHkNBpcs7q+r83tQlm16FEAVjthDek2u1NSwiD/uaTJsDFrSO9mXz3LGllJthiL1JBhSjKpJJ1JLAYd2SsCcaSEdZHcIPq6HSnPWcRYlOvljpZJkDBmnawq+TVrTGdmNUohMj+d8fS+eXLdr+oOV8DDmryesFG351TOub+Y1u64rTvbtNLAezr0oMQYsKeLfY85w+vrmwMZ8RSB2qorc49JlPxe3DoQw5reorpscpO0+IfOg7WZzdrlvZLSbU9Y7jkiZ4LybqNTLFUOGrZbaIpAsFuR8nAaMl7r9ezm0rSBmeS7WHkZBKKALvPTpN5LpCCwpnouYTpNtM8uKh6xmIZyPOYbapcCJvzrDru1vqMS49UL3BvHWD3ygPFIgMOmk3I4YiNZMM9m8QvIRPqf8EQFUoceUjmJjX1NGYJTh9QMYuIIYRUGW3W8UVSmp0uhb2qTRvep3xbvZc0xkRZXQOa42OLM8bPHw5Dmx/NSztJMzvwuybInYhg3FTxVJQqP9C90IMaaocptfJ9meY9gXkPkHAJwgqpzUp9ybyjQ/Fm3ZbVkJsH+VIyOnm+J8Eqerq3+F2/i5FaWMKdQE/roaLt76Js+6Hve1Rf5vG6GRLyn4Kmbnn1Vy/Nw+qvVgMPU5geFU3PEvLvp9tq/t+Jm7yEzVBqpUL0FfusWNypaMI9Pv92fRXflgFN/RymV/n8nKnPbwBCHf185/68m7rIgRT6StSndcXv2PDnOq09eOmPNSFIYDdlSa4WugiJB2/9saQvWEQ9xg31k9+iQUTVbgSw8rSnSj1HoQBvnxLh2d8ROpLEwzC8ZWnw8WGWxCKcTaDr96AZL4/CGcQwiMSLdOscAhB9CKdxIZckYR28YLLCDpTDFCyh6yOF0QaVhuJy4y3FGiBEEY7RgkCf1bdPM96PXeStUrNS9/VEhZy54+wibUghUUhZgowCYiCIg05OalLqdbtXulEpA1ZsYGmrfmyvpXbPEqiBRJaFtz6+JIPq6bDDGS0ehX88jnO3PwIZA31EH4qmqwFfHSzUtfRJKs4N5Gb6G2VNIf+mbnPzT0nXr4p0ixjtO5+aF+fGhkb7eWej18qw8Sduj1e3c+XnkpjczdH+bq/Jxy/gU/Pwh6C314NaD9B1evsLtB5IMbfvN86t99tUBODyvjjZ/dOKp3AvH3/APhg13S8kMUM9kJdL3DqWQm1/cY+yWef+ENojWJmpjtvVncgjOSDAJPoewNJNNeAAjUkIgMXBHd/P2QWRecRLako263kuYToe5s1oi0OweZd3hzd/0LeS+pntt6WSoK6njzUiFEeLP8n+B035u5T6N/Q3u6fOfhjlVBtUYceGgLwC0SCh+yByD1yxUxZjE1Pef3GPW3zJMq57kP5igd12noX8jHDnQ19p5MgXrzTCOiqphRNjMAkLgLBjBzP3J2730fmTHiMklk88R2W3OvZyL0hKve2wxKOhLeZcy+UsiTlAVWO8fUV72Ng8aqtThaLu5VwRXFl6t9KGZ1JnC+4xF5xWaHhORnd9jrdsGYd8yWz0LRSPwbB99q5Y/13AUsPTp20sqBjMrRvP6mbFA4Hu/xwf1UQRkDW602/N1sxjekdX255RZXcNtOQg4YsjYYh1fSzYf4/NWG7roOz0EAVA8hBnu/Ow1eot6aWSal+4ntTUFeqCQuyPFsIrITjhjQUksgeJS4W3ED57t7hNPbuoj44hLj/odJthsvOLunsxTCMiXRlGk7cL+U/HNbcD7pjo26JmxlLMQBiWMUNYcu9Ji+yrUvZJWJkj5rXAKCRATr7uMVjU71DniQhjO4Tt6ywEdJxHzXpZMY7Odq9JmNUTx8VskAPpOl7UwQA6nzxywIqNbDOyOGkupjMXcBGdEJr0AxGCGUmpIxA6fdF/jvDlj8mcPS0OQXwo+qYgVgJ5gTonBlltTa2yK0mNyOB346/X8ZBjX5YtZK2TJq8AbD8VKxM8yD+ubKKa5nH/dKudSsjKVyXcgYbaqrLZK+SkQZC+z4IROxnnBU+B4L269JPYoeKUcMfvJTvXhwFIS10hUPXXprmkfM/qq+OUTcor3pKxJgaOFua7lHZ2uAyBk/yMaEm06sESjg0Q5YZlVFxkdLK377o1iZB/11g6FdZcZ+6QpoXT0u+7r2waxeN4QG9OK9DRMWIg47b1dKuzoj0Y+pM3YjPWTrUaoVsxei1MTh1hJ4MUPKSH2EftnP7MyCOLGhnYU68y9giffP8fy/0dLKptOOAkjXcduS3/SLJp+jl8seDKnN0oFhBd9UPn6kYBz7T65YKgUhcf95r0Nftc5gfa5jVKTAYUUSS/QJ8JfVyz0bGPyDHPIKsYpeMRZ7dGVKazsrgCvFvRFJpGd0LuV6Sj5GBHoEN/SRm+ZmAnciGlX9R2DMLIfOSpkau54K0JkPND+DUVaVGpJKQWFVWzpKqqRfJ0l/RW8JAN6UNJ28insoL9H/ETIlEmX3Gke1uUuRFcT0UhUTKzoDqVwXLpt/gLbObbynJOOjZekZhtROwRxvzULTbd7kF5iQezjxtuQwLfbli8p8tNeEKwA6TcwyGf4KQq+Ra99dBAIABn3bKoEG8xBR1Nx2d6TAYA6dIIj0XjRLOQxuCk4APHeaw66N06RKNVZEeQ807tyAndXXt2Z2tQE0Pex3TySKRj76SvUJZSRpwPRenb9WWuCXKeNRwujXGg8PPigKIwgfV1ggKH2VsFYkPx1PwHGb8MuHiWuIN8oQrZpdaZcqf6QRVar5GM1JXvmRab8RE3XlAefBQwLyR8ZWf4xslBTz5eudxv0LU1TMLsC75cRnwuV2LHoCCBOXMgava76UUsJ4j+TRinYZEBfIFhDikxuc1OPjQS91C8LkLtF04wx1QTscTzNdxYG0Hka4vugHe+WrqfE/OnMrKYW/+zf1GsLWHT7vrNDh0oK1zd1MTu6GhZRsLnCoe1Q5PDNfkZpt58lZBe9eVLVqkGJyOZEbvdLg/TO3qpgPOxPlAhEHxoAmGqyySZIvF/vpWrLphkCi7vwmzzu80NVRc8nTi7uz3V4NWrM4dLccKCufm+Tz0pM0o3Bg9gjjPyUydm1gVuMzcTXPmpBIjBMVOqDEHa7IeVEI/VXEHMdygDLEr03rZcOUMyuSITkfsmn3JDvXbU3pzsqbMPKURfHyWK6lGUeE1Iazd5h65M39RosAlmgXkWvzBCM8CQMKYnkcw2cAualWPkmKNhzG97AVWCpIdYNDyTqwBQ4YjOwzk0NBPk6ibJxWIzhtdhq0hyTrpeTNR7mqg9tbPv/CZV6xfvaY850DrEi0UjXq8Ha9tC/MnrlETwz4FoxnwHRYbsovqAyLqdTa9k5o4xoBenBLAz+AquwlDrkF1Hx+hgkzkoz3p3bO9Sm55eSBNoU1lmVdyKlClpYRdN8JmkYeUT9ykRDT3WUBUCb8wmABRN2QFh2PRaBT954oHQz5tOga+ySrDtrKMO9sfTrJdaXtQvE2oMadAHUzL76tJz3xr0YH/mv79oaChhLoLp0/gLK96x+OCDcsgcWXSWUTlLZWj5bh80Krc5KC8vOG75HB7wOcEwko/Q9rMVgR8neoQIv51D3fHPL5wz5/S6kEhOSHjheuHmyXq+f/KTf8ZLJ2V6AyLjpCh9EwThxkT47Z9/059fOPMbJuGF64XbbvpkdmOotNnT2TzffBigVdBNMYWPoFtLvTrp645n9o3XotZ0UxqbPq6mKuvBRoDsWjhA+GA02PzFvE7AuojkUDM2A0iA/89RuT+PytkfEgAdzVuO4Qt0Db/kDfMkGTUb0aRVqEs72rAHMIfysj/YPho9iHiMsQtbkVJejno4T8vPRrxXWaVnfTs622MN3vk+C8QCMrenWBJapowmN/lCw8t78XW6Ocx5dcKLf6TI6uqsqs6aaqSJVxTV7Ax6eLbbaK+/k2SlBE5+eXlJX6OrVUbpBfQgRF26Wco9u6GbSBY+gxYv5wRSKqsPVZjerXa2H1sXT/AOrnZqPujFet2GNZiQJoF1tqXluVX4xQy09rLK9AnpEJ1HeoWr34aLyTZ/R6u1ORwt+CNZ5aAOhrdyhy02cd/lcdRPFPLR8udedvzP4sSN6CwjX/InyrOITKqCwmKvKiS7ZQ+Gf/EiV8lRrIlYZJ3OsbslQogugiAurXRFjCsI4iEeCo83CwwMhWCgYMGQtG4BhAn/OYlJlL6g9jknsparluaZSLGawl5J7v9G5zpP1ygiIfESm+kYMEBmwsj28yRVcwU0GSIxYdk7jBOFWxZxhFmUUUWIt+oy0W0O7oj8Mo7OhaKWDZbBHG3219lvThClwwoG/qahDl1tWgNvKstLAuTakKgqzrYUrXW5KecSraNPaF0s2Uq353EHtdI36G6jmGxG0jMaVWFEcWiV9VOHRhGyO5Cy9/NvUSTy+jbbwsNyuxVY1s4YEqa//0ZxszNmwJFAD9JH2v29qiU5wGCjZtV7T+hod1OUQ+/83Szbgip+J3PDL/U1iS3DMnrYJIr7sG/jcNoCW6UuAj+NF/UwYlUTZ8MpmBzuZtSIh9LXLYzfdikQx1S62E9CamBPo/QOGvq6bdl2kgFFUXVuf8l2AliFWdp/f6VitesYEnSlNU06WvJQAFBs9KAg1QHNEqzWTuxMLQlxCfq1nHNDl91Tv2Ap0r0ElyO1rLwzQfPO/nXn1cIboyz6ig7jlWCR7CSAZRQQXRU6H88mNAVlTR5037jVNCIskbYzMj1C+aLtj38xl2pM99hHIsWLyHvb5n5xZCOMK8J/HJE/OU5aKf9anhuVlLMZz07czM8xJ+cu4fE3/If06zkb/lVvDhy8L5CAGk1d/OAE4yNSi/eWAWjv0v420GHR2SjfO8grqJ1epOoj1reKE/dgg1D4LS4NkAA4y+Kb6A04I/Y2aF57Ib2z5G+OCs4IerCcaNF6r0paLaFhts48CMk8dqJzbQMybgul350VSuJAdM0z8mOZeHug2xEVRv3HG7hHQbkPKIpz/r/P8M8kn+rJ6s2Kpmi25IxolV9LJIx5BYx+gv1Fg9snfj8On30+RuD37G/89BhqnztZOg18UibKTS4A/TwsgEF9XEstWRHTyY2KX05M5Q8+eF0tTP34z+Q1fylq9HUVfjuVO/w3tV+M5g9BXJVucYFzI2SuXfwZ3p1PE1jikpmUc/+0sBxISvmYtH4eJWuxzBgq5mfAARtWZurRni7cnL1sCmryihoKc1g7fQ3q6odUD7aS8B2hrGpuzX43u045VZ69Z+OJ99UBg90XlXuN7opJBsXGyvduOhmHqjkdMhFVb3wrQwa4aPMjAbmd5kLhNGxa5Qt6HUvkEo0E0fLihmn/DPkrsK5+W6P1qs9utYZqtoAnxA6X0PiDlmyCX6VjQscxax9F9QxWZoLVvVhPIG4B1PHTlelJYW+yz94SAtSOe0m9YRfuEnmAA/qQMg7Mqv49eAJNpsxPZwAIwfxCigTYtyGSvA2fk5RJ6Y+0sy8nqxKlQIFdfKZydYjF4UeLOSThBV4Lvc+FaKOoNwqWMUIhTa8kFKE4WDwGmBrNiZUQiCAULiIJCJkWIbpb+BebNk5dQqCQfTchEwsPFzRSyt+gRSw38o/aemufxt7un5quk03dhOwrfxPmFG/G3uYF/45Na5c5qGoAYtJrJrKxWyNjfPW76fKm7XZ+dKmNQ2jm+5p38+XNqlnvdAG65jiV9Q0BCUYR63uW2iyQ+RCRSzZGHybkOTPz203MroZbA5Zr6laDhIb6Yai6uG9jPrDZc0qqSZaXa2smqmmef45PU1vLW3U45MyVYG7BAcVBZ29/EO3PvziXwsc84YUMq3RpGOAoD4l1SHyHkOXTbjM4Q/8sajleAxbj3jKeiYuFWK9Q4feYgbgbBSKP2KZ7mHoK/UehPo9qbaagWZAL3vVtkvajhjORTLkNJhLPsn1USDWvNRuC0ZNYZC4QlsQNLAvU6reoRT/WhMbCQYIipeFwz2winR14GHkLotzpEeDebnfH2bWycayNUshjZg9macu2vfk7PbmrnY/ViQjPbXbb28qAblBho/SHSrGGIWEG1m0oxYWiuhXtHwK3s9XzrFR2T67pgjrvAQFh1bjGdr5VO2hXCq/Br2A5JAutUr362FaDrXh2clQss8Lk8yIbby0QDP/Mdbi3PO0+Kv4s7wM55LJXOT3lDtbAx4aHbkU9GWq8IFAYY11FW+/CgaYI7jXtI7XQbAC1cD7NVWjgUgR8lQjssdz3CR+zVYzC2ZPrftWVrpU08hUQht8qGOpbyV+1a7sCS9yEtvXLeH9M4tRZ3ZRLygyFyGvWv9zapyGm1KKV5ShtqC6UlUBSXrMrSU63Ej2YcgbXCpBzMTiD/NbCkw2c27b7+m5iAHF6F23rq96EK4SwRLF0iBnUDDebxOCfSzZNrN6HxOKfS2xpNIGrDPKH2eqwLto2g5uhkQuCtO5mErTfPf3hgow3wQYUlZfRjit8ZC/qmtXmV5Jb2YmCqJdXgwkTm67WgQWwgztxcHnBFGVOZtRGGkVHV/SHh4b69Pt8C8N8dORLdOeCg7MrrZucQnTAoM5S6hCOVyotfR2VqDONU1+GeArBsREX7CCRB65QKmyhZLTXiGB4r1QmhMAlzZq+ntDTBRkcaSEFvCPMTAbU3Yi3Uwjc0LtlwmnIrdiUmCigZTvsXdVZSjXKGKoI+w5x3jIU/SqwKScNiHdbbN4wA6miF3YB2ojnUqPzPj8xRUaT8PmM7pA9EHx5PfqvEoaapIMu7kHldzk5Rs844jBye3bwRO4HOSgkgdZpQkLbOsXqHUqtxG0SmvAxgsmxRDG7BaddynFQ8efi+NmRXD7qvAu5zRTNKgjwin39NaYqBjXqM9QHMVez6fN6Twh/x4SdxJZ/GtCRU30Fr6c1T6SqTmuwDiDgpXhjcq4EGZ6gGBY4FSVLETik0qMfZ85nrlDHbi+oNZJNLkhjQd2KreA+UecQsJES5/mr43LMoiv+tw5uGE0uFsqrDeVQs484X6CQCLDk/RDAyQOajc/F4wyg2juZtj2BRND64SH1mNdbFaFh45JRD5EqdZraydTcPw8wZHFTxDFPMKa+tXfy7Kajef0NzXYufBw7L+JyMvVkFBRd9EAfSDuzmT7TJBqNrTkmX1P5ebnmtJ+P9huOQECNw7ITH49SSWd2hL4+Byx/L0nM5yzoHv/kO58GgX8q67xMKYFg73P44Nl0GzMhdfHNlwhbPQdokzfOM3DFqHpMEEunQzCkuN5JAKRCmn69Yp5KNEG1PLYsLOWzXyfhDYkwjvgTRfAg23vKRNvZGp11MjFe/kZ03dc1BbtIQGWa8s75/spE02koLvNa0VdMt4oflPH/DY+LgyMj72g6fV4c/Pc7qPslhIo55ensjpY5rU5Ccp/yxATRaXF6ZNDy/ZWoYjmzfxTcUdcPZf7Jp6/Zl8QDV638q6xZv6MNfjjj/9a+N/Id3QUSSQAPBiEbqw+xlyzbFHCPW4RpRZr2sqmLSw6maI9ycggtQOscHJFL9Ywsq0KAsrda+ZLMHzNl0QihQNz0iTI5e8i7ZltbkBf16Z9mKd+MFPUUWMsR6MADEkUn8OnmE/Y1SDx9TpCo0KWF73ENCNnVVNGuaB3KSrmDRNAOvKiqo3R4omX22lLKV1NufjdMJ/n9rRgc47cni2afPaddic2rDD7986MYvGhbjzXW/XrMQ3NVuEc6dMGEK8dkAeHzbRx89EvPUPK2u4uRcvDpd8fMXd5krJ17Jp3kKP+hp98RobxNGurglnt8OxYH8d95FpMvpkJuB59+Vw3a+MZjn8gz4b+/Ex7K5++ot/T5W8j99Dvixb7O59NX6TT50h6gDw3B9EfnmUcnzR/hwcoEL3ZTOYYGIcYKrqnZnmVHVlNLCvv+J+/Cd0nUBZ4RzEs3vht8aQm0H4814CYob/w9VgnAb+Wqz0EtPtCAO9kYzg4z48W4CksxZnoci9i/Z+Gm7ItdCR952q1xEF+j3KtrDgeh5BHSm/AOE15gzPBROAdpvoji6/T5jzOspXc/3djgtUX6CuksbGZfX/Xy86vIuXW7fHQWvpvzL0OQToYIdJHx/oZPeTCF3lnfksLc3rnigcOWsDwzFjedsTGMwuVplulVvPwh6bI8YMk1IhgvM5fejgI9imEQNNHwgpTe+WfKh8Hj8Ac9hWVrP00i2c97X0WmmIz6NoHvx2ZK5tCap+lDCATwrIYwXKt9rVNSdnZYonj2GpycP14gPGfTKPZySnM2mXWxuchC5PPdWvkX637TxGed6/HRVI140np/m8TO+lB1z3YKY7aBhV/O0OVcvwoUUEwyc7tXeUE4W6NvvGa8+VdqD/7zMJ77b+gG6qRvJ/n6dalCDMDSNxf0AfCXreKkh0PG74+8DBDZ5SWzo2lVtzkx/xl3AQ/0+ZMr2jo2CaQgLHuynGIvCrRoPwKhn4ujjWQQISTv1oFk4C81G6dzgfG4K5SLrQithlGsCSAM3BB7I8bAmT6kLGwSVOV8t+FTsnPyExxuqmEchAQTg/GHklTkX6sJTlhjA4XZD2e4bw31oh/MqRJhCvy8SCyoVa1TWLkyJCzsp9g3jZXs8DxVzfXgZlEIHmqkhGHbd2we1Jeu2Rj31bC0hC/RP/Lb8OeP97wNWTmdEAKlpurnj/pDgfp7TsYXt3+B3Ty/ZzZkAN+yu2rWAj1sagflJjTZ1D4hIZDQMg0fHAhGF+MwMw3/znz6kpAzLeLLYX5s4t+W5ThUkU382zAJcRer0zufLiJ3g+srHGwH/0vYNBBvNOqKQdVRrQaOzAlfKm6h2Zr+lftfL2T5JLqXhV70+ZX++mOMAyy1QF72/jyLEe7MdInzSDQbZ55KV7xR+NRI5SF8uZz7pkEyeTuZJxIY7arRmQkyKO/S0/pJhpLk9+cxt5OVliiI3/5fL6QAXlJJtEYSeb/1MEMUYNoiEtOY5D/au5OKenG0m+32bpNipvvZ0Q0BrtAVbQ7c4lIXZN5DBSdaDmHPIahRkR7PuGHEEUcLeQ43FC1pKobnQCx9hUzLQ+647iyc6nJ7R4fikHDtAuL9Gh3qnhHl+mNNoAkDRNuZETuKTstBUnfQKQDFEkDel2en7peWmaRV8ReXMP6X2CEkjo+PdeD3SaiE+5j7HLjeyb+uTuDXrMkH0N9gr+rinc/54ZlFEaoCA0ffjArpoEHq0p4SdgqKg00bG3yohb29VqqeySxtdMcI/jSSEqHdjHLH6BkgiHfN+bqHtOoBmMDXxArsWUTYcU0jV8fxuCL5C6dfDkh2DdhuHexQaGiZNJ08pQ2Gu//5lgyO8e8zTVgJ9RAbZ6FSvoQWqid0+ilvehX5Il+zwqZcox5do967HZlFq7Qh6UUMtMhxMHtvULKBsb+PtNudd5m/g2xk7sQ2G5H5zco2WpMNTYiBlilYjzY4AZvZELqvZdvLM6AefPIdyTrcffrdMBvym0eLrJ/l9eFtSjhwuNjG+4Zk/5lKL96mjAP3pSHercN5OFJYpDM6Fer1rjUPYhx+LvVtoD1mJlMcgfoz2b/NlDpw93/70GWxzf/+Fu7pq51MCO7+ePifvv4W26CtDJ18R9S+dp/OZ0b0vtan/8VNnN/oqI3NTefr6KdWcaWetm6fFUm5Uu7OvxOniwGrSL3VniMTqz8XO8DPbX82f/SfauRdLZzMO+QRBBxYFYuOnTSx7vkBBVHDEnOA4SxE8WCawuc+XABm4z7iY6IjILizJqSt/0iYqeoqbuaFKVVtygDwr9yi8zvyLu2QwIHvs3gSHcJINZ7pIFRpXuT/H+8HBtghJfJuOKHAIn11coZjCF6WPQhsRTs3z9/tyEDwYe6gl/cU9B/t/317Hn+9PU+f3/bKs75wBXPXnWBraIMtirkFcOgg9IW9TNi9UHOCrOxUdMyIpB6KtnZgyoZ2Jene4P90nBiZ7ES2vP/osedQg9b6yfzQQZI7SuQN9yKstT8PjPz/XZx/1/9oris711dm+KFOEiqhWDb4o0F/rtRPZZZ7f0MGrqREUzZBSLAK+v/JeuqzsnLEbT3Tw6UIx/lI0Kj3Diz523SNCUG3rgM1cJZXw9MrOxEn6OGPkdUiNgJ76S1IMiFTOFv9928SrvDzR6V4K9oPB5XzvxXP7WT/ooCQHX4Lhi+xVziJ/z46HMBIn9/C+bb7G6kGEdkYTnNjJtkmg06oU5JaMjQfv8p7fvcaQEfoENxDfPglXbYO0jOS9DB4HdqBObAUX+zstMmsgpDCqKRzMg8APhsnhI35tvwLdYYT+WkYtI/lDWP6wPt4fS4RQ5DHTe9ar+lcY9A7f7ixT0TyVTsorEUs0yM2zNX4p+rNVoxYsIFFgA7wRHLKSvOoypUlkX0gonSYyThC5cI/T14KfUa/4jKe/eumrh0RY9hp9XvNQARiri52qIvGDogCY4rnY7IDJhBD628/eLDYNuakQVxQfwFtHcsLJb7/rrFjFfw953v++8eNDHzuTbSS2kEUDkIUVeTQga4JtBXqaTVchZkon9ThW04eOi9n3fgurPQsVphFtblQvLjtkFu+T+AgWaRwcS65Q59ihLoRyuiccc5EsAoAheyXFTUdAfjShxlXY9J7B41DiiQH12DZmqbhspXZ5xo41T8tDtpiSIDIc4R6jMwfLvfKauAmQAxfzv6Go89LHjY+5h/VrLfgtWMFAVmBWpOT4jtP52lEmJvBRNhCTetO6O4bruxno7NJUbezS6tm4CStLop04wZVhtQA2FUsF/QarrmlAVQvRTPHw6mET1rvVdzReEmkxVpoMoNEWmWBDexpjHm3l/FKJJZMlc/Q/147mnfvzVAcECwTsQxzemoTR5YPsY9fY0qghacFdbDbLLch1TBNcL2aQC9eUZXBnBlWgRRqb8xWlMTzAi0hqRI2QDWAOoQy+E+s+TLuJ740/E8LpRzhVIYSvhH8RIW2ZeivvSwwg30JWYaHGvUo85y7KrpSSflBbgHLtdd/wgi86tSekQETvjialtLAslXYmUl+6Zc1uXka3mjZFusY16OSOJaqBtGcDEP5GkzuCy2/n38mw07BeyJgrvNLeGXDdU/vx6FcvvjQ2bo8MG7x0YFLEHMSyaY4G5y4JzsJra6P+MAxKt7FujJn08nzrGOYf+T6aEyfbOu4mGwGE9n70ShY1pEhY79KdhCcd2UCQMgpgHOFAYFADnmEutIRaNsecjTSQwBwB2gS8YZG17XoqHYSxYxbVEBssX4M0JpkLBwjX31b2eXNByBDqZkAYu8sd39ORHSsOw5wMgRcIhZe7KQuN6scjO/bwA0QyFgvgwQ4RzWIJGYiYPPD7h9TRGgH0jIQub8cOoZ08Xr5Cph8ydn1rayXXehB7HkcZC9cCpjNZuuBKy61cxUgl5F08AJQDERqdD9mAUjXYgLPzRNUTppXb9OowCxGM6B8xLVQslBX65YBy6pmPQiHirxM+8OEgSoLzQI3V0usbQibrCAuIaiLKB33Hn7mzcNu3UwikFNtscj3SC3p+X31hiDFbW3a+pAq2bvXx7LKJzef21KUwI70LBHcq7PusYPtr64TK2/eQOBKvP1N4OHCTdK+xwRou3qObdRJ3bj29TfC7c1GGgWa+9eiUSCH2lxBBL37HqNFLg2RpZJPTArye9btj1SbJtgscD65RcYZvToGJ1TFs3oef5HWQc7m6aB0ihxzvu9RcHEcktk5Kp9wr4ZNuI5Gi2IEirYDTaJRDOYIiVExPN2ba9h8MdBDzRLhoR/9HhnyEtKbX9FAIVN8vjCpGQP/OhNHOZunKwnlPEn2sTiiuRVbZupIK/RrARfK7XLiTLmcTMrVUL6sGO3DNmbjSTUDef0ngFmjmaoeogX2TBzw3I/B568RoQZL+zMOvwd3ffgBP2VSVEhA8Sl6yDH00NWFFNdkbdtThE6exQx71dm8h4UMzoVew4oeEPkdHhfkqcDcfasTeGjCts5KHDsN65F4iXt0DD/IBgZA49mFBRin7XkHyybONjSToJlRVfFOxEF4O0dYzH0oUKR41ilxTbB/4XB7bAfHMWxJc8ZtQlQhFvdI0+NtRBV2qEj7h13dbbOaYfPvOKBb9x1OmjpI0biFc6haAvO7JX+VBXjYpuFmRtoj4lBX9Uzgc8voNIpoO0rhsjPiBShFGO6JWc6WZAV9DUlyOrG3ASgKCBc2ZVX/JLS1tEjXREiQlucd9oAaBF+jbiznznsFZEkZ47snHO9NHBC8orebQZz3+rAtwfPjvWIWaNano3KEj33Pl9xRgZ8LUSSk5+6KZD4ogL2VewdOWl9sVAEn27OLZlT72on3lCyE13Pk/NwzvUXa9Uh0lPUaNIeW0EY6mpBIrrULYM0UKGA4esQ6Ih1z49pU7pRQdT0ISqGoMQyoixX0Itw+lDT/3SarOVN9CHSbEwxAL5pD134Jr00Iz4c4YUYc5yIlN6kH8NIuK9bUtDpXrS/h2J7WKGGVOxZI4qx9Od9aWF9wjm2lQBOFppjnBCvvEmMKMfRq3iQ6+0GM3d2fnRjLU6nDNvW88b4n6nDkDHKLBHDgcdTpoRvRkwFyAak7rSQnXnImlGc0jzOLmd0X8QdyQkDXeGKJrHUbF++Bd/d8+XJ7CMjs/bxxm8MbaUmPeek4xekHmcQ01NpKp7e3RJbiKgDHI2lW/eoJUcxAG7vx9KILhwBu5pQARRRdJ0DGr6BaECtZ+LsMTEHhtxp5oSNhQ5p6qGVpseeO8Eu0+mnv7FT4j2boktPoxyp9xHLpC6jZL4v2JcXEEuqZDQLs1jl4n6ZJYYtLzc4HN5uEK3wZPFRiYuZn5XfBcB/CvlSkqH1M3MbFMWEb71qOi1hSWQX2Gxxr4tJuzOZR2X+Dvg3jFq+545k+CxjlV95hNKYKrELmxjHQ/JlJrfhVb3vEW7I6V1vR+MF04Yoontl+gB1gTRDbGo5jq5WW5Z38Pb9ovuzjS/VuSjVqppIezTx9bZD2WockCkgObiq8K2Bxb7puRLxIH3t7q0W2pBgXFC72FZEZmLFIZWPU1sjCJIA8BnOU+YWxVnORsdqesjYUrMGKFLWEGU3zzNzGw0X/bN+2FYKA9HK+DxtCVckJN7yHkFN7x/YFk/zkZZ+m07FcOstyi4Be4ReR7WheJsXQ66jAuK266giQQZX4Grb49XdccOZwYzNz+7p0Ib5EjyH4z+DspVxRjeEaKV5yNcuZPTHgyaKXV+oMcobL8jNzXEez/VR1FJTQddaONQdhnVdzMJ5wx+V49ycCBbEgSYz5SCbYjmRgIEOOV3R15lXIoAibcysAMZi42Ouq1ayecU4WgKItFXGKs0IgAuDLKgvzOuHFdpvUbt3CUNFi4/9fqJItHdERvLMSYnVuCwHZA+Ku72jVsLrPecAvUi3/xxsZgAnUNR5THsqsp1uhoc1lzpmlgD+OJRKUbYreYWp91F+xHj3Hd78l7SL1jufLnxNvzJGtBTNLNuBUtnOrhNtaJUG4/ZhbhT7JiVPD23zjPoU8b2caH0Jhtr0ZtXnO40AfpRy7zsKepYtOyNxXzcjrsAJ4Ctc3QCzReSlk7fvUZjrlzhGwsawWLqkvG2ox9OtjsNmQhJNQk3miJ3qgNAE1RMu8aRxkaSi+5qC13dRo6Xus6mWRQAkHEyJH3FH4/15IX9NY5oB+UEpj9Txh0cywFTo/Xnh4ERgvXgnNmtAdmuc73DK1JGvhFYX72VNatpOoVVhPH56XWvft+yFoKsowxwdJdxPfChCxEIzUWz6EoQ+cwHOQNTnu73yOJEYar/GglbMXQOr0IevbUi17Fx4OJ0DB2LCZ3Lux4sMAWs+HW2IhDXHCPNRXQsiSSRsSImewNDB5gGNLXbCX5xiodJk+1UYPCtuVr84LQDG35Ry9OtWIfNtBJlNbKvle+PX8MGLVN+SJSZzPzmpgk8cjCJuLb19AUC2KzyqNup5HgE4YLy0q5jt3zkjMJx7bSqclv/wQGRKrVKrfOmToG2VjoXU/2oTA6nm8YwSvrM+gE4a8lmKtbZ6wJcvAheuAzUrsFO8h66VzutrTzw8z6r4dhetetfGusCH4f7oiMPhudkhAtig+MMDu78bEDiGHd+GWnnPrR57IV9tCkvGcsvDMvhY4aStU9jMkuD6gmT20ILRWua4/sWJY2M95Wc5N3syVepdyzXdmH/QcMlDoAmCecDzYF4L4ULzg/rzP2N6TtQcFecKgAMlBZrxFRF5ZsoCn/g/TBXUWD4BDO/hyRfNLw9f9H85D6M5jU3CyjI9jp/HdxUzAmQ+wr+EBbsm3RxO/v1GTKiE5Iec4LkRWEG64QlkRPOVZ+Bmjit09TkB8nG3UCxrGI5dgdpMPM+awQKhWICS+jabzQTMV2MYSqWXEoMBBA+mjcivM4ythn+fa5ea0kzg6wzktQ9KnYBEFLYfObOeM+/1pNWHnpqvY4plHlyJxarkDHYoa/Fqb1uagQjiStU7RsD1XiCUMQaVoblldPvISmAa2sQZAvTMXAGz6ntOXek1u0G0UBsau0tL5Z27gux8cnJfpX5DgvPqTGiB2zdVAk6hs2qQY41vNwDHc+VsCPWLoLBAzSniIzCCwJZgwXeCz+kiI8abFPDFYEiykWpyB0qAKoFlxOcFcqCoOZCZg2oJWv0DMhfOE2zFU7M/1Y2Ws9vU/G7YpEuWiNonWsh2xrcH7YLSyTwrRW1oSRL31LLDhoYz87rDQ/YvpuvpfVw++SGxq99x0L+IQqYeQ5L13LiqUJWgaKoNmQEN6JjTA3yG/OkrceTuDuVWCEjGHarws/ql+0CAfhcdwlvprTOvvaQ+7miaFsvU9U5Wg4y1zLcZHLwt8R31hG3t/V8FsyCi+dvcIr6i50hEcMSS273vOM64hYbHPDp6DjKu4q7dxs2cISeyxOzlm9C0SR7L0xofwBW4+2IseLkX4XMtgN8GeYkPIMfEJmTWxXs9kYhW5L7O9+WXyvcAsGYS4ZpasZMYMxpxhAogh0e45e5nCJRy9UHsGR6/cFO0I4z57+Hv0FAlyl+hKQl4eNR7hs1rX3r95U7UUNTNFpCnIYvmLogeR8HvWqSM7/eCO3I1jsPcbJ/N8OsKad4k+QP0I+HjkifP6AycHGtgX1W0z+H/V2agSUWM+Z12+7jkFf65D1DnL4kMnSqmQFAAmMu0euJOdkGdaBMF7XzAwTNAnYcQWOzvEVkCYVHMYx+wi0RM8xtm5/uUDvmvwxb3Pj4iI7tu9CtjvnhnBcjTtlpACTWz73PuM4h9O7Xsah9LoIX7UQ6Q1HsIn6i5xJ4gAOm2WZwVBK9cWskuM2UEANSBFXYcOMX8r93PMJDpAoatQx8pllDKJ1Gf+Ve4cFG9FWXBB1tCzWncBwoQQnxKKqBypLA4wJeKlBm1DbOV2MNeTPGe70BpI0wuij9Z8Bo6f+4pbmHomxpw0nKjnTenyzgJi7Jr2DHJ5OIRM8Z7hF3jsTgcpYG4UjzXTV866KWN29ERP2SfYEwO3QY4HWtTXGZ2NfLK+Db3E6LYvdtVYP3Gr0dVQpFdYET5DZxbjv1oipDBTGMDpCV/Q/cG6rZhBSxNkIkBpwxeKmdIYP5NMuk0BDcEzvHwybo72GPjB6dTa8Zy0pLV4/G99f2NFZI14GZgq2o5NAl/dwjSWppEStJkrS2g+KjWWeN9S8GuE8Tw6yq+jh3Y7/DMJpIdv2LvMeJBXqum/WaImKWMLMM44RIufpgwsNVX8HvD/9B+HAyoC4LTl5L0PWdkk8GiQlzm+aW10oSHkvWfSghxpbnhygUJwazyQdofH0Y37JtRHxOfNqI9OfPGmEu+QaTFJEvHej/1N76VMGi+ejb4Ev8cfh4NL+ztVn46z1IYniP2ShQZXtt1jM9rr3DkHPpp142us6FJ924WGo/dspLpTgz2SFAMVRtN4CI3BXkgWGuhTjbaa9sC0ERJd/TTpzJJelGDE4utkrKif+JYc11HDcI1XnP67qmUk4cMY15wsLJLs45HIfHadtCi0uzjbwB4Q+r6dQ0Bvkm7XcQpIrSqlGI9l15TZIAPea/2D0eUfttku0pgOb/YnjOWn0kIOtQDQVHgSJh/KGQDG2D2kGRZgjDQ/CSQXj3T3LzbDOYC0Qqe9+ubY9ylxRkbIb9cM+RztvHWNa1N/2eJl5tgbyv9ftzl5b7cO17Jmn9PJWU/2+jcc67UFvo5sRpMbFAswm3TIBULtbY/KXvRstoHzCeOiiFJBZiAz7uEQfT8s8NcPi/zrh6X46+aWfib+/gbQdI5NNJQt5SPadxHpD0YM3D8dThcN3atpCPU1BgzTHTtePi1OFXljYuJN4TJYPRhRseg7UUmecBNH1OKjO9giVEzmABmYExN/zTBFFhctrTtsZznB2FfOAmZHiFR24UNJbDJEZz8bzAlmhwc+7onlohbKNDhpc40XuDRQrvlNkak8mcgxN/xaLyldyx+ri3v6kZq9XMJ3ND8c+P4mlwI7yRKg2eKA/DzKpwcWYzulFFAyPtlumBdmrmHcSYpvWLxQvn8h/fbJdsO8MOPG2lhD0shQeRUX/5bbK9E8GcJQsJfpiDbgKFNsWJvB4lnSrD7/LnYU8N7fYQbRGp5qdKYpE3DJ7bCrdblWyMKFKkFJR+ZioHHDGjn9i4oH0OPUMpuJz2Nj4UGgQqAb+nLBqOVIZ1qHGQ8A6/Ur//E9rZz9jlq2x2V60JrPwD3AAlgMC2HglPGjFtUBA3Klzu5On/65SFjvB973lwaeALiN4hVW/SDRarPs0COzWuZ7VpHVRNTP3q50feFej83p0rqSf+6ZYnxhKVAkWft6Dp3sTNbbWVX+GSqTbIGwZ6QQCbV4CWPQeWoj01Qna2yJu8Pt+mSWqnteT0tw9lVt6lRrzLHAQ++WTr+UkSNxkp6n8i715Tg6Dbq1xGhAcUo6CdiDYw6f6Ls+KLy9GPeAdOQ4qeJVbxh4lPJJT+oTTspzbJ8vwHnpsG55MiFxhq1OfJUCS93KKD7zIfsj4UkQZszS1vS/wMhDU5Xzs6I9VkfqPGp+ZMblukRFQgH6Q6q7F1L41RkjKaH4DsGsTzg5gdZIrMRPwQlA5Rij9kByzPnSvyaHpzOhg52Uemx+copVGhO4BLjrJLsm8Cg17SIs9APuNT71EigCZSzP2WAnDk8JlRyL897q4gmZvOFjtKie4aWvdqJgh7F0lAsVS05DeVDPz0vP2eX7e55w5P27vQSy2HN6cuPWL1chNfRmiGsZ0bPjlnU1xsy1XKgLwEE9SajgYOFyVyeAbdQsGwI5wIVVPZpdC65osGf1CLkUfUCObRJ5ClFWmsgjI1s0r2nimTLepBzwl1k9NWtZPVQuAFYpVfx2gNX95sEer2gVD5KlCUXaJ1iA7/EsrPPsTSQE4nCR6CY+RmLna1UjJQ4MJCaLLoNWZnC4lqgqsqpn9+H8r5mXrmYduacXsqLXh+Bik0W7KBtjRIuebD31AyNdeLoQ9twehfYxE/7Q6sc8YLgi08p34Y3A+uXInHh1nly03MQhDrItBppxiPYxEhlz3i+Sy5WMa8oOSFBFO0+JnYh2uu2cnpz4TMG1oKIqoKUXMokSvH6F/pPAKEYrY2VFwdyiemDFW4L2qJwL5Eel/ST9P9pxCjjvWX0LR6jzezdcQQhJA4lCGiMCqUgRy6x5aHyI74VT7vvj9awMbpi+cJaQ0Fd3GBrQNeg/snUpGDTrw/SvTRmtENfWPBCLiGahfLllxrc4jYAZZSIVR4IsTN6f5/bGzK4A9Cq+JrDMmYVUnAuhDebQE4Dxv/3eYrWQ0bchmcvKbWrSG1fgw8T9iWeF6KCCwu+1K2xAxv0zRltVY3i59xXQDJDVp0Wjs3BBLZpcEjooZUBmsULlluCLRdCsVezn7vVXommNouCI3go342AXHcPUTU0OizJJbNz61qbZRoYc1eYPv7onaaDYNUPDeb08ExS3K/aAGF3+qOC7czdFEUw+ba2161fvqBjdA32r8HjwSty/xw6B99fmKRnioplD+uubCNUe3LcBJtnY3HhUiN3LXBG+iPvRT2Pfe1o5Rky/OjYv5RO27gE7I1p4IMQf2bEps6vwYy6dMcU5F60RGsmoo29C7Zt0tMa3KgW/Pg8tZPOuHSepU0NSGhvYUus8P1fBvPNkPze4sN2z46VlIrxh1y5hmB05NsENblDBA42DD3PeJcIybw3WNkFHYEI8QO3aycKsH0Z4yNz29PFZrbfkDwdgO3ZE337Uyl3lUyN9Wa9cLB2EMULmoeCBLnJ7I92PC3UmwUR++F1C6FTSl2JbjfURTp5l8XsCzPWU2cTuqLOopnc1H1imxC1ZSF4DDoZtRoTkyuEZ/Z4koa5o6g5jOkx+uhhqruDDMWOYBybmBzYPcIytt1GhzBhhcMDf3Fv/A4ne4MAGRCDlpG5S8CGm1uOEc0R4hnLaD1Fd7wZNdEHkI9gcvm3hOazBLHwaL8I31DyOiIsj5JErGl93Vvf7ELDCEN96nRN3T2vLznUDUgNGM5/gmR06zFBZmf35Mg2UN8cYYAGOEIS4PS7Y/vHAbiIKWMM6XXeLPXvG5lpHUFhlNgCPdNWIZM2nWSmPNch5PIleNtCXXeLj/hwKhPreV54Ez8Y0JtXIZYdAAajWsPyUT/KDjh9AsjHnS90zi78oDMEuaZH17ADBsUb1Zf47eMAKpxR3eWsPXolMpquqbMFdPAJGQMnOtCG+vPHio7uj39neILdqNuuN3qM1cZDHzFmJm8JJyaAmf0ChLlp4G3gZD0L3uynOoSWuMxe9ZG763kESM53Bn/c20e+8SvN1Afxo2gX/NpOhxj3PLXyG+iZ+Pm5sJ8iEuralc81pcxNThqLtrJXjd4M9hC2stMQ5uiohsT8+h+fc5qRu3bqIA6JIml6oJnSNEHQND3Ic4eBAYxHu0PUE4ZILUREBwFfCeNy2Y5we3UT6NRAHeRL/eMZczmh9uJNfC9+RbFAE3kH71aDG8XlO+QVNF07u6qaP4GJnI22LIgVeY4js/OF3b1bSHm8liWs9eMbd9mnusCtQOZ71YBWrMNgIdwe3+XGWopbLQjN4YAskG5FeE4nWp5+4/SrXqYvIqNyaD2Ms8vWHHDMNKQxjPV3bXcWMmHeSICYsprZ6tZ23Bm1RQK34ZRbjY4wvVzdUJBHGjq+DX7SdunA/wfat3jJCuiDnkDPvpfskicPZEwKaCJmbwYYwAXdKnxCKRUzIIWgCA19rgmdFUdLw3fGxnK8ZFYaJ1hlVePsHI5rT/U0vwOYQ5micwqWskxbV3vb2NvIIio9ZYWmKKg74qcJcPbVBQY34xCzVuAdVBMmB/0CswjB+6IjvXuI0jJBalZWqj1jh9CHA11TPyPQaP6ObYtDN5ii/RiukJESL/7gJ1lfKJuaDpjgp5nvp1ZRpc5s0wPkNYD0FwAxGeP1QbpbUAR2J+FyF6x23FYv4135czBJ3Vng/Micz1c3X3sOG+GnniH2oppse1HzlR5/skJ5TVNNT6GsP9fb3qTB/cZer0DLc+end9hefMkl60Ea1ONEJSuPYMuFfMAezZlk+Kvt9t0k7xSxUVSGW31vX0rQruf1zHvw0RZzmCpF/cXq+3fo+LU2S2+Xp2ZdkKTbg+EWle/KlAzkLOUsk0/pPdNw6ZtyR3yXmxJkAJ2QSMcT9nvlOeh2Nu9/QgjmD8XfoaxUUkdUVYI95BaVxJaXm4dhfEK5X0XyvbzIeL9QAn2fBwMzNHcD5iObEHHHzKJ0VUAMTMjRIlvrqWAj6pkrC3ez3dBx2/T7oC4RANpDrPt+YRGz+ecaVjLcQ1ZrAWRZSDvhwFrTFTOurL9QR+j9N8DnDyNmUQYgzZUIwiszDJ79fuuUxidgEALrta0cCHUVDDbewz+RCF9dO1Rvt1Vn9pytoS/Bere8+tCVUhFWpQtOZw4GCxa+wSMhY77WyPWDX8t6yzmveWRCU5TY2dlVVuji8TCqWHUXCyMe13m6Oheu+Qj55+L67kB7n1fcRB5LB8BRURaOOv8Cy7/oUhUYL4xVI1kmb5S5hCrxVT07tbGqqjVZtbHPdkZDa5VGa2vDfeMCA6eRUgZYAdO6pFhc9yMB+POAYvOqm5MLKG96+6wUzYEGc/cGqR/vS9xL1xz45KjvAyB6NH/ftkq2JryRB1DRro4E4qhM4mhVmX6zRGNf2XI13jCU1kqupOW1Ixykgoi31pn9EGeOOaMMPEhR9gJsHcW0+pd7lomKvb9G+ycX2DGtcq8gZ2QaAxtGwCBg8SH7ypnv5edrRB7jB+l863wkktOOpP8vjY07YN3sTpm2YEuTwcvCjRfLOflJfKDMHVapPy0MSnSWZx64w2S42Dd4H2yol7DC9fkecH4odH4Hql+PwWng9p9WG8nEwXaYYbXPtT05S6eSW6DZFhWrj2zNeKx61a8S0lBW7QUGuDUFPbHPKGy5sB0GoVxBIzKndeS4VNhvVk4irZIaCfcEIkkQa2deo/jbVdNXvnPQht8wogFk95SdeNgEFAyEF1DVu89yugXqefUMhOCBiQMSyVcn1wTBNzeh29sft1iAHjn3fl1CBK6jUc/oP6aX3bMfo4I12WNwl0tshKH6edMFk2evZeZIvStVkPjtivOLYVhXP69l8AiALQ02Osb6TW33zU+oEmqGElRCLBRpqS2vQ9Qc3r7dhYj44jFeaBUZg3s3sH54P9gSF3u91XRKcF3TNyU50LCJOwXp0vF0qcE3xYs7sah9tMDZ/m9ulDLc/MGEcoWdbrSFdbESOSZ4+dhaL4InQDerD8srY4SBGrCgh+q4UhUB3NjCi4LEubyVUl3e5DZGJzpKGcDric7V4Gx1t3VGetKCdtc8CKbuwMQNjlSvIDFBwLeHKQf3CotJP0CF1hWMUM28ug87UQGkXNtIhRm6VGnKDR7BAM353uqulFUmYayVQJRsN6pH2d1y1nKJYcHpi4ZU3SnaqDbQK3J+zf4hGx0pXEVRx5l3Y3zb27p90HT5l84itcAIbszhwuJXrEAi8trAcdqeEbxB0uUTK0Ra1njtJmfW3sOlG9VL9op3jVcNT0QIsmXK/lHwoh3cE3FfTm3Z5mgXBV3Sbr3p2uLFgimCmvq3UjYz7dkx7ycHOjDlTba+WcYi9VtofBDycA7IeWe7sp4c8+3ea/fDx29rZI+G0xmMeh7vOzCCOzMGyExSoQ+gNZ0a4AsZQn2Lp2JHR6QW/a96W4L6xJJemRWeTbp42kWD+Uqb1fzAkuv2dXy1ElGWiX6K6ag2K0EbDG2Y+YJ+cqVlyniCtbnUwyD7Zn4clc5d8sSUOp7bfcfswvp3Q9POf3rbiodVKt20Zbwr7SFBBmK8t8S/J/OWX1oc8MnUXoqOtYH6FaHILrC1g8OWFqI5E4UTC/AmG6ZoXA6K27ahiZIGnr+hlsMjvlnj+oAE1Kn6lsjq//XwO9rN8iyPeQhg20X1bG4PQkkavYE9Wtvk7Cbe0A3+nIl+QWaAUvlSRq1O9qJSBED2j5RKFNpnMQ7/MY3s3uWOA3zWDgdroYL/c/wL+iiW97JftyqMYSs9KOhSmcOYZRwFnRJUtZ7eQvpDgTzrcjN8Dt9YXVg87Jgqyfj80U307H2ya6wrBNGm+r83HOlcaquNaNG75E7i3IMYgDM6QpdnLYMHMrazWtafDqc7sSIkqv/lzZZrKIRf+eVGRn7MEUQEL44t7z7p4zAm2Qsa4xzA3vjh8wN0IR95LEnUOk2RL6eYCQivsqiD249qpkuP3C9KNWqISTrK0I4pzxlHQVl17JklGDolB9FNU5PrQyZP3WwMUCNffDiYGaTYti4KDKCsdOYn2saHpaF2CNvFK3U/gN90DIRuu+Vej4fq1w5wLc4GSJC2IdXd9qTBrRewN7lBpj8ujXgVhyI0pJBmPhlyRgqQwKmpd/Z1vtbEVu2jQCn7GIld9WCuvQ5arhzQRBGRNkMnhGYoSKOZD2cBN2TpxNokbAwycVx8/hrwXOLhTzVKvMvjdf+wDvHaDYT+g8rPg+/dgZbFYfksev5O4dH5TkxQq24q8tqmLgrlqezndo7Lu8TECT6UFxjjRNmSTQ+SBEX2XebHY+0TRJQyKbeWw9mHzIJIozEC8Tf58TnpQF/ClpgWwHMGgsoXvia2Tpn7HGiWqi2J3Ml2/VDTOSxYMKLogILHrS29OwmRzAUMDLH9+5fHeGzIKRwwP9pfgmWgC6CNgGu1NyYQjFqAJzahMwGiyE+vNadSK0jUgaO34vcB1gdaY9UKgAEwBDHCgRrFcV5Gl0EvmZQ3n2PqajEF8rBN9oZMA86Er2wA701BqLlpQlUUaLFE2colF4BWmX6r1FS2JnYL93lHw/voTw2uTqs3ripufuRIqrIiauepOoL0n+p2pXul3CeKVHfBlGmQQbOccmmMA38O0QiVXjqgYlo8W01aTnYkwaY8KVXgIvJ76RjLN3UtgAYiu2wQAwDpMiW6nIq6F9R6Ei7m2hkhAtGj1aloHLlfr75xPF2yo0RVVFiSOaUONQy4WM3iheBxJgCuqCwx7I7KCHP39OAmX9ls6C0LMLEFv9BAi+yZ8MQszZ7mWrhbYcm64K2JpxvdSRDJSZbkJH0mLXL0p6TEFELHGqYhGpIxypFXORcbXzOagKTDPobtAR+g3Xuv2N3Znsdmy37IsylIdtCQMXnqSzrCjE3xzmgCmLqyRxOMuILKO6b+ZL5NqITBjm3clCsuptsuJGFjTziP4WtH6NZmNIh2EJQ9iITYj3SU7iivtRvo7B8U8j1RBgr6ezoS+sJZgt1DaQ40Vc4dHCueomlnVe+AzPKrlKy68IiYCCEpLZJRAYqdTFKxa9pWy2YUJecPQu7hwrcgAgxG39QYQFlificEQJjkITmbA3qq+CHIN3LGB8P+7lZLb2Q1ZPxn3JDsPsmGtXWIVeAhMNqgLgexUvg6dEPI1fT3xORlbJ0hANSjwqcwKcgzdRwdiaFW03ZjFi020tdTYHuiKz6v/Nc0P6dW8tQv+v9q/xtBhJ35ebbBl7ck6908FEhIrLczu7bX3H/EBNa1F58iDhp4Y1BCziAZGjSaVko8+DBXdZCQ6sbuwgXbMDhERSHlR+h8Y+V338Lrnd8PDG57fTQ9vjqAWcGFivCnGeBTCgUd1ZNKPt4ya2kVhyaUnI8UK6gPSLX53zzzpY+qimir4OwCvqlr5qpdaX9RVoNRTiGs3fv9oYVPHYSBtS+748ER4+qxf/zCdav27Ug8vhYdvURFaq4J0kGpHEaqKiMZbPn7WY/S/JO6Udsu16q4ftuPnPp9/ePjrh/H46vP5+eQvVhBCHkzhcuyK9EsY949Lt/KoakCbncIyJStLbbjeqSU0VLNbcsjIRs9IP3hONOkf82Lc7eB6OHlilZkLfRJ8K60gOZ0gz2FIzhk+gJzqjHTNj57LGhCt0SYejzHjUDSvzwDdLtylDkt2w6xkZNrSDjgsvJ+PSj2CXBW95Hv2pqEQsw1Gb8CQZslH0iUJBlxF+jJEBNcIcQMsofgH0KpB8ELuLrbiU4vEFZ14Y4VwyQWw45YHphra6BfY4vLqUh6EQPBmZUYKg4ANSjZesbxJ0QQ6rxW/Ga9RR5H/bBgDwdzqLK8g1z3hHtgKdiJjNj78NVEyPWZ1pZdcvlx4yfzlypeUL7d4yeIGZR8nVihdH6sRFISoVm2EBaHLfHAJINUButocfsPMDSKiV7K8OcsNfHsl+Q9T/uGVyB8m+eGVFDe5GB1Hcx3zs5WGTWyfPo5rL1SnJfoN5Ak5TfBeMjF+SinXU/ufexY5G3b7RZMxsjM9LIz5oj9x5x5BPT82uqoTJ8qIetakjZznCIDd4OMjHZLFlFwLsOL7rxPGGWkDOKCEAS3+G8yQjuefkplWwDfJNBeHXaQQ4P+rfgyHIM/+usM02WvTLWuixAhzoCAEcVD3mgjzHLafn9Pt5/nVZ0E3WhWYoBpN/GVdcZCiDSg4ikHBN2fVqJe0EeMFhAo6Cwh3d4IdQsxbWZW0Sm2A4pMR10BHccwMcUHtGOditMaFdgDjL43OZlXcDqUjFHwFoF94CWSzzOiDtsO58MISqCBprf1N0FAOKXafhlPhVRHMthSx+tL/uZ2xb55bnLvfM/157Rwh+6mTqxhLYQpZp7Bu8Aq/7mG21W5xEYwch/1J9v7GwRfZVA/IAGSHU2mcKCKdzvcmVSw4O+fMdHRz68kZFlJqCqHqLX8LFWLezUepNZ33rmrMu7U7JdfEshBZFlEfOvC1emSRVxOIG3t2HQKuIRGnJL4ixKo/lCYpJxQ9lKlS13OpmgjWsnYgrCZheD+qGB8rzeIxd04gNuCJxF6Y6YIT/jOg+koVMntgnSLdTRTCEX5vTUb02RSjezxgFeE6GUjb63UCB0xAx4uE+GPT9x9QpA9Cq/ii/v+tfplACEgUJLgBgIe+DA9rOO00j+OhShshzoBxIAUD2Y/QjecwNc263fWXa19aCNLIGyXXzwACLf+8MNpwqIoFCGzzlAdWDLOXdyqvR0ThUE+1yN0Yw1g9vQLkqMGRuoe35lamrjey7w6GicxL1XkzKK1QHIGZWbiej5TkpQU0Dx02trjSNIIwzPRFbPtndEHz21nz+8iphk96OvsYKuIkjp0KnI0UHIxp/gS3dK0dxkSBA4gQf1NT/SSyB4i06MybPzL82N8Viw6GZ+q8eTwqkjMBHY/r8OZu0TCGaY4de7R4MrDiyZv8VACAuicDZaw5HxVTO/wjENQfytcgHB8ShKMaNdDs79/2v1RjClQtkdVrF3FLXsJlkhf39fhr5vp4fNLpejSSJtOqvjK7MngCrtoPdtOhN8e8rpe0xDIbpi7b/gSbYzuvEa74AP1tY5BuNW5mbrznbueu3lWp01D3PxRZ3JlKBymGzkZAaD/XRkcI4FfHs/OQgVX2zAV/4N92z6RlwSUcTIRYPGQmqCKo6HQwwOUtysnZItrn5Sfiuvsc2Gf7ori0tTI4x13KZN4o4CYGp85LUOX1574c9kyp4iJXlQ+Tdsx8WTUTlUxgaou3BUWwVpaehPMU4KV2Jx2nJqe6BGu0FL53FVIosJnoYkwazSA0Vx2O9offyKck+Q2V/bAux/3fBoflvbPxZ1ddAriLNPWex4Ya+tEBE0AMWq7b5NhJarHVE9doEQ5L3HN6CDPIPAo67QQ33z9MdEtfwz4TkgyAr0/DeT341guyMqg94qYQqLomy56XZBK/Kbh8fJ19yTgluczXKazWa8KM9jqd3omECvsD+3VNcvlztwECyeZUQsKqgdu9z/0m5qBIYECvFMRVROu7LamKltAC6bdTl1IQBpnvPKtdzMIcPTSjmVfaIv5+1uX4Tw3bzMvJHmAlorM5QjWjydoPo3EQKcuL5+7E9l8E7w/WUO/gs1EHVHzyWV1pUE1gT7Ga3AKEw9ei2L2ynR9UGtzZm0uCp0uYXp4SVSVpwM+G+0zQBnU47m50yGyj+Y6jkZV0Rt0kSNElf9YPewc5uXmjXI7tnJYU8nvp7Tsz1dEznrJLPR7FDM7Gt6HfFSF5m/kPS9vEChPb8b7uMH+srJ0bVKvvx3Ylu/rJuCduRzU19yT7bqnbFfLyZqvj5+0WBYkVM/P2cO2936LsniF7w1xyeHis4AhBPeTFSnFl+ouH+YNdNf//9Grt/8MqQpgFH6b8Z4Y5wR+6XGEPy5sF2N6jFnX52wdXQ+1KsaYZEHTjwtHgKUAqnyqBtyyqB/u7JRuQKqRG2+TbbA84wQHbtfo3uHzJ2kCkdSsbaJziJf/K64h3+Dw95gJK24FpNuKl42gy8lLP81mNEmYrp2ZEXNiT4vMj0ikeNcuVslnCaHQZ+9jYXKjcls/O5YlvlP5Bq2qeLfnAqxxfqKFsxed0xHsxtTXyaESEJJ4RSpESliplMQotNLOWkC1sbUc236zptYe+83DtbDil59iv4/9EAPIGCFJg0g2XSM6v569ivzjwqXVgfZapF8ok9TWtckgys6LeYC9whqc7qBpKQHaC4x8aEezYdT9DqFqlJgsZxtlpvLNs+Sum8YUGoR9G95Wznl6PNFvRHQ+zT6PIV+yGT3m8co7Rp1VlEkUBZcQ1cmfv165SULS4KYlFOl3Kojero+2k2sYaKbWswacKA11KSg1MZcSZ4TE9Dk5IWkcLtxq33T8B3xNFod5oSG47smWUAEhMxATMAjzi9XK3XEM+3cCrNHAS/KPerxH8jibCFOE8Zrt+Xb4GnplQpRsgU9EnW0v5o5Ley2RXrXpSJDeRhVyCFT9VZVOZVymkmIHlkvuUABuJ5w5dyRWhG2wghCnKz8BXbWW2Qeo9Zzz2Dha0JrZ2pJJ8UxU0cm3cTZ89PrnsuYoempMggG3KmrMDKODc3Wc2fZhDGq4s5hHjOkceEl8vhqVxfA4VoCCCT673rHg/n6w2kUUONxQv6/YknZMMnhjJfA4ZMuUwqTV27rEIMAcf3v/7lnwmFVCp2vJWfS5BerRYnTn0jmEDgo2nmM7FE0ozM+TnyMrxt12pDwBHo3FRq2jpuJ/2FVFc9CYxNDIiqJBoMmpNlxzMcTIoHFAA9u9Y0MQIvOSJPnoporSj+kExmSSVxCOoXU5qkHPRFl1TKg9yrMj2tAasCaInVzSHeGZU4xlxYwz/C3FB15wML7qIdNVQgrn+7JxtvlvcCEEOydEedVdfZ8htwzzmtxeM9iRjDwzuVL2e1pBuuimJpA1rwjiLF6730nnaHtmFHR9rX268Repf1fwUfdd9VbmYA2ykdVXEn7bQjbr8QAHzAkaP29lVbaoqwrQvF1BH8ImOqw9zmq4vPKOFzVqj+V4C+r4EVD673JyVs2o+agfvSPOuww613hVp9K4/h8VQRKHpx3ZsMKj5mkVuu/+LtyG33nzTNHuXg/as9eP2egAZTYiRJAKYikPsiAqh1GPb1AezRXjj6aprYF+O1MLxUfCbUaQCOFLC4ohpcRZaFbEXjADcWiZZHKYzVMkDLgbzLmed8tDk5FlLbJDXT3V0SSzBTHxJ351QMSvX2FoAGcBSobdeG5syL1HwK3bgE4Df4PlIa2KcQX9L5Xn2H8WXvMeI3ABBScjFlC+DulIMFzQe9+y0m+gl10eBUItR+ekRJs7JSTamSwoH4AV7lRAAwnbdou6kuGxAmLICAkGk4YoJCJdCBvDkNlcbb8eUhCKFXK1woJ8kKFBuMW87OlCJFVsm9nmKvOsG5eMwazgWfRFkr0zWiqMjdHtOi55fleheraB5JaoX8AclqVy3Yu12rKOm3eKmq1cmDpf7hdCNuhgvr0fN32RtVGEdKixP1W99qX8trzQtpO6wiDBk80e+guD1HCXBwSGw4CAnoARUmktjywFLU+GxTdoy0b6EYTvkt7Vz5N619v3B4ssv01i70IXDrZqmWUOFSO/ZxSZv12K3Ld7TjuZSfKFhtGOz3x15vxtr2hzNZLfIruilErvdP0tbJw33btkxWH7EB5LWMAbg1BujCT39A6YUyeOfurxUWQ0WezPuB9w1FzpXPkgUN5cfpedzCldXGHqY+31hAokqHE7q7NBdDx1ZE+xZ7ixzk42J+mjsgsA1Db826A2XddSiVhHdXZdFG5lmGeTo3nv3OrFM31/fzcCNRPeUeVODPC8PZ0qAD0oUuZWwHXQwXLGnZDiKdFA9PVq1buJLFXAVYm0w7DKDA526/jbQZIP0b2qMmHSjIEawpZ0ZsyN9Au3BQZ3pJ6I3T6zDSmpCLNs7x+wG0ZfevvdyiFN+yOoO/zP+Cl8zLjssMP9lGM3ZMbbW1NlMu4Nlm61NnW2magQmeHNt2SrZEiivJVOl5RpImGgik2SCM06xqGzhBx+SU+xrGAQIWrgfOZmuu0ozZyVqnX8sDQhWwFMyeAc2SqiqYCbUtT9ZjTTWKfCVgvOyT+J1Ig7rA+dhkTjjtWSqqmf5cNWis8r3kLL290p0q0kLbyaPicQCo0ytFjMm/H+hOb1tyfTf9fcZPfc2arCIxDZLrHDjeqAb3meIQZ4Yv/FoXEIh6IjtREuDWdEi0ZzsbpdS6e2coiskGX7eXVCNKfz+Affzgpu9ihGpgzmYMQk2QoiQCbVwYqr/Ezg0VoehynmfyfX0KC3nvDh/kVzjoWeu/V5QcKS5vxJ/sZT+8yT5/sL8FeUvXL6i5QuvX8n1WIt+9GMFxUqPrxyO4wGnvxtd3/XLuhHv0HJuQDWOPS5x4/zFst2PqVvqC4pa6byrd0pLfq80f4XyRP/lnBA2iMn81kR8GNcL3IEJ/3YmUzuT6VmGnafgCcWJRBFFDUD+UpV9VoXb5hj+FofsAKJpuh1kmsJDzv6I+jvSQ0j4KBVzXnzz4tuT70LQbwaa4PQFxStpthjyvvDuFd298PYV3b7w5hXdvLD4LVoRW1D7zpd4OfjSxcoGXu4LUPg7EZe/9MG5Rq4bFy/35sa3N75rB8wGNa71OM87pYLfK715pbfP5V0cDLJzakIUtc3fHhD4L3Gydib825lM7UymdvaMEY/9AQbcv7Zj4wHkl7i9wptXePs83qUMBgb37akHC+RnEFDg9IN2foZdpYJqMuLbasU9QI9rBU9J7iF3f0HzSlJ7UiIr2fNsXDmsAD8WzMQHVCut7OcJMDE6sr6y5QpPJ6w+Nkl8bbcyoiY+ecYvvW3WyF1ptveYahqQAj8LqelvJyxgEQCczhlgTsDky6Lzk6wGIlVXKfNWrf7q4BdD/LOmwJRzYiSum5SQtx+u0ME3A5koXtC+knzd0Y+e9XFDDTdvoQPZRK2xAofZAV5TJZwGx5IV4p0fwZnuflRIB7U9lEya7VEfZEDLIPFiRPh/gj91I29YCi8P8XujeYxk6Ah5HzolriNtr7A+k45IzIHQU0pFKFyR2bYYb5Cvn536baEbMdu21xadlwenmkg3vPtRId3gfFP+AX3Sq2fyWwSCF5kmHT2bT5zfxQ8tqP2C8kTCKRKdf0Uh32nZFDFsDmvzb9+Ez95hQA7+cEng/Cvgr4f+0MjuifObaNmqJNHpCiBN31QTnH7QBTIdqdaQeg2gfNoPil6hkBiuvxLqpsuXIV9a/mXg4KOVI7/VKZ3hEDUPx0/tRPY8f9GaTW1St4FhRp22L8w0VrIppI5F1uPz6cyzMBBimJ5byNNWmUt5QYc11RbDL/73byQBDQP+XSg1Z0A+UV9OUJvqPXtVx6joOJs+ripAfR1Iw6ERv6KejJjUlh+VqqlPqxfOvUipByeNOurrmvq0fuHCi2q9HgQcvyMhb7mpULS54ZWUKYLx2khrz4HEYybtUwy9HwUZn2KGz/KmXMBHpvtlKgSqRQ3XDH4TuIU4smgQKkWcqYUCsGhR4U6GI39WGcNM0qudD7u8ibAdRI2lRb7YdisHFyTvObwydZrWdWNTzGj1axm5cl/lhJzNtDJmHBBFaifT4GQYZJvogwfi6970Suhzk9VVVP+F0SYy9D7kVLWHavrgmaEdvGQxp2VRdJ/Eu/HcWX05jcaBzVgP8KAGAH5rp6o9rxB75evjBJHVbhlaczmCBhFNNHOxK8Yon8yc6aaa4wUMgoL278Yqg+eH6WrXKpxRXeLeKZ4uLhxil/ggmKaFWDsaxR+1B56D7kAiYwPSmyvnIFJ5c1ovqVeahgj1lyMtRjcNI5IH/OU3Nio9aUxin33WbAmLCjKbx5dE7mINCynqCrinInw/DHtVpZq01wMFb27skG5McGt+2jTi/tav6MDMD2t43jJWP9+xqyZV90D7VzF9cxBjVoezTbfXB738FrfsDp6Yg4gkd/J5m6jshzx1UNM5R8nnucno8JuDJCI5Emq7vY4J+LclZrvczc2JhD/yMvLG7vSVO9sXdri5sCkuuoHcIN4gLWRLQmh8S6abqzzz5TK++elZnWRUs9XgcE4JoI01+Kb/pcv5JbTLxw3JFEZeWHgJjLVMbppsetlxDeCb0gBX3dOFoLI8PSeGxXdbmsNqyaZz/Q5V9JRsMM/Er8+5dAPOLfXJzuOpbhYEgFnw1+x0sMatX6itAMApQ2I4ZrtH5pQh7u7fkdyLPLOPF3sjj3pTzl9bWQlVLigfBh6Q/Qm9i9FP1OmPGZb/Sp+qVifMTnmSxUBb3OBMcZnuPINcJOvdg5+/ROwEN04kUyzUHV36sNkiKem8NqweFr6v5/HReckPg/iu4ADyLXKL26GP1ZRomCHG/f1Y3MofUvLougABVDora3M6z1x8qEKMz4Hs/uaBPNHnE3pvV5/ID9cTWS9WBs7pjQdIwawOA02kEt4Obg1AcWYZIrI+9mZUGz0I7H4u3jhDye/dnWRQFBnb56+HZm41dzLwd3WyAKV2Xr2FKezjhYZAy5j7twrGj6+ufBXl5sl2YS6NhNYhvmgQ71rmsn9A0Ftxh7Q3DSaXN13lpgZRJS/0V118d52p/sWNhnhqSYX4wV5vZJH3JQpsU12orZvqwg66qUG4whCqCtNhVC9wD656hcTgaqhAul5V/yJ9Qzz1SZ7LYp9ViCmwTPe3lpG2NrFOWFzT1VGlR7vV9a4fhqPLpYrmIc7Freo4mdqh5PNTbMZN7ODzLpJyDtlVafT1WHUx8VAqJP6AgLuExnMqqWDYvec9SkvbCww18ZJvDy1B9wcE3FvwPadxJherVzX96Cs6NfGS54faAvsPCLhzgL+K6WYV2cbLp/FfwtiXMeplyMuI61NcOXTKxwZ7n1JQRgWrZahEmms0cfMAH3R5HdBs0PnmXtHyBvor3vcI9PO9BPz5XgL/YV8Q4PmURBPo2rOVINALW87/DWPO/w1rHvYb5jyfcZYMqmdZGaQnnj2cM649nDO+Pewzzr166SYVXYUgP/sPx7y0vHTMBCDarXgnKRktI4g/JgyaqEa97uY/dlc1J6eCiXniblXmb0/+U3pqUq1OkMoMpyjtXHbGU9/kzzIlShR4wnKDnaILuBR/jahgdg6sOdTltN8QbE/riEezIxmPt1DYr3JAreZysfnyQBE+JXvdo6WbU8ssgFTnZNDhQ0F9e9kMPtInn12GlgPPjcL8dALGpmx3buISiRTqVzB1kl3B1P5fID51I+mPjVwjvUzGddW7OHAoIewapL3fBsi7KDQ+Wc4yxhvMhuAMQHSgE/GvZAMvYWIfscx4XE35+qdg+eeHkiVbXGwXGRPUgSTyW5C8++2DImqaiKX/qRjQfJ3bPuPlRLgiBFRjBJ8o7hnUIWA9SeyGxY/+ovvpXz5Yfak2OqBDmnN5NcDfHn3XaUCX7dot8vHwzOfvWwsBskZLLYDGUiRk1xwmQtdzg2fNNoSQS8D7uZEX1q8wbqwcVad04veY40DvaJZ8zzbJV76naaNfYfl00YXkV/WU8ikCBFtyC4/5rG7hMY0oGniP77qVetl5ZUcjvSgpgefGx0ozCDQTOL8otLWtnLG5/ByyvhDFPsLiBCuaIrnJJuaiNLdiPWcGSReJUOspibBR18HvEn5KB/ahtOCYxt2+onQXh6LATniuOPWyC9nfidHF08g1sYDr3Jk3Tdfho3aeiXnJwIvb4dXA5dZ+ftF3nUWt+hhJG1kyhxy0sQs7flXl3DJv/hGaefUL21eYN8zar2o52k7SnjHqYhzwCuRrJF+0KkcJ2+by5teCss/PMXP8H07ZwRE/NIkGTiPHtxIrPGeSdrr51VZsIdr4kZCjlvYlPjhGX4FIas2PgyGjS4+5iussztlmOSLa2T1Bii0UWni3SKmE1/RrW/Xty1S+P7y3LA3tJrdHufyRlPAeAiK0tFZA+ng2YXHPuPWx+WWmGyfHtMOUJVh5TCCqX0yFs9VzKCtWYcDRlvvQk9u6Bz4qKXnMGRx3PDLctzHqlxbITd1VbMgxzIxZVycOHIlxY2L5drmWS938wQ0fYVtvw/tPaZxnVV4iztz+udjndP4L+wnBP0kXvwK4Wcu4OIKamVedn+3ML7zr4bx2LPjiplRq/3XMAJmJDLeA5Iqyv9/Wkd0L5A80eOAN85J1xTllV7/29sVNyTMErx68PPtVU+c0LX13LQ7bL6oP+PdNlcm4aon7Z4zZ4j52v8z8UnmgiOKQH09/mu3ue39tc7jnYrqqniv53KOwXDbhMQERcM0Tf2b/fxDGWp8dUgqubfZqznWWxbvI2c33g903y9FwZ/dc52kUz5dk4LwC6tzzqIGmOuuF57f1v10mu2eJNJRnutihJf3jeolFOvu5v19uUp5hYrPB11x1BdWrIV+OfKGkukudz1Vl/q7OLy6Ob84+RpN6wvyWcCDo3/bmX7gNIWK7j3rajNJw+bfZoF8v4+9k2z9/h4GVHkf+KgeneDRO/dnURJsnzVuJj+wcqtQFPMWsGEI2yRohZzDIoyEFi8uajOGUm/2iF/SrOQDH3Ew4M7AuzxnSG5EXOHCcQRhYNfXnoKY/P2ryQWzb4ZkHe8HZ7kVWhisk07lojhxaw3asoIUqDgBIaIo5XHKSEMTlLEyOFdu2Mtbs+u6i+KXyc0Pr1lf0Xd9Wi3Ji7v5WxtiusJqrLt8MwBlIa0+7e9Z4WxAG/d8J3yDq/6mxptXKXXnoZsiMm1PBJZWIHEwL3jzbK+5zW5CdgP1aI/nsZN39Ns379uJgSYuHb0ZRKGYlgCSnOxUSRNMyvmquE9FeVGc+oWx8/Pz1atRTchFhOOvkPLBs2uejtcs8RtGhaSxVPAb9TCgVSp5jNEGFtKtfNApSX5ellD5FiUEM2zUN1gK8Kf+mcou2ZVI/Pxv0q0bmMNtYvsWg7aD3z6eafCA7A5Vtzb2yJd2w7PWydm661HjjWVH5Hx+z8DUMp5brmyt/m1F2OuYHnmqzstVvTwzFqedUdsoA2hKH8NVNkL8Fl13TlqT7I8e6edPR5Uiybt1+9KfnPN6ZDPflOY/Rt3TkClcfXPNYD3k8fwwtqJrH9KxW7Mv5KxK4rKLO4AvpeKn8K/Ktsi3jDn8B37kcug3vbas/YOqFmqlLWZrHYEYqTuQKMHyTQ1TF/rkpE106beh6eqM8/BVcRvFl0gb5j0Nz7ILZRPRgT9D1Gf+Oa2SdKLeppzJq3Ni84Uh5X6zYeT/IOhjPF+X0zyVLB6PZIi9hSl3szwydrfGd1iYWUHx/FBESgywDJP5Ng93IkfSZbpr85PCm4M0sZkaOuz7dtPIWQvn5dE6JvX1ieNpom66+H5JXYFLPTJB3qeaWXv0JOxzq6HsewV5zE4zYJJomn1j6YJN2bi/yX7pexF86Z8kYWO84S1FAhxvMx9YgDZcK8eekJt722eHocv2TjkhV7u1B44/jmoOqrrC0S/wYjzE11tY/8BI0o32GTTUSQhdVTrWrsqkLPrJgpCU9CiIibmMY48SYilFvohDY1QlFrVW9cAXZiSSmyo7cg2kgAw5OmrOWXVn/32MLLcGeh9OSiyLv1NjRMy3rjZsOMAkJ++Plp5pX0rL/z/QDWpnQIX8wXANRk5IeXc0LUxCmChIZgvyC1OBbwqaAqydsPMl8u7LRUjuv5XwRAE6jqF84hkPdh0z+hqzcV2vjqoNuCCs7g+DflLe+kcm6D7gCydFSKbD6kuJ14ofNuTO9YIQqE4NIz3Z/nKAC934VsXH7/KACdrlym5SgsmJwcl4yIF/1yGDDuKliF9+8so50MNPPgId++wApn8p1zpX7oCApew+FHGEiza63aI1d3XOOMdClkP7OQPUQ5EjUovMe3wRHYRxcGOZmSzBDCxoXESemc47snkSG1KJFOyfMxP7kiObwk6M93UcnjmX/D6OQHy3TrDbrjFlRICPsK4kpkKGeSk7nqUgfJefgp6DqgCS0t/8Ol4tAbSDHPkSXn14drfK4FcYhQk33qGqEI0p0K5lEGdtnvqd7K+MjAUUIdCBPeA2rS1xc4oi7/UVRnj9LrD8dX3mdfTriboERAbv9FjVWdUsUl53EsaMjphRVJag3PegoZvcflaAFDKAt6HB1EjtEhn36ZB9PZ82Vdg5Fj/FjJao53I1BsTp3sTZI6QN+EaRNbE77pOOzuE+cOzVlnF43fjjAiYnKHgmCu9xTI4w/o6p1CjBkh+cklRb2gV5wjcfjrQAedng6jOEVnKX58XHCWQeyteh+SOyKI9XFSw6JXhsXzVwt/a68dpO5umhwxya3C1VvfYwKeD2MVSdTJFp151jl++zErPebaa8GfoDBk4mgrYOxdyT3F46q0x1572f9YM4twe2jzhTg7UXj2ud9uXMImf5AueUhU6Eg7p5zAwDFA+eWOwmUOaA3kqnqbZGsBlrbKz1IkXR/f2hGRIuhk6rjqEJO4q3tSmN+IqkIIJqXh4G8j5j/gN4LPyx1SpWRe5XshaMu1N9vntWwm/P4vdMuj55VVE58rNA7Gd/ks0PqlRsnPYh1/iarEoZliRKfA2maG31/FQqNcLhlWxRI6xx98cmRuKd9wAqLP7hu3gaTkDLodVen8chP5L8iLJj3D79iftXJ32aK2LeC4iRkopaxsQjDjSUK+az7zFKxQT1YTpZDfQGQ8XwUeTj0GMgmlk5c43J/LwHk6o5lZpeDIVmp01wvFRtmnalWXI4fyHOK6m3LiFunJTOeBQz6R4B8WV1u5FmCEM7cTHvpFqInsr5i5vAsRHnw5kCDKIo3TdoWe7XkqyRDbAF9DMmCokwhcJFWBmXNam6n3wFLFMQknC95JQUKdEzNpWriRb4pSrfQvnXlbh3D9nWqKXNpW2fqMjfMfljky3MuX35YipfnonihxDk9BXM6CvqF2HpGvh1pdyvdndA44X/rFFeDnAmvBhIs+fJpEi3ftCdfLh/XXviAdJx2toIWYSj9UJhszEAtTJ8BTeZkkxw285Y3WrdXmAiPxU/yE+2fXWHK03aQSZotne4PolqMpmiGJQGZ8p0tvPLAX2a0tff7maN2/4x7ecTDHd05+80jK4HTkaUToTp4BRvxOCK56oTad6uAXSr/08uLOVRvg0r2zDIXinkrBQJT0K3JQ7Gzzu+SQdNBOspP7iTVaUvxyV8edjv9pQ1Tqgfs7UzScRPIl+wXkUXdPZQvktqwxZuDU5z203YX3iffHjlfLaRqsTHBRKoffTSPQ8E1fRrEuBvpnUf9FVwlLO7xPantjt05W9/S8D6RPgGfbH0if0J+Un9i/InixxREEPp8b/xr7oaxR/afsT3tqIHwgaa7J5ZIcf3OhsMzSbB42j79xJo/nxnQC28nz4HAMOJJ74R3owPRcD9HNJQJ6yL74IiN821Ts8CHoGf9AhOYRZPt3tKLvDSf+bJuSARwiQRy+cr6iLYOT8XgBTIBMRdYh+aX0p7iWyqzMDcBhiPNmXPJLSciM61r49MkjqtZKRfkWYsNlMXcpXvWyT52jDMQ+R6pQieZAjx/eaTExTrYuKIwtV80BbTioBFekl7z/AFQYZEgmWAN/wBtnKMiaX+C3T2NgaRX3FKxJslFslPSAJXsCTtLLAsD6BRniJU1SVRPbBUEwHtYLmCAwqQSZlYlY1FrSHfVKKdE8SJ448Dkd5QUCtJi+bI5xxyWYRfgNlEtiFd4KVLU2FMt5ZknXmbPmb/UlpUwIS8lDyANwfLJuUly6jQ22FhRz0X9J5RN3ca1xURaGRVK7vsR4XlNiA9hWqn2Pdo4LZAUqtUMeGcbm9xuC8dhciCY+cnCR0FXhITd3zS+TtYVNfBjLXlfu7KmJoxrnJ9ZV+yAM0OaWEj/v4EY1ufo5XmIE+nsZ4hS3ou+K6JbJKC1yoXsNuGHSo7dHBd0lSpB5ldPzksHPpqsCi7GGGrwHxlVNU2+5zYoRmMQMunizg5LisxXOLL7I9xPixbHoWB/IwjOfl+yda2NMIorNuH5rx3r0frD4lj93uXcEzWZn7BH19+5H1+MF/cwzPuF04QGhY7zVw31ismRT5gvW6umzaQy2JoD8SwzipkgdrKKb4XjMyJ8L3DM9lP0ZgJ7Z9wAMMaB6Vd8V2+IpfVin5bGOClHg+6T1qVmPU1JxKWgV5pwSto6vYQLw6nANz9QPxNknnMMsV4zureEMSEBsmrSmtGhDMMfgvdHvvLde94LP6BCf0ucxwB4qEVlXMTuPDLUMuSwRUeE2xH+zG81x2CBc+tPERbyV57VKc4hr7SvCTl1VdaP4vIEUB2YAGOGc8aQ+xW3KJU7sOeTP/J0JbnlD66DmnrpUh/8cSFtwj4poIhaBnyqPppqgjELP1M3pz5TN6zP5ObkM4EQLJO4yIlWmqHVgmEE8QurkD/2PdxPxF09bLMhslW7967uWwjmOsyNpVsMYCma+bzhEL4tOhf2FfGt8c7Wbpl51PddERN8WQszmRGH5jWoChAIxXzwhaoQpsd62dqlf7moydBfimlMb71BJVy/7do2ORmp+m/gu4yoFvu+Qn47B2bQVPtG8xt9HcWwup1mQMnezwATEn+/kpAvs0xSstH2LHrZ/Arkq5BfQTETb7BGgXJzFzcsv+/nWDrdoeFC54RZJx5mCqbPzB2zVRptvvdnuo3jyQm/6H1WaRkU+jl2HvNBNcS0jBb8DixnF3zHJuCgk78UNrkuHdwyQlPOaKHdK9R9fHtUo/uUAapYVYkUursgckpCDG48QmRYSw6Mrycgas5YZiLR/FVGq4+ES5JUzvWfYHRWRu3YgL5JRN9MNdndBj5L8+Qdd16EyuP+3JkF9CkEreRPMDkTMwKW7R4RsuKXmOwxkyP3InfggxT06M/IXRCx0pcKcli887Aq5M0T8YOUw/4EjJulWGzK/pDCAuvxkqaRj0CjqciUHLBWINqXIm23HxkspKMtwdrwOKIQDej1LPAbqMXvALpz9xFU5AN5JvFG4oHcaegodg7V0c88VdqPakTfy1SZP2TgwQ5iwc95v4vbxS5KJEtKYSFhcJ/oceMKko5EI96jmxRrMsSLd4GIWfmrnGo+Ei5LUp/wuk34JB0JApJHiRRruvWecZz5JI3qLegy8sEaU5JD8/FEiQ3dQhhZ76gEtxsZkmYhKf4cVTNRNX+mna3h8URJDP0+nolqPEud9Y8cIn+Jatu3u2379MhDjlnLLyfSqtmP8LAvIXKsLXYPgkvIiU8/aS1R62Uubvfnz0QBai4NKHm3S4ksa/061xLlJsNoX48LRIyUJ/ePc/ePL3r69BPZKFZRwxK8TfjMQzDv3kqjQgZ/XzaOKDYuP+uzfZ/xXvIZLmb43x5VfiGq/KzPxIyXHDt6ytQZyBfwi/gL28E12Ue3g2uyN34TZfie30Rlrz9gSt7zgImkB6dOn959NMeLj+ZISzOdXgkI13tDnV8c6vzeX1LGu39JBqIvJJcSFErdP6s+I9SA/eHyHnECcr9+diNGsqU6/KZ4LGGQiXmK+eMIgQz2H1Zi2McQXORUj6NCB0pVcm6mJzq2lHMJwVjmw/G3Rwgikl18sXGxwn/fncTPJZrcejRHeTnwv8KEUn1KZ+stgLXLCQzE5Hv0/ocw8CMPLUBARF23/psw/X6tUfxY7jSqvetQryDH94jTkVqhze1oWvOEJaGDS2RN6pWLIO7CHiQlJwbktDvd3v50S7Z/yogh+cgPuNjDo/WrHJ1FSEveLC8mcPv4nha37X2YMIWhQcqNQiiPjJhQ/+QD9Vhm4qtG3eRboYGfkp+Vnyo+VdCSLXCbFBNMMxS4RQoF/Iq1xDRezJsM7xGnJ2lp3IybY5xecGGXm5KPAHqa3j7aLpHgXq4eb6NSxYIMeZiVyRFXNRPPuSpZi8SXJLQLQRRJv23wIeZ3Bt9lxPDoz/U/WpU+5euuLsuhtv261hLTPkt/iTY2uaMNxD45IPOVkBc1Xsc4Licwi3d2miUqFFCi0SA+KoUCyFBnzAhgYHF6kzy2f8dyCQVj7hEnk9TiejCtrOKhCEdTAyEXiKr1igQQzsBvJ6HJj0flh4bMHGgQTSB4S/fdaIaCaL+P64TbRUG5TN1626TEbuyIFkdIj5uHYxIHB7EFvDR2MlROO46sCSqw67N9n6GgyvJyAjuxhMiVohPESWKgdCnCiLP8VvAvibvBvwqch1deJAAUvM89WHTnHu/SL7XbbKIIHb5Cn7x86iLxMOG7xMXUV+n6dCA/AsrSaXiPOAHJnaaXEVCE5FVKo/udRV+PIXiQ+0519x0I/X7TON2KbLeug+wv2rhDHELcsjGg3rtHnEKChQz9UoKKlD5CDSp0IH/ODOYU8hD3qmhcJAALLExKuEDcRhciZiBA4uJMNXlEtU4MgN9DXEpQLz2azdI0TdwBJnEjZDGaRFZMNU4gcoG45+EzStof4vGEUiNpDZ9U7d4/pzUjqcwI/F18lNZH27SduUecTOabijhPQE5jLPWiFtKAMiG/T5zuEhdTX/V6Uv3DN4wfBafaB+wvZ1yi+MLTWXX76UzfOncbKukK7CbZYZJVlpr0J2Twai7LL2XYDEoneqZ5ruerJ9jvG9VgiQB2flHzosbv0zxy4OE6zGwV1jAou+wpZxeecnbLeVoe489pXMuoXAt8lRd1YBQqKDtHeIgRt6PvLKc/++V9P/rNusaLDIAFND34bUYxwV9qhbYswKoU6Kyr14pjliwC37YbbC2iJhWPguB+UwhUoX/WAuIk2FdKnoDBqZl2RfnWlZRRE27bOSyCqJnHMHCV58O617Gh4GoK7gy+EyCwqLmcAZs4DjO8nJG6FNSncArS1AuXn5whASURrw25lIH+KkgjlbbBcO1ljJOWL3jS02WMlIUx3n10CQNjmSwew3cZLqHuo2ELQL1Btxf6tV8Y67p3fI/hFNIGXV9kAFBwdlMnhShJ3X7hNYZq2/aST8jgkXnBEJpQUEUlg0ws2ht1j+EEpLYTIVCE8skT3GM4lZRmnboiJU3qCEWRvDJiGtO97BkFMCDDzccSC0zXzJuuLSJQIg7AkiIkB6raWp1DEOt6CQNTWe7ds0/EYMVUun7HpjvXR10qjfy28sEHwYvwhSiLodJxtTmfT0rMIBNdtpPwNZJ8pVeFvhZcNI7kIgOAgu+4TDTFdf7SuMbKShaZ+ZOYWc8dhoMJ7h/zWfMp/VnzKf1Z8+V/kPGztLQYwt5iFDJI41vs2XgXHr5bPnzdKR++eI5Sn0/YFROpsUSIRcH38z6XhINP/OI9pEIPO7E1IqiWgg/uwGPe+QZ3ykATdF8B4QxAQwDDThx+TTEh0An3yMvT5D61gyn7EBCcIPZhwTCL6fX/YGI4hPhKy2ic6lY7QQ6TLN2AJniST5BsqKse7z2Gkw5vHkWY/LZoOkygT+afyT9ZfKagCC0IweRSiloDLk5TTE9lJtEauRf3KoWB2A9zSvCKXH2JpcQCUzzwo5SWUjcwpCqIJRjN+pphuCDWlsEKWTD0bYZFHpiDqpA89PG8CzbBo/x40zkIUSC9qB+DbVIGmrgBNxkRDaie7gEgCA2GjCgZgBInDYyM9g2O8jWWgvpJUF99iCJpiHI5B4PA5Wbc8ZUy2ORw26mWEdjgrGzPxTAWn/3VhYjTmFoMRMcldogJfJtk7w6JOgw7nYDxm8emiI/Y4Cj2I8zjFKYWI5CNV2PgVYLOPVr6D6JVIMEVvzHyP1ltoVA3oIiwiUsISkRZKQGHzN5XW20hMBDLktzhT4LmJ1TnilRulNV0MJ0ba2SpKjkpG0/Ze7/whg+/z7S83jNyvX1ms2BUSMMljouMMwvyLenLJ2BwYubebuBt9pq2bJRogX78fDjnNq6KOzH9k04y1uHq6fS4JaBJnMecDAz6w7ATu3a5CDnRbu2v0HJmwJ21RmVuR8cqllYqxdG9wG2TC8c4qnGQG+EucVgfRHOpDZiskGIYfrO/X8LAWPaTPjDWHWlff+wNY92/mHTOd/cYTiNvS/lHRpjRie+Z+EIQdvIF52ryOMbZDGTeahcZgAp4/gT3GE4lsxmRrGEzKCmYcVdaTtWMXkb+8vKXUbywuHtUuuPxAYLp1BbYpGsmltmZX2QAKpDJ7sdsEnxHS7+lRxgIllyi/vN1T/3nSwZOmlNJniHEPUZqqcyt4iIDUAF8KrOnED0TyTOAqnxSxWVi6TGbcNroO+5UQcilFM1PkxI6hOSe8dnU2NwL+++/Z9jL/JUvMgATyGS1FxkpSF+Ufza3GSM+wkBMIrMwuMgATMCzrrzAcEdaHoukltZmt3tCN0NlK4nM9+QiA1ABzX7jLsNllJ/v5CybEaUKwQzTFtg81FLxS+SuCiez0FrB5qyVG1lNBjMlk5fO+TXOnjlwm1HsYReZmb2UcRIpXI1SHsPAWa5bHCi8b9HG9RuN6/o7C6bLv93OeVNc2C1pg2gdLxeiSDpB1REFtDh48Zmc4Gx+WXXReZ1ngEm4Ox4UoW4iRWoAevyexSJoN6odwLxMRrLzgBdQx545PQnjiaboAm874F4uI+d5AApgySLhDajbyJxCUpXRSrIPfRXVxVRYhsAfZUAkYr8p8+2ICPRCGULpAS9w7WUA8zJtNwyIl0jboQe8ALT6JbiXx/Y5DLNYplc/sinXmXrgGa/Q0kca80SfYNnUBSKKFxmACV7LVpFvZKsI+rRD+UJOvew34V3WRQbAgrk+qElWmFe1CyUZYUn4u9kfZUAm0vCjn0+K4NMMZlwBQ+CO+UgHYhLupqAoufFlE6AJLTzUfLQDMtENnm70xCES6V2Gy6i4wT/HmghH8YWns0p6sZp/rt1jOIX8IiJj1E2/coNdZAAsoFBw9DajmOHCJ3bzJJ8g2dBPOj9lE9KzxE2ZcpEBOAE9SHY4QnoOOfeEbNrRuNoVRKNU6rkRiiJdkRHdVdF45bpwQLAkPKUpFY2kfhyhRJSVFnBqxmsp9D2GU0i633p3b50myGGSfTL/TP7J4lPFjA+Pmi9lXEXKfuzpttXTiIqLcNmjDMhEYWGFu5SBRunoRTN9IV/w65O/ZBYmdxgOIe5byVbZBIVutSqT0cbBGg9xCSNlmfkx2wWGW2jrqp7uOqgN0c+KDF1VuDMtSOIIhA7FiFAU0QvTA0HgrZ0lt2SLIxD+9hwURZoqIAle45cexyDE4EtH0TEKHUh/f6Ywx9koxd7PyS4yAC945KGuAq+jhwYQg34HJoLqPCxiEPQxXjqBspQ6sG4zij1MVuISyvqGTnc9QGm6EiL0hfg1ITB24U7D3SEQl06gWPrn+OA/n3BCHqalv8d7PHzw2cRnbiNt2CQOFjGv4CN3Ga5AnWljdTONvI3+Ugbqpa/PkTRPz5FI7gXv3J6++3TOperz7B1uM4oFRmOHvZSTZSeBBLbXvm6KI9nOurFr99g4G7kmGTtnHmvwGaEG6ilfFxmACmb8xhos6RAucupDf3R95uewj/yC6xNv/wTbewynIw/rnSka6lZa+9pPXd96nsbOcpomT49vorY1cqizTx4+dbDR3idufHf3F85cYLgRbT2bMcxiMExg3g0LSKOJNZVo8mAwoWjknwdEoCEt44UJt9BfMjowCgX0QY7hoxNnFtG4MjaSzB5fue71ytXGqkqsHrBK8LKYfYfhCIQs9N+XMLAgsxY6kGbppXn5upOXL+4AGLlW//vY71ckLgVoHHdCyaUUdRTShJgeAII/62JkKiRLJmp9I31Eh+TFAy8Tle7KGLi6VhsMF8S0sPc9AYOLZvub2liAgGj0vBhBeq1nCib/nO6queSuVruwOEq6NDCFJN5CSFGiu5jW0HhaitIUsR4AAqq9uBSk4U7A277wUHiZWlIhWW7jLmNcUuHHz+1kxd9xbidrcPnTmj/2ac1pos0EBwduEnokSmJp1J4mddYjng40zUGO85jDHOQ45wmt7XgItZcx1ldGoYYyxTqJITKQE1yQjCa3jlAQWa0PYFHG0UxLl/AhS8BdCkQw4AjOkkkZCiJQKlyO+Khgzi8OD0QBTxYeEJa4AdJTHBC9C9MAHXDyn0loqZ/DlvlsMU2JQiXEIK6bXJcxQBbixYzsMQwcyHV0DwSmwmz7oRLbZrpC6erlaSgB9zjBpkwBo3y7QtJb5xpYmkraZKTv5pxzZja6wiyOpkDg251Dxd/p3HBoF+R9XphwKx0DgsJLGWguXcBMX1Y0YwYqJNDtWXCycxxbeslnh36qg7Gf2EDcMlAkXIJkMptCMcGDnHf/b3ggCKQ7YNZ3AQgIbwbcBOgXCjAv07dTQLzEb8Dbqc7DXux7mSLQC2/4dMMn3AMqwVku4Q7EXYaLKQrxLmRUlbN+kNvRD2M76weqHf1QtLN+6PDRDxWWeDb3HYYjEMfHvl4IZCHdnh35aEc+uuOM2nHTQ3RERcofP9Jdhkvcw1kmqUDr0XKKW83haVLs8jevQF76yf/gH9IK7O8y7smsHMsAT8K4Bo0YO2JPxJgbtEJEAjwJY2bQWDuuqnhHDFUpqJo9SooMED+df7J/Ir9OCJtGkulReahPdXcNc+J1snoAejYjanAiPaY7uenxBMWoTKcJzo1agzzFmkyPyrlje3RKeRpQFtpKuFcdZgFNVEYKU6ISDC2FE+yldgLovLiMMZEUfunUomHNV8Um3V7oX9nLBShz+xLGNKDMSq9wKlB6cTmpkicOZMcn86wQzSANnzj/UoyD87Q3Mn2gQxzwAqJWk9G4a8lpY5yJrsO8qSDK3OrJMmeViQbCasayF7NMdAC2yuSOtsQmE5NaslTDLyfQM1dTXVpHe6fVSGku1pmh8ijNEwANAmDTBnFwqW6pu/IExKROZccruyN0EQEhF5aqCXxKp0T6eudOjbzE6d2/QX7Mxs8X8jaKPH0ENP82RoZZaH8mPEJ2yC964sDzcW2ECOQvY8CdvbEpLZ6m9YTp3Ra6ez/pVd8c7F9OeB/2rquJOI8FI9PiQGbmpHXc3Ud1NOutXIAhz6sqMGonBMkRtBqu7Euy5ZjYZY4ATd4GxMXlGLfj23t4PxWlFM24520JQS624FrMnnNxwpYRqoX3X6bztZr1gXTB2LwyZ+mlmINSN6bjZII3j3zaAIAqAqu+ZXWug6tTxzTNJ2SEvRf803zgLrC2oDpWh3ycae6LGK9wWUB5HKFOXrXY3MoyeoA0JklypFPtwXa0eiBmsxg8aaN7TFxg1QdcIuJ5ig+h+mqPnllveZvBGMegl0Qj4Aquj9jU+y8CaFN20sLwBNHnZrsZOtvrRW7mc73HjUBuYWkIeiTJhgE5U17EOHutviikujhDLql0xCtBCm/3JuXtvfiN8S9Yx1IgAoQo7bokL/vHVsoeBBdpEndVJTUspa9Q6LSCjwt5rJh4CNs6PKRLXuXrHhJZnlvaXjAdfPJ8+7B3Z7k7VAAy2V3ycBcPKc+OlnsJtjRWRj7uD9IOk8IFoWAQ+TxBAgD34jw3+FAaYkXAcGSjk0ehRxsTDbCu5P9T2/LxKx9hEFIuW6DrRjEb7lRsg1aEFgtCd7nv5pT3QvwVmwEuwntDpiFfFOAhMkRp2izT0bHKnfk6MT+4L/pWwYgcooxyGpDn8uc0iMaIv3tB5En2XUNPJ//Q1iqsn5fiNA2BF/ERfxOc5d53+fReiNqQoHW0X77RvdeEBACKEvYuCvExVWyqd8QlYBQltrmTD7H+aYK5gHjbNQMpX+bnRuXmtClxxowAgpD3UGdSP8WhurdROOg2cA5DFNC3ci0RekMvsJ7OBC+8Zyoz/xkwfz/h/VEFhcrBtYQLFpKk7u983yhofjNbDlknc8ssXIUaBR8tarK8+0VR277Nz+exuH8asl7SC1+xsL+9glud//l89XPfJ/rorsIPGyMyY6xyHZWHA+btp3e/D40qvH4zaW4xnwPjEusIeqdEdxKKQD+daShXcAUvQ4jZqkRfQRRpxkoF2wpYnN4oKE5Eg63sTCSrr415s0kDoiGJVNmhJSup+ldsVHsaGCUNoRKUkyw7Bl+prFCH0PIH6/UNI2aX3pveDKUfcttzukWt8aUZE5dCxAGA97TO/D9wVg1HaMFa0pj2qMLGnO2l+PQLzqpdmNflOTSllUm5YJhoQcKW1uWgIjVeIR3WaEKSTvBvpqrq6Wj3Fq4f03CbjWViB9x41v0clgzBd1gkTaNVZH/32SwUNOVYrf1ZWt8i67mwGQ3MN6gUyi1a6z6T/imEk1uHPlLeggZs9SSEmIVnqvWVUDDc8VSIjyVJDWUrDzHQbUhA3g322VdcwX2EruwQDNE7EKMQ/q3OrF/0Dquy3FhSl25U+nhJfwrQ3kqANrzLPlRryghrcb4H3ptjcl2AX++sGkMC2zb3bwpdTDqB+dFUV8i3R3tBoLxHv7pcXQOel7wCLG6ZmaNqfMREc2/RqU9+c67yDUMWqb6Rf3n+RublmRtpwFv3w4Pk+T0WULplHtrUXBkCnsYJWMWHUz+6NMu9nK1a0XRT/cszSVqSMKETDXUqSVHkAbHC49FiOmceVBCIMBOmhmvCO6ggqLhvI4nwYMvUWg6HbJx/l7mt3+dKvskUrjZ/RdulTl9cCUo/Vae0KXb2zduVqKsMX2VEZC/xwjPIEtiOTsJRJ0hk9iQkpiAEVQSOtoJbS8ycG/PMSH4LEH6nOsdbKUd8gEv8Z/DhkUraYKf16AMGWuiaUrFlwC/1T+QjIlvYnwBHe7LcPitjniWtu6haeDZi2uI5TJsQGw/c2k67FvffmAEgYNOIKyVnTjQQisObsek7rzj81pujuwYtUl+AyFR4YNgD908V3NOKd4C4SyhrvADBsJrD0xPywNRM70K9u4p/qfuG5OVkL3iRVD/xk6l56AbI8Cg4peozjyMhbJNqcFeMFJH2oyVXPCRhBARGZ2cd8eSo6kiybAskwyuyvT6EOFJ7W2JvngogIkShfZNaOkSYRQoFi250ZGpUXFR9laMzWhgbl9jcnOqSGzhyqixN0hKsItfmzNuFlcLRivVOr5AgtctXfdOB7AQoc72llVrZq5bK8byKTYNpzNQKsQ0rMaVauGUni3CxlZS+aLPDj+HHuZRM4Dg6T1lY2VH2Ds863fGQcasx/Cq2AxjG+vsZSZTHMcCJtBdPYYOck+Qh9pt7Dj1AunoHRXoIA2ZEG4Klu4j5vI2eePRDUz85kkj//IUu5pF9h18V4sKP50etRp/hvF1nuxioSQo8OsbyO2XAggAYnG0t3wsL+RxZXGfIYjdHlhiXU05I9F0gEW2eRIYjCY154u2IQ+E25MENKVLPvDimeFP0n1O4JWqJWc8rSjDnMFiarFJKDHueQHI7g3GhoZjSFolN20CRuVj00SUsdViMWW5CW6kLZQWxsrtN06N4UOdRIvSOlC+voYwptT/Kq+ZwcWwvOerKBq6sb11bcJ3YkcGRFQjXdEQPjuCD5rxDW8ukM7fSdUX363AZY0MoFCwX8uUJJnVDWzHPTgeyaMjZFzAhHtrSaY480MkXDeYxkyjYuJyjFx+Lh8oIqa28TPyvd3NbDFceSG1Z7ocEMvHPv3Y0jBEsGtiIbmCMLrSa5EYPEGGiDK1XXbVbdeYWuqDe9hM5MCLgSSlPoKRbwCMgUnPd1p9hpKLOBK/H6PCcktucImCIHQs9xjg+Z68f8iEpyK+WhXcuRM15kWUCPVovbkCwUALtvI1fZ5vMVffUGcHjWXDkDGo3bUyf/WRdrpZ/0kIfkG4iuZCLrLiSYn6wjuuaou6I40yC4izBe7L4tD4h+kY1ip3Ge0nNe+dS6zM1Ml2Svu4TFShKqNwRKNgFmPqphEvo5rVOueWFLtGYOWoHBBscNmIaIL8tOUVgQwXu2luySdE7ElNRtphsRPF4UzAcJARebS3LnRrvFiHemFCSZ5cJvuYOwti/rQQxgt49RUqcvgxEOQkcK0K07ntmXA3ZBihOCZjsIhax0EWUpRj4e2Hi5NFvWnbRhA/MrFSK3vf0AkbUWAhiGebpZhbT0eXW0qFfldoKhRSpIASsdwX209W179KhKBwtGbcbSnNwRcWXCdtyQroS/5HFp0JxDe2q454FhGFL+8IohsSWtuk2BF/Pn6QIDTf3oGJea6CnBnfF4oxGDIQniusmtTJEMXS/hLZRrymWAW2+qAa+NCZCWtX6PS5iRP8L0N/lUtHpa3c/JEFDpjzXWpKjIdbrEvxwy5ehjGu9dExjQhzplaaA9Qwl51rgxiXA/AAbvM/G4sLEuDZhvWu2b1pOrvb4RbOMOx4YBKSV28BD2RGOoQq33fx6mf96ciP/NQwb2wVzH5+43bkxyKa3R6aW3ia2/lC30KIf8Em6brIJpT37jZwd1ObzawfhS6j0vSGT+JY8s88gPEnddz4jAF3OJa2246XbkLSIQSgdbGN6sm5pzFVUDyt3N8CUFLwWD2+/Zc/s/Zq4M7YsBltfkyjZkK5ENm8D9N3a9q0PHdbwKr1MLdVs0ervXEOIj8eTPbr/h8EmpmAPlEcSqmgPJbYXN5AiRMpOz1DmAjsiyScadDkKafy/NGRdXuP/VOZB/JqYmhRntFEnfm4gVQ9lZnymKh/CAsVVNCDZZAMYva4hGqllo0V+6l1UIM+P9AlnlhXWXbBpQphHmS/lEbqpPMTIbEcv/K/YfpWuywkW+3HbDhCf8VryoMi4WjypNqvs8gooR7+1/HwqVlTwibXEH++hZexGNq9v72Dyz10jHk3oyqAsEV7+085OcsHjIfXfMPYfdlNrPNY+yNzAafN4aOkulJhzmHHqSw+WcKz4IuU4cCuRxaPw8hX27UviyDVgcOOkvoZoONk2bO0IE2z4lg4rqwbVurYmlmtJ2atRbp4Yuw7XT4Ln3LI5AzR6NmsjY85aakNh1NQoxl+/cWei2EONbbQGyOYO4xiZjYRzyYoU4oirC6qrbkD7Jea8hnMzJK/HkZdjpEfsao5CwUNsNzRwJotMaUACoWa9a83nCkb0RpVp4PDHk2yJX4l4XONDr+q0a3tH2Sk4SC5+qMFX0aLhwiUGoBwTihB1raLDgbpanps02m218AN+ThxKyacrtLErmHIVCmHIguq3FDQUHA/MCWWGRjVaAGi2d8M6shy34jLOC4nTcKxV0QxeCvcayC8kCmRnKfHySpT1xcVoClklFFbFC89Rrdm6YNByua+Fb3z6YobwMMmI5YfOo7KsfDqOxHFcsDmRw3MJZOAvwksJhWsfmbVvfGTmyzr9hd5gbOU4LFCVyzzErMFxSH0u/VReh+DrAuAGv9cO16wlnJ5pcnHOZ0eLfVXs27zpQmKBToFIXcOPRE4lLQ/uCp04KccpV+KKxyEsu4qqMx1s7dgj1Xi6qoeTjfKRzT+nXwEJuMAp7vMv4oOCm37QgenKThE6LEiSOXQhHQ3Q25PTIz7S8XnsSGo1bGyV4PVj5kyWRamV7MsoqxfQOmvaFIuaH+jB15pkpSnoRaotA9ePFRLnxVGI9rEk1V3i32syw/nQ6ZNP9nP3DXB3VGObM6ejK03rqVG1LBs9KT3Csjlb9IQKeKMJ2/D7OXt6r37uO5SlDrZ+ENKgRT2uaDzl0iwFnK2fKxnKmCNL0sFvcXi84dJKxQ7EiZWRzxoZtZMfgrUxuFeigT2DPhcYxCcs9aMKqGpx76BHiH+foN+juXPglomvVe3hTYSoc50I0dngwuVv9uKEse2uRbuF9lWLeDnDm5a82vmPnhyeoxx+xOdwM3I7ZbKG4qb1Xu3iR08Bz1EBP+ILeFWLm9DnSyUZbCZVrpOZ9LWJmRS8tue4l7aMrfgSMQc8wBJ1rpNJdLaJSZS8tie0r4aYbtyvW69XO//Rk8NzlMOP+BxuRr6q/+td48Rcr3bxo6eA56iAH/EF3IziiQbp/8tzV08t0Vo+N4Hdq6keNchfq1IbNj5QqYWUamO8gt2MpJSM/BVScVgswiCvR6i2ZGZbs631q+UQNF2bj+8BFMnsopea2T23xIgWAkT9aIOdzDSR2r6wIanNCpqcoBZKdFaUkWBGFkVpPqqpnkRaLNfL5zqRYLFhNdOFg8ZcUEtFi4q0YCUXF7RZ5u2l8B9y6meLw0O0vo0KQGhKaYWp2wMyO4qEjPmDFmEU12dS8UORw8vMX2b+auW/7ZPDb1Fz+KEo4GUWL7N4tYrf9ingt5wFRHr6RudA0k2o8anDC7I6zjCI4Acts4RnPJPN5T8owGGjj4DanQZYpE4PyOz4mISl/KDl0YJ9njH9OxQgs+kfLzN/mfmrlf+2Tw6/Rc3hh6KAl1m8zOLVKn7bp4DfohWgSU4/2yb7c4A+PwolK1iFvGwvy+uVU8Yaj6RSME75Imh9q0FcWliHSPe4EJe5UjrK95T+7S2516GsQa2n13TE2PB4359tzj4r3HAJJzcwNmhcPhM+7dRMhHsEU2C+SysaC8kYtuH+gZtN56BZWpBdSqkK347vxg8d7hBdKtnnqFsJEvVwOzc1d/wBMrjl9XRutw6e9L0Zw/90FMSevZqT+aGO1Ov9zlodnhcsStw33AxyFllYjv1DCx0ge1xUc2sd46vwqJ7bvR7braUluhLvWruivYHfsK5HYw2LTCO/qhgH7ZITMNpqnk0+1iUWWVVVYBJbwR1kCE7PEF0XcmGooLVLwC7pJIS7lSn02KVh7Iai4Kk5tQzaySkf0aFq188ZyUU9oj90zXRRhMcHlXaCNdJa8TgqFdolxqy7x1aJSwejL2LlCpAkBCupxa9MXOiFb0VPv+VhvAnslH43x0i/i/u0DT9jbBNKjNhodDI6bwlfTE9Xcw39sA7JVCSVIQkSB6uYSeszqrQmKYmmc/5sKhqfcw+UUjZ8eUXafF4pUSV+ODVfvOEetarZlosD0XeoIPOoKHowltbLHE5mnYJOz7L1AUV94P11c9g/Ceb6JlWuhhO8xodlxl4AObCgafTabjbPLyXSSpeCoQdvGEK5Cw/X6fg7j7emkcI5dR9GBDpTX7mYvrS+sQsaky6KzED6a6yhIdNzKHHn7fZzfrvCCIJ2H7g3+z+390Txdzv4VXBYIdbgps7yD+feddmmnk40RUHgWR/EwTFOSnFHmTUNIDLbTijoMkk2Ri6QXDM00ZPpzLrgxpb4trZA/Jpk4a5j7Nh4KOiWTljn0FWO92LUruNPnCKJ+9Mk6FS9W0dJo45OInXWi/eNIaiXtNOtvttp/PoOe41Js5u9xT6pkpcPU9VqztiLOjjKVBuAxN9F2rWkuTsjh/aU0cDwgBkj4k5AgLltmU2tPBzbbrMykuSu8FrtgXD0awWTEyTWsCWZrWYjiYb9v2Fphj2+4VYW1nmBIgxrC4eZ42NbzNTpk0pt9WFLcg4ToBpSM2FjEp0UAmxFFit6lk29Y21uWJ2O/8bO1+tJ0ldwAmWdjrKJwI5q/c/jjWL+bZFYaS3GbvmzAfxZAf68AMwpeGt3PSB0yN4E4pe9FxjjyAztXewDh0erDd05WLa3OEYhylcD+2wlQ9iY82XuIJyhNP+j7BCqbvOLmrf5Jc6tD0Hb96Kb9dr8VxMstg0399LjnBX+bO3xz9oe3432SLh6lkj8AcbPFXsPaW9eAMQQb6n0fuVk6Ow8mVITuJcH/H0EipKS07BuBPAbFB5BWa6CT8gBmW71er7fm/ScApSJmgLVbQBao4/YV09JFXWpNmQPdFpHW7ANFLzW3U7GUB59baVQeFEXvpreDXTlYcBZyjT2bEQlsx4ufeckMSnbUuCdpDtWztIeJ8hOYktE7aSY1K3qlA5jXUyNdjGMJkmUXssz5UojaWabTpB33l4fS7lLs+Tp7pquCiC8JTf+8jP7H7XRNY/V36H9zUXWViKWqHONssNkd41Sk2NTmw6kQVNOZEJ9ZOTLKpwjx45cjlOixUToWXvyi/y8LqY+KS5k5bXE5W25gzOmWYdwV8ZakZn4u6kyP+75O+Q/oYmBhqw1KKm8UClmj7Vjgi+A+CoteyKVfXyKO+2SA6t7JrQFGFKXABqHkVYXTUA2lp/JDr5Zd8KZJ6ipH/mUBIi6GunbhPiae8wvYPMKpBf9VCVYJYdUoQjLAb/REyJ1LGffx4NBM96GDWckr/zxvp6R94DF/hEeftSzqS+QxA6/deewyC38CV5sVzK2S6OfWVLSIRllOT1a2qeG7J7x9LEXvbJ9xKSgQ9OOvcQQVDEcGvJ+qkGYq1XxVrQ6zceRC16tf8VKTo4k1flXJ+8iTf7hKqa64uePizatl0ogIGPc+GLXty8H0yRcq/hDMwNkRysfsZCO0rEguSS+ZQWaVuJZnCNQPUnu6RDOm7gfqSPxiebpo3qcenMlC+I78SKHNYaxLU48QQDU/u4TYJYqvbfO+NDEWDvkcZLjNlJFV5ybzGf4XRp9gpZSgZyJAXem+uJmdlikCkKVSJHw0U9SsCz92MqFVtquj1I5rHqdFg0+c7Kk3aak4X2FsIZAm/U8pQy6o28xhwnV0eJDprea7ussnkXfNnUi9o6HlAYVdCImECf67qPXsQVYVFTLZScm8nITnMTvicVx2SgJe9ht3/rBlKUcG2ChYrJIQJrFwqT2S0eopH84SArN0zcqZGJ8ZSI+0jQ1wbQhrX2e7q4IEpowq0hp/ZICEjzYoDzDUMBp13xqnIiMOY4pQ9IlybPJWtIxYE61VuZg52N3OUEs/JPzbsMf39FEoScYskwj33lOWsN30ndJRIGWFcCewkg3AtZpKQYutVb0QuhoiI2dD4ZQbTQv6h96nQTXoFGE1oy1DkVtEcDJGiVnRCQthN/FpgcXu6u+rRbu0OaddFrst2HmqGiIKr7pZYIY2NiqoNTsEv2tVpK9IWbFBAV07O9z82uT2ezFuYuD8DYmF8ww3YC18jaBqA1pqmC3hTqr3N7n/tiCwthXZdkLeKUXmGxep6HbsgIW7XCXVpjzEdehaj4WBKris6QGPD1pP53VNsNhbUys0LARFi6hLmlWgiwXQcKVVdBJ1v3UCNYMVHszDpXGo92DlO02NQizUX6ZlZsEwbGsgIHXS8O1Sy5LMbcdDbKedGG8fdSDclx+VMApSh6UV/9znIjAOrOyzJXPgBbwa8Il4ObM10CNXw4W9m0LiuBv6KuqVuzA3mhBURzsghVElTRjRa0YGgqF9UWBwlr0X1UAZ8hoxXXQ6ImAi/XwRVSw15x67C+r5dVDTG9Vcqr/er7+60nZMBEslXhCa0byRX3WHpWOPR495zM/WspSo4/uTjPIllgE5vY/yD+FJaci9RXnLbt6cfyxKPEqAz8KyvqG7aqrMR0KRZc24+dpzdSZPRCyVFBAEIoTFz4vJoHxlRjYgYVRGrfrW/KCLSGdY7pHV5/GgUjWRbLh9Xos7XdcxZvEjlJlejrs8OaM0RHgSUZCVNBMeEDEOciUMhlMKU0MXIL46AoWG26GZdovV3/mo2aL2lbkHZzKX2SgbL3NGlU+LMFY0U9OnBMoXnaRgW63ajr001as0qdFVou/smFBdSnBNTOaU9RhG5ZgQCVoepy8RpvE/KhM8wJVcZN02SXAKwm6kBX5d1t3ojrHHdwFPXdFDiRdOfo2iU8V6kNBsgpVWHg/MXB2zEnVHb84HodMmj/VifodShUYR7eOMxqkRTYjR8S5JJcgtpbDQe1gzC3l6RF6xGMxNfedO3rmFYRaWixdVUuclapSy4wzmmbZT6WNngY6mIV4VwqxTDZ2LZsr2INNVI1m6nqJuEjU8STA5olNCFRCD9iVceHnIkjIVbtzzCWM593jTCA0oapCC1Luz5/AeBbcicreEYHPb2N3NYyaYOQEeqXF8BwSRaCeDK3zBAzi8YEOvLHXOWDvcFhkgmMgDDZTEKSiKo0uxAq1kWEBRZbMVAhqNJqgLBwj8AnnJpgebbqVOq1iBZkzuIhbdM5U4WS7vR/pQjcCUsRF4A0upxXEXe078zBbvu0S2b3PDEMpheQmm/pWRN8lR0Xgu9+Vip/Q12lwfh4zOcnYE+yZ49c9R9m7xymVtI+EiC8uNxN59DRYN0i/lw9d2BZHS8XPJjYjyKjuiTgcgPp+RkmDDa5EtYK1NgavQk/hiEqQ6oz1wxgLjiSJ6MS40RSdS5RdcD0YB6MnYIwTYN2o1CmmC2evJ6Iyq7G3cqzNxcVVNZ4NbSqclpq9IbwC2YAwXV8GYJblzMTP8an10S2aPaxVFJJosGLLkLK94ALmF1hGVIRazeKm2lJNnxnQRxtBZx4DyNF5e4dcYeoHYaHYI3JcLTiyK3Tfb45ICQcDvukkeijvRef7+DlVkuCAx1JgpuUIeckmD9UBPHOJgPC348CgqBtEBRrWFpvqqIJU0lQtVUKMDRsa4MkChTAuAOHLMovCnwyQ3fE4cw0F18SldXe5LakQxbzdCTycRglzYP1Ird5MaHp6CMouPS4+NzrURHC/L6T/qmHUxGnjNaCIWO3oRpX7odHKWAacLy2M9jHMhGvx9c/n/hmTHUxDbtGzrZhTKkKbliR/R6uflqNVn6RomwEChEdXXHQKP1ajPQBmGfxiBeVlciRD08TWmxTW4AJsAmDE+D6DPmtOcmNRlt/JPA3qtUTT+5lEeT/fN9EtyBZqi8QsBWDUjZXsrcmC1fSglIJVxv0AWS9wJJaFpNnQ1Jcw7bycGun1lAaAdydm+kZKg8Si2fD3dCLnDP7ueiX8tpKFgnZY+wDn/P+gzZAAf76rUw1qjHNPi40u6yA7tNF34JcthQec8Gp5lKIai4HM/cdUBtXtcEUl4YkFRqcbyK1yyUYKf/X3p1uwLFUjW2U4CNlk2qOnUwWEQGupqYkqznGl/v/xqDWMUoTN+L0TeuS7P7eBZ413lR0AbVpC6C5qrqNVG+x71+9FRPmzKrShK665p8WNv9ypEboYURLUhDzssNRk2HEdx0wU7zbCaNV3TPICOC/JpWydiduDuq4y9WKa0n9m+5RuVYfOkiPWWpIuQelpPZg+UWWpZS8Wb1Tu3qTMqRR9fzemPWlEmzI0UZ0IkE+fTRNk2XI0bS0Qexz+gbkXZiK50hrguXCBgrTajBRohzUhWeYdq+SsC+WfraLW0fIvaiSd9xdoee/RnfNv2v0SajqmyzrFNcMcNDhBdvbUUd87sEj2v4Sraw0GW52jbFmDemaeO0o7AKFE6sehllgFXD2Oi6L9kmrekgrRAnL7MbIVaFvtkdNosFgSve5CSeTo0nEUdTZf6hDa6B+Rdz/IeVnRNzeGEqrqP7PZrhajKIjOHwdeKlU+qNT3z+yj3ghLxLmvb1+ljy6o43vXCH/zs5W/Odh+sxcYBFm2BdNYnFybJDQ7CZT6CB+8LRXSS79jYYYxczddY9qXlqw7aUC3kGzHHmxzJqR0u1BWg1u6XsiDwCwBN4PckGYBJm4djm8VKusOH6YkN0i09eNdrFMLKu1SDjmIMMfrQIOp9st3kfOqVYqnjR7bEKWx+QZ1kjGfE854uXj5yM955E5qFmIWrvHC8+JmIdV2lQ2AbPPYjwyyGP+EkJGbS7vl1CMCNMiiMWo7lUXg71fiAIWPWbg6MogAQlrVVp3P6OE/A/i/zBXOylGJ9vcrnG3nNy2boJ3TzQ0YCHyLRSbsmpWRvoauwU316/DDw3DGSlWWQFNF6vqlPqyVgIc5VZ3eH7RDYVDrZNODO4/EESpLf+d1kNfn16geQVmwtRaLXkgTODNJqHsw/W6PFdE37bSryaHIFW0croYzGXlEXDZKHnHHiZ6uQoiOd0q1hdbH306jpgWy+A3jTj3oaBkyU9jpbMdyQ8Flu9J9GAXENHZwFjocKu226y9BMEStjQdavAoS2kin4TRD6IBNYRtNrWF1VyBR5VLI1okxqEniZ4pbZJNnBui8M+myti5dUPCbvZotk/tay8Z6A/1PTfLVsqvqSxuQDSome636G6sWIbaL8rugPbVtQcvWlAZlOh2LCb6czIw4HqVAUGKhgKUpm2ow+dm2yb6hyqOj61uI1hV9lFBMzVzSkRSu5/syQs/k1MyxxvfivtKiH0+RS76mWdwU8JPkeZbx15I7cb5hrhJ/F3qkbj6eVGHixkYK2mDKCO/D9hNoQMPY3AM0zYm6ImSVYeE4RSbs82JD4/LJCpSvKPLCmNEsEUE3uFb7yxWEz1xIuKe1mkAeEJPki/ne7qHNanGw0B8skq7mv7ANEDdJ/1/wBCtDa49JyHk+W3StBXt5fpmCQ6gjv09TAMzvI0WXHs2K3ZOON0DEmQh0pKVyBsZNzOFx1DBWww/kamqxQbZK/NAZded96Uxm25i3ROlCAsuSaDEsRicrUIX44UWqGKXwa0/s2TqAzbs5cT9RF7cnxMm+zpZNE0/OAV4DNp98x2eusHI32i2O8lueLI6m/xDrOVmDk/Q3NyuvvKHVolJuIHHjtjzrvgVT6dZKfNF4cohqSUfPigrOWHYH9ZvHKpRgg1+jQVaZDYMMnf45bmyqlLxS8hJxCTTxCnxVHbesayNiXveoUqDRMf7bB0+3n91X6vOLGJwsRQ/6EeyKm2wF6fkojZoV7QodX1N/9H7JL1zH1+zWXpzeLkq1gAWtqISvifjqVFtGVhjB9m0DUf6VW9qsgwqrKuszjvquN/OhfVfq7T50RQ0PETnow5eCduo0J+ohJfrELW23dAtNknex00QSq4AMt6lqHQ0Sq9RQOtFIOnXzoRjNWEoCkt5q9mh0YwH6/oHI0lxL6GFXwMEmC7VOPG7o5uGkB94CUrQlXr+8sNcNG8DycmTPwY1hJcFJNpXACFrbPm5Hu/kUgEnUkU6UdG42BolbZT20EhzJCAe++Pj3RMqLeAKzZoeFow+QZeo1xP5UxiP0PE13mC9Bbf3QB3MpxAbmVekOIiVGHI9yGDB43S2vHry4rCgoIagsydBhUg/TnG0oVgFcWv6FxK8VvPMyw2I3F+xtq9XOan9dQnJ8pw4ZQGrWuVdqgJJKgi1GM3hnhXc3x4WjSV30fQFrN5NumYjEEZ2XZWQJtnHizyR2oyJ1PyTAkb+p6kJH70xStfAylZLcj4wNAJ9rl3rTR5LYHsEoXeTN28H5KhFNh3AkMukJCcF4arcUhPrIQw2BXYcttoaUKnBDXeMhIqSl4U7NszQpmNB6p+r/keEqwcIKcV4W7rlCvMjtmau+szBLG3M3jlZVbq9JZ2BDsnssKjXTv+X3S4o/BF6p0VTNYUq7WWywOdMab3rO83zPAVIxagVW85UUW3fWAe3dt2Rl8idsJkyj0jdjLd1J3f2JhwsbKND6APrIsbECgu+Pfx/hCJ0Ijwbc8gVI3zHhZAgeZSwbzbUBa6J1Ht3VC+uyfqgNSOe2pZplxt/teKw7a2WecSsnwjg3g18VH86JPIsZ06/QFGzy4k2aTzSUPVlhRtxxdforocmcv+cUEJIzHuSZZU4oRnNrepFcUeK+/B/KFlOZgL5iaaW6WKTasc6sz8h0xekwgeyV4lTlwESU0NVXl1I4SJGmdUlK5mF+EEQUlOAgyDVbuOZ8slqVXnQbcqaX8o6PjvZ/Rt6X2zRlo2PXmmtQfpbGdjcqv+osJHLLqK/ep5JrwzMi9NSu88sPL5bwGGkfzu33EQe4Jq5RasLPVirtlSpckjEgeZl76PI9shu+3FW05DbQsEA63RuTcNobA9XH6MmpU2iy81chFCASZbMRDFQAvxnb69HrKWhDltX7iWW2ODA31FnZOLaOW2dG23G65iPLv+HOpOVk6GCWTt/BHYemsg7FeBHGtAbKz1pNMv/RmwD1Zs9/yoB1GC2qTR8vlb9U/jLyV5i/VPFSxcsoQgPHMI22ARu2Xd5dL4gfr0KfSBXDl8e31Q1HGr58ke4m8CbQ3zP+K0s5u6rzsjV4fyL2bxgBtiRw1YbF90PJGnepA76rI8/uBIBZNq8pEBcVQ9fKYY1Q9W+n0/uiWfry/KyZc/7jQCCyoZ6gEF6AoxaaqsTzqKeYYsgvq8iPvolGs4yjrCK/Jq4sIcLPkKvamI+n/dXwLCEEP9CqGW3HjNBcyGzBaiwtW0BhPzy9t+Ln86rqfrQv85112SRNwBXWSlHIeSw63V99YasVR5aVSqeOH0pW8tMvkc8WuwY74IYBaQUsZyaEBxzIE0I8ZQRY0hacElVpVM8S5evNOC/ES2nyAPYns9+WaNp+y6fipYPuXESrnj52YTKrAkejFu9Hw9Fj4IdUmrvayOOgVEAund8FZ4sp2dQdjkK1Pf3QNd2fvtkd7B8XS2NXenypRL+Hon9NG1LCMIJDhKXLWGsJmUnWQXdHlzN2Hv7Gbi7PKDWo5mlUnfq0PQA4xk01N2MfZQU8FsabSE9oBd/IMs05PpIqoNNOSdD5gYdmSbp+pxt0bFPPC5ZRfdKK1Qrpm5m1DNJyKxqv9+cSqePbj1O7inAzT6wth+fxaoczeP1Ds7PUKNtcsFKCLgzbrQ6Nnfq0K4S3E7DXtGQy6huCbrCUwPYw/DV4Iv2ftIu+eZCrLoVeBV7GKMFG5/afGHTlaHQknPjFd5vuWBFgTa1mUe4wu7rLgUe/TSXJuxbUcHvazg1mYReM3TSWQtVzMuR8PDqOMqcrHNZzilurw8gOz5aOTz6lecGo2UxdwikAq+/B4Y08kCQg7GdLrCJh6HHbw7IIZndI6PCAeQUfNR7Ig91vxcfSr1UOzO4Pnz3CFfwpc0J+/OjfWflxLhSedP9iyIDqQLSJY+FW+KdrASi0j0SIoV++bU0hnbFpkNAO4Q2uWGCrOkCfpstqnEIyDBAdPK90e9ekf4t2prf3EoPG4w6XdYTnO+B0xuFMuayzyxoMd+LozwFTXJunXxSsWzoLAbHY/aBWtz4Aq+gjBmp0uLqqZFVmzMe667BjX7YW7zCp1e2Pii4AXgVXDII+/BPsnpScoMTsESQMsF3/Eh8sf471seuNrt7zQl9VYAo94nRrdAJ6rjQYwz/XTMLpdElkAHvD8t1vVCwUPrbWULJummOj8O6kJxKYz4QgQXZs5N+tsQAcztojYiDxGJfCK/+nRRzmn5vP/Wcz3Kv685cIozBNdq2xSfHOquYtoshFbAjbyXt4L5KS3kNBIgvKJdw369VJPqlJ9Hrp+TvFJ1/BYblRBehZQt+JeuodhpLsvx0Xh3NWtBkJPcpJcbV9QdGIx8Mc0qczZBDWt0hKNL2IEGVJ4RdKoQzTVHXBcV05Z7ggtZXQsPnFMnfmPNGyLt4ISDlqVX9hOdJggNd5LEmorfEAJXo9+FmELjEBlGeCERnShTNs8Ol/rdLnRYYjG/k/vm4QYbJOptM37uRgdAGnRpMx7LgQaVzGxQRcKc0zqV6w+n8dfmhUfhXfFASWOwefiP4oaGlWr7nxMJaFficut8qCYUpCuK/SkvRGTHPBS2O7ctkJs9mo9tzBbj6wX8vNlVcDG4u5Y77CWu6piwslW6kP19YxMv8qEJrP/eigas9uXc612kMoXBHBAWErkVw7fFSo6qwMXNicYwalMvs2JYKoaXASZHh++kLho0YnuFj4kABFwXYvEnTXUze2r8fO3XNGNqCU9famqQ37u03RWCTWkqlPcxII2o6ufBLXrBap8ht23rNeqG32rLjLna5p2JrJBImqAa7gGBFRdVDFYvID67WEpmFRG0NShaOOonUb1CuIbYdeyp/3pOqNyzoQklUAv/c7qNp5qxSiHyE2nzhtDUdA2mm0vQLf8qSXNg75uMl+4vNxxu6OXPRPdNZbh/NoCwqxG75+ZfkN//3KCkOS39TnQzt1R/vgyj4KLyE3gKt6brGYx1WxML0z4u5a3OUXVlOXz926BUCErL/SPkN+Ww4+E4qYCo4r5zzYeNXiL2SQTdTtJdkDjN4Zjlb59UW91/bOxUIbjOKXayzU+3WQvx4J8XTX91p18iMLsYRfdUmLM+GYfOczQ9jnHwuE4VpU6Kow5oUkBlFKdbMij24TvYuPSRJAbsTPmDm7VInjNTevxHwdIClTJVDATLVsczmkIkhUVa6ZiZpMcG7/MxZ9A5gdanVqx/TTviKTl0g4QwMhy9oUTjiOTCAECUEEEGwGBMxmlNCgqlxmo1FMydegX6VcRxvEdtQH3AYBLmxfntCspkk5YPpySMXMS5MU0P1yKo1QJBF+S4WhGZYGihDIVWv6jxWKlCUw8ACumFjdYNtsOO2bKSyyiwgwGf30S1k/zZ+nsFwR50hNtinwEsRpStF8khAmS0zM64WA+8uYvA7TbWqubaeVUxuko0g1PCCvLsh8UoPEPAjAffIYrr7CnLFBOcx70HYwOSpfdu3ulX+BvniHbJgv9AID0LAoCsUcGLFi0Q0dMx7wB92rjBs7iV8rqgzHn1ivy7r0ifGVyCJkGD8LxQHt0uPDz6TfegbGl0ZYr9l4PRDjywYSRlAiQGnHpylWlTK+w/EscQwFsjZkGZcf1C/M2N/Q6dgjZcw4Ta1y2fdBdWX134llhK0lkaldXGsU8eJzeiFkTji1NRl1BmKi+EChj6r23ctHkpMAPsApZJXnYllG5WDq3jHHAe5AChGDGTTNcaK8B1bbf7X2aEGApJO/dsN0TIwIpVlIR2QVZj6QDvVs+b6BI6AkacmorkIaCH4BEYcZ01D3F7AsbEvJxv7t+EDbYSUpTF8yNhaxy67rDsFpPrss2eYZJUjujIIkCWGK/G+eER/cD8iwALQJJeBv8rMG3sCT63kQmKlvWX2bg6FjzUuapKg3hbQmCwRm3uiJPDWAjI0YlGSsWpZuHqkedmRrAiROL2trPpOuWCoOxOY5BnXB3gUuIb4TQ3ZybCAKPxNYbwnk/H30bdAXby+9EJuHOUiBGefJM8+Wm8p8WUzp+wLwssTx0b372Z0lYtaULanmJoQs0Xz3/GPccNwW5HhwS4c2nPyaJBNbGoW+sZj1fcLU8iC2E197uwsI0IfdI2YOeYiRXk03TnE5A7J0j1aepQ7PcWBBSbGBPoRxHyshR9UU47ugNsrUlcNcVtkaSveYZg7bom+6pkVHgoVy7WfB0tIh0z1UjWoP/124W7g0hYI2A0sIk23qwpoTM2jLGeIEvCXORTxTPXTVpdkNCEim0NgsNrNeeupG8f9mYi2ncMdFzBjmU5EqK0N2ZT8umpRg+qEAQRnagIV9e2pPV6Vg+B2Q1IDWpPxZ6VSoHY75WGUnysgr4ltQLToRkHBoax4hrrkiUNWycNTLmbflwCfeglKgNR3ihYusPtpSp9vL3egp3QgtkZoZjDERCd81iowKlH8FyKkh/RXWQMO6mstXWVmVf5dM/nTaMWSjGmWX4U3/bkptEPfEAvF2AivjshAm0sWZ+RxriHwqnkQ/O4IJd3qqreBgm34DAUzUVK2IqlQtqhVbAcK3QtElngnn32nU7bGQYxSo5fmcwRlaFXi3ZymjYzBdGXjfpjcT7RalS7/66PQwRyWGSw4PF6BstLmKesc8GbegCmINLcCHFpFYVABCGMt99lF2piuR9OdqhCq2uGRAPSyAD8BgWK7TRrrophZc/U0BjYP7CrqZ/UmtNji3WX45CVe0/bfhNUC8EAe8vCq2Anl0x7C32QTaxXFKbntlvVgo1dITLg3rwsGvJVIQenkVQRLTvbUaIY2ae/d8i6UQNbbl3V78sCXsIa+rU6J1/aOEWg76ljuYTzxgAXCg+MI1hVfWnCg3E0hI2WoAk+sqD24cjjqefSZjswrFWWSOxHkPUgLhg6vxURyK+YZQbTRrYEOEbCTQCoeFH55h5S1R2exwQjyCAnDRDYs0pFwNLm6KEB8tAFlcqAwwItdmeD0t77sG+vgSlIC3rZRD0nvfDmLBoYzJCQK2JMT3nYU3cULnlsj2mLlulSDlCqCUuJbQJpsWr/vPa3N4yzQULI86PvojYONM5BDmRwk3ClezEtkcko1li2fETnllTcLZovJx9cGpKZwapzRFlcvMfY6umQrN4kz06RWg2FfUBYFCMJOC1TL3hLIzCiCPRPyeD+FLtF/53NMKGuwpSLisWUT0YZlpLrWrVj5fk9TCrOd3BBu9SvuhsjL9NHtIl844FwnT4iAv+YjKqrNQtas9QgsL3Aia5sciKPe0p4NHjgSTt7JANoTST0msrs1NcVEKQ0BQTqWMBQQfAXzxpV02x8F9oEDDp81hlLA3o+oVyo3EybzPnUkEG9Y3ua6wgMs8JU2bfLvdKDVusUesxlQtitGBEkT7mzau8D/NbT8/olz/cxJfWo76XeFdanJNAcXjZ7aiipfTgDR5+6HudWcoL3EBYafoCcOMzDQOb+9mjp95nvAALNyk3xvcpn+rGLRtfwt99Tim66zZVFocorCBVdJlLaDubtGB22OlY4RYXHzew7+0fVDqT5LwTk+hN7dkaXvBMcpEmxykHl30CaKPDh2M4uHWXSTVS5OjMMS5CbfSINayK46Eos/FYTUKb7tPJA/OUbYbkFILDjOsGkvKMDBtQI3Ostlh1zb3GCZVVTLOSTYZWtyr4ixbP8w1gW4WWuPUcI57hsRICcvlJBxYkkpkKTkIW1RmDzSiRg0uWWhr3dcadTXCMH0WsU4vesHKe+aTcfzrjcD3cfKczVtLTALeQfcd8W/L3ctpgCRTLzOvwqx6+ln+cWOCnO8GeLq2jPme6biUIRkSjmgmjZRxtI3o04uoNAAHo2h1VUYREuktEDffUG7KO40Jd7N26nDNod4KkSVJYKgjdYfxInJROvLs9867PKHcmOg/00nHPYgBWbBZJN6SHXZpDMB2gfnkjZGt0QwCTtZg2oDlUqPDajfz9YDvluZ+cYM0lNepkFz/tDRXI+rVid74tCCRPZjOxo6jTqHsQ7guMut/NCuCbDIfkp+mYtvznk3fWk2CI9aZoGiM5bsUgpHESCFSibMgCESLbCZBH2Cr6fKsrI+5Gvee2VkQfASOAGt59ibE9ZbaiTO0sKzCAABr3c8/5jKU9cX4g8izKNoOjpicx7mfdn0hFwu+c3qlAgeudgl0wkncFYSWvs1NuV8fhXn0lOdDsYN1jY8RAQyZLHQ62r7dcPhy+++uAbLL+epnSVwWtVu0acNr701BtxWWCSRdqnC8tmLEqu/uQurxIjMa73G2//lFvfYFpSRyAgypaKoViWbjn6g1sFbQpJZfRiQJMog8ds3ngsmW2h0Tchs3qWnp3rKCdQ0WUhFekFtw+aSpZJ1GTkYRhKAQhYXMw3z0/3F4CHEQj6waRjx3xK+MWvwO0Pr80sVsdWSZ6hXS914VD6td1AqcK6zLLppLqzCBNBH4O4CHCOufJxUaqZflZOScbHnbyW0ACYYDgcvSV9qOknPZgzcWaDSClatsNmNZLJCZQiTyp2tQGXpphgQEDqCACqqmo0rQkbKU5q6lQ6nXbN4jOvtNer9aWOiU6XghJxybr2oP92FKXlY7JFwKbC2QAgcuIzlIttghXBGWdKkyHKdqr1QGzqFwt6yC9TFCc+K4UFXNp1wLYM4Vd8HUyLRGibrEXBMXBhHiW3XFxDH26wWIubHVH408qS6VKqzWt5nQLwYBuBxpl4xJYsut1bh8oMgbi6rKfek1OGWQQgV1IWlSrVLdqFUSXZpBlAxQLgFQwwOAqrkR6hkSMdjz5VKKS43rIGHlh8m7JDjC7ttZ7PtBUdANIhOGgAk7swmjQnHqIUBvAnx0a7WI4EaKE9axoxVkeoDw/0MRsNjzGd0zLmY/TmuFmySq0AavreLAY74j2eV0JJqfCRHO4TccPI6PioYwFqBuO7QXM2FbggnSBnNwD4cMmXiQF/Uji8WdjqBszrl9ypXUyNptCAM3Lqq/LFif0RmtEEypI8gO1cKt8HWOEv+RsPYJU4YPNNHooVgClUICLSVkKVphhsnsNbBVvZILQydJUbVXhGJHHgHcN96Q0YnVuW4tSYmOt1jugnjwBKgsSyeIC9ao6PHHgbDsnZbRGBKuJDB2R8imNWspv0ujEyKGx2oBuaOSMz8c1Anwidi7qGYObVulUQlTcEsnt7O7sEmJ+ZKMmsKw0OVNyONvGHhV8JJy53LPdY+So3sdGA8W+m1NKBQleQpzpIbqkABG1idMRao+joQ7YyA5STmFNZDOs9uiHyDSctF7rUeuJ9XiZfNH6ZsB59RJBcLSq+qa9dIeGYsvjHqgfebWs/lQblTwM8AD/LROf3J45IACIVwhAxELnUt0w4lcw4BJT+7bK23sgKIV5flJZ7bODFhlRHk7xyXD9QhKpvM7EioLYTXnVlqbXMsJzUPhplTJlDO2clc4gEbz/YtuIufRXuc6jGF/jzV2jFT39fvxlo9jv0EvtU6vWRrKqrNYo0J+frsu73dZFQac4PmTdqLqThKe2HQ8yq8h1Ba8tMW2397Z0/HEba09E26W6hPvGSNWb4QZs+N96exZrOKonbv4Wpi0BCEHAvVRTDC+DJpOUm5sjOLcn4UC4+j1/VZ+Z/rRv1FO6AjLJO/l5/JDJpGpchdzjCUWVluGOeuXSVo8qWQ9JbDuHBmX9z+QSumBCB+Fq6d8Qyqvh3MkcR2Ci9WEJP8BIQyQeYXjDIu0BvdYD7kZMzxv5Tm6hVLpEvPuqD7puw+Tpj3PdCCPkfMAxd0D0F8TJkquBgekN96DDWmt4qSpznTioj24IyDOFMiO1zUb2qepRXjnPaUYiTeK8fO/gvjoeFoZdVkeMQGrrpDnQrj6lciFXl9NNenj+kbhA8dcrEIW5WwRBCeqA1IHl8At4Roz2uFm3l/kbRxjSNKoY44WZ12PDebGtXVL512F/gYvh+Jad6IdE59DnoBIbUhHkQ9XXbxeYoOkHV4cA3ihT0REIj6XWDB4wf/Rbc3PUkDoT0pm6ry+tFrP4JinBGtWiWeqIUNJ21DjfTcsnIRuQHVakjaBxlCKqGKze3M2+vmalMTZosZvZmAv8h3nnpP3549T5olScoKFGmu1KlynGHNC1dzbnGUV3C1a4TlodRtw+CzNKozmQ5XzdENe8zuIUVpWj7FC1n7L1A3hdSJdO8fNNSeDL/PENutlVk7nRj/MeUGkw2EgnIBSEaOPKRByAAdFmQE6KMcqyNOvVBKiGkw/o2Ia1uxS1XNtBgq0IIiGF+nUmKdVREEjJluy44ykAoYiiktME+ET7iXf2k0HmFyOxgumbwxEH4rRQehdw0YwBMLv6MGrhXd8cvZIHLG1IVxmco8sZ+/1kH8LE92m/dcQuloK/exuG27B0jh/MV/LJmQXA8Z8Qo8S1K6X/EOu9O3MK3RnYc3jLGmD9nVS4KdSWrZfrN4Bv/phtVeZ4JiHY+ZhEKottF9bAMHfBQAJAR0WYeQK1SOu6aAImlzsiVayBbW6ociPK6oBIG3jbapR4Qgo/BfcnXyzqsv8Iqyh9p/HmkRtpfn6iSKdoRTFeIzZxTp0kYfpItGEM0QQTTpzuomGG8wd6XnalL66glQnwFPSuQhKMMDepDZ0LhOkKZPUXNEtM4e5G4QfVKdJI24cgWJquTu6Qm5vTiFhnqU0WecAkVPCZFQqtQPXhBWLkDdsCTgtRKRIptVUIyfi/eVEbqzcHE46rn+ASZXotL7Lo6fgoWfEAI0CsgrtNV6xNVKXeK4qU02wApRUzWnM315jFlRVAT/ULl74oyQId9XRtQ6JXqlg70rbVglq3OV15l1ABoaFGtY6F6nRwIdb1rKRt11tAbNwGWvkezL4CoxG37YLS4jxC8z5bt0hTW3Fkbvxkt6dcMZynFurAfq7S0kcJRoYFM9DazLKax2wKpjqpxwktdFPG58VLUcQB9rG30bwwMj1eN8ggml2wEZLpimd82wpt3Eqm02o6M1UaqKgEzmxIoDo+LxS86cK5e6KIQGO1dxd0Kpyb4NrgECIW/5MPI9S40ApHvBjs651l4sNO5EaPjlnbN67Sc8xHBo+5tAxbD2yvpLZTAE1KI0RrWeuAhxqRKAHon0mHq7HM07djhB1CoiokDpWOiT+0V7SJteSh6rE9x75q0tvPjphBOgIhyeBn78Rr6R7vsrzUOPeGnDkWn+oYKoPrSLPdu1Pa/bwH2XSNEgBGQRQeFLafOSXg1q27Xi16K+S1C5wXsRkinroVzvETEJxsloHsScIDJpicLQUq+q8U+WxmmDl6LODEzzIwJquN9pXnNOxIQ3OxOszrokL5CttLHMsktlFStxGRfnwxV8F3KOLQQcOIjCowhJjWMBFk5S1iteFC3X4OEFRrbIbxFiR5FbxYImrJd7wz3qlj9vSCCTSVYo+Q5/zZk4w7vtZFKF/NnysV1SqDHlhVZ6NgogDs/BRGgeW2sWrjwbUkF0EyNysRiiDWlimVkFdnSvwDVV0Rmo9reuzg6jlYlFT7bIPl8izuN0AihxsySddy6ADXAtNwFwxmjVOK4pWOaPmV0JzVnMgFXVeZk8qRslN2kgU5RAWCYf4ZOXCtVpBibRawXC5svCHhpxfKa+x7McGdcmlISVCw6VZF+SPUhHdLaSW/DozcMsrWTOPumJmasyn04JqjLRNSH/+AYoWeAKrRikuWPrTXInO69hoInSQHe3u5mP+Sh5nsgfbOnh1ugl6bZQ1v+x+pwkPE56AjMaUsdKEcxzon0mLj4r1PsvxIvTkl/GAV553wW2OPk75q/8XcjyUBj9/s7z1EXl+drZC2+j5nPwN4ZgER8uBh27nAsYd4gWOusmiaOIWIwawwjRGvBF6v8egifdH2GMgP4mdMInxlOTXBhZIn7u6y3mDD6kD9cnKTs3ub2za18wPiTHZW+/AKWS5NUVdqtkgVOdGQfgduJAl2oBrxoXwGJxgghbyH3MlNq1IusxwATQOEgVEqNhYte1PTHZdWkUFfUjJ+EbdrSNUGw+OjqV6IJA7Lrf9VERXAb6DppFwy0gxJVxNoWLNjm7OjQxvVmFjHh4RVTNFoIYvzQOMMf4IaVDARYs1MzBMyv016ybbQ+axQgCHamScEl7FdXRDVNf+aSEPeZV0xJC07rsdk7Jpl5aW7LEaTKTXJSfHq53jyJk5vYJmutvJdDlYCkUCFmqUkbNQGBU2q5KgJ/0klpWt8+8NYOBC5c3JcqjbU8ieHyKlZU88I6gpUKsPg3uodHsgkrhU0l3bAd3jn5yNlkfBOVIBd8pI50dqFxLk4xmbqUC8YCppZjhGgz9JIrrAB3Tm0GVYVMNmY5ApQFcl7YEA6cXaSNL03m0A4zzW+Fp+Gtz/hOM1Ebj2c58DAQmIlpIJZEXRc04Ucp8DpiE9NuyRY/2RBAHwAN0sHn9Ja8p4Okxk6ADvgwreGe/bCQnagcMc0o/NTRKoM1CD2SjV8XuAbj/053B2u0zrhtEBW7fq1LGVS3jppgNVx7+L1g17ExI685GUgjDy6Hm3lsfCJ5lEmQt3Z32KYqxM4PtGs4b0XEzp495Ft01CXVSg20827g+rlmRL1nC1MEsU3VjBNHcSatCD05a5K12cAoMV7TFEPU7PKSTvAkmeJvexA6XqAEXyaWnyjjbEN4H5OeQpsTLYYtcZ/2ehPsySnWXBVMHgji8kxnyqro628iMluM/Y6pHcGZPKv/O6M1oOsmpO/vg+1p+RcR/AOMubYsUMjTnvXyk55/CcyaNmEOXQcfw+jL9TBzrgMefK3rUnaQDpEfd475RhS7hgngknDolySDJUxx39FRNVr4yrvclKp1xh2Q9upW25vJC4aaNy7zd5mrikZnd/DwItyMjhuofiNk1JjRRHIQ/Ppn4ZkfeE1AX/rGG8awCT1FCIAkXFcTxcxYk0JCjQnX+G4108gSSe9QO9ktkignAeYmsOm/SsX8NYR+U+L7FmgOEgHdZjajVZCxoKsqzimejCZP/1L/tqAqvZzV69VRol9VhuobD4cO5LcHRVGsrR24kFJBbKRoxF+3k+ldB0zfLATKU9iM5JZ7j7RtFfAK+KE3UHx5Yd1yPCf+sMdn9jm8EybOzRjO/npZh6rzTYe0e2k9bu7PlRQdYbP146f5XyCg7f+PHSxatRGBJ+nlbYiIvdPhV3R/9Mmqew59dL569S/tJFHzAiJ00rCs5CM5gKzimMyE4EynelJP3q2gKaGpa1jOOYqum7uOUZsgNkrV6NMj5ROYywLjN2JTcJVG8SnsI6MJLCETrQdi7ryETRJIO/Z209Ht5fe5oVoZXXJkeRRfG9DG290XCPTmbYO7EePlhTV2VFINiSJbCv5mYNJch9lric4PwiOjLa4BdNCFEp7tExFWWwQvA4XaWcOxBG/S/juD8k32OfZruYs2mtI6FMALvDMXM3u91d6/+HR7dTgKuschrF3rzD0C8CRCnbdeTXsSCZPP+qfzYI2GNZfou7v2vObVq+EFeGxbeCgXEKV9V5FusPTifoDGx6GfxTySIgA17i/vLF7+eh/0fhd8LJ2j+CCjn1S0yVS75SrXzb63M2mw4LT92pnpO5X4ybSP6cTiqLLYQ5xwdTzTMr5GmIZwIF8EkjmwBfAD9ti4uZPMbngJLXR/2svWLZV/nDmQKCQt59AZ1vGGKYh5go9/KvH8sROAz/TbLbRYyibh3jkhRgEzIIP+LO5il1eyLMzZwRQgVsQ2Ohqn4M/3oMvgWugj5uUnWhprtIYEHgzlpwFRGQE/3Wy4hYTHTgW5OqJ2qd8CIP4nhe8z8+PbmZbJ4bc2dXr5u4PlSSUwLBioKwIFdLYhA5143UxpizOHiEHR5/dEGlMGYk+d0HcoEvA+2f6xC3++W9LYL1WAbbNqE06/r+ZkcTiKohAMcwFidZJe95BQLY14FRF12AK+HbMbWMUgnDSXTbx6YUrXjDe4zojfF3t1zue9fF3La5I9z4w8RronLTGe5UXXp2WDaq/BZwiV0BzkYvEKFv5n3qrUNLebrjeZ7UjKMuewVEGBvRPpwrYRCN7g1WUZAFD1cBs9ioLHBUiR2FRUPd4lqh7g73GWSzof8Zj7FlBVEnNIYNysN72siVv/yTRJDYSWyyw/HsxE+6psDXyxumoz3vCnoStWgowCffD13sVtjKf+QczjOBFwizNGCc3nXkFmiNe7Kre7OPyHiSZ8O1KXlUpcLx7NWqLvGpLpnN+cm6AQ15h69C5yU3SjhL/M5AEyPuFiKYXfRIoYER53RKtnHdWodecQlV9M96NWUHFlpxCJ8uAq0iTj6cZxHN5Xa0L82wgYNzIEy3gUVeqDLZAsPandnKn6JIt/HBdFY/Rs9BBofQZ2MzxXw6rnfBOIxVkS9J+cRUrri75D0cgoOiCTfETaZ4Q5wVbb+dJ9Hb5dmlj9ulnBtymemoe7vuxZrc5g/cOI+N/PINhdT9IZ30p/a5vTZXrPGotZkgWRn1Xrt8GIJb7uR05lwaup0cq25d3+5kc/qkLvhmsHC9jVcoCfWcdzdVU5YhDnIGZKsNfTA4NG+nmoL2dtB0d1KkDsDoUiYxi/V8/7U0JDME3Coxn9r8Z8o/3rInvqvjj9428pg6XNH/8GWT8qS7Y6KkDmWkPEORozIneoX9XdQokQFhgfwoRpgJnT7rebXwwoVTlX+bPLgNIWFu5NML4xnhxTrwwSU32pJOWYY2t5i0eaQXrbBahDdV+cUNwRElWhhZIXdpo2lXuR6u2Ryckgf9IQb0Kx1N8+6vhuWwSzXSWURXthI5j4YnYqIvHs4Gm3qhVIZPtX0eau0k4bSKEGOKQrB5NsZEBNZW1EN2gBJ1McvzT46KA7Y5CGuh1OkxJnwNkVMWl4xojuN8fnRyotQzKDtOc5dW6fVcECR6Kt/g70jU+B4Fq/Bei1YpaWAUCSb+5Akl/RF7DfKqK2t+V/bDnPBoQitqktrR2ulfprpNcr0XIvJgq9LrmYUi2ooEp/dA0jVhQFtgMBNf/AEDPa0ZK54hntbCFr2fqMExrvTTe8uK5oLu2OOpRPoY9VBjEiX2fMUBZrdqxG/cuN0uZDt9b+ar3YTdrlkXRbCZ52Cm3DOGATeE2XZNGGtNeq+TthvMHOuo1a4H02tDABzIuOr8dcUNqT7mE1PLRlLAYdp1yMKR7Zos0TQbmR4RxGPjNromJLD7LByhuHgx5Cjc9uKJKhxefmPRDm+1AkNkBArtQHuE4db8Udp3jaTZEKuxEBK4DF1K/zgAMTgAmjqxSXEzWstS40AIEonRfMfpbNppJSlyxhreCrD+L5ZFecnm3ZJwl6gvIRgLbkRme5b8nDnYI5dbJtAtg1vpPO1M+POPM3tu4DQfWS2Ech3XPOU/hRCMTT6DTR+Sz8BY1wd0SOy+CyXko2H9DMf2SK8bmX7JXeT8jCmXtaug+1T4YMHIFZ8bG3+OPY7ebmIWR0zW8u0dE/pl+Htk/qV5voZA61R20zBqkWZ9xEhLSbnZcPqUopLoycop/emaGUL5pDzZ2pJ2QH81l/IvV+MJZwvnoiioEfv/OUS3o5BTsfS2h1SmqzPR8i9sqWIbUXY8jM+Hol1aQovK8LhR1Kj1ofYf4Q6a8HNx/wEZLp6HGHQGotxWwv5jE8FNPlBuAGub+RCZ8F67KwlSwH9KWJV4uubjXQeucMkMWOdY8biF5TDmYPSRwQ2ZhZTxTuYBy7NsJiB3ublIWN2k2WAhZSXnD0NqB5iEeU4Wjyoerh85e0kB0igS04HWobjTzdT5VUfDsA90I0l4G4+M7U4PKMnyA3GWPzCbV3K47sz1A0yo5vz5crxWp6KVSduFSeTrcjObweTiPUWK1LbT2luzGM3kj++PpI9FXxiUdn0V19rStzygsjooMDM3njUssuxY92hKGjVlCyOLJzGn1//w4kk0mwMTL8+nMeqVqryMRtswRo08KW7A4k4j1ei0arYI8fdcoy4jiAeic61Pem8L+tCIbbpyB1nKUE7XuI8m0367U0c9FXwYGiBfRT9dEd8RXW2l7qSFJeIlmfDn2BRlOoFCUAsGk0Lu5Jb5+e5mKFfMphuWbbhfkdVOLhdav/dU3CzjIHetPDtX0p06k7dColYjYvMUbNG11XAS+lU6q2aGnxsaR0Jf5ZXtj3s8aGVwcSuT2xb7b33l0B3a0BtvGZ0gBLI6S+Y7Aw7cq9t4BFvhqkMFDc5+C8wALQZrTqnjbF3CTd/P6Iwl/ITA22YL9dsZLpwf/LzSj+dpX9lyNj36iQCSBXFJ54aUEoj4ddgVSq7RlvekgjQ+dhVkiRap6vbIXPDScUSoswKimUc/FaixHfLCjIM4CNIMJuxXblUoVxeGzJIIekpElsNoZoQuJVjY2NSJEEPPvyszgkQA9FGHZhEY3n2JOOOJXaheNE3BaVIKyTA57Ti25ul8s2PxYIBeMcEvYn+hmY93HMh+KQEARD3waspspwKNF2sFL+65qDkZwSz8pgfMnmzQaNQ4eUi+BLyP+sF35DNeKAzJo5Q6DiximhlWQssPNXNKxdjwXgYc3iQAHhbFTNiwvCQaN7mSlFhBG4bGU6R2btMR8lT4nQxypPMpxjdS29BG4u1tvh9QT6P7EbuFlPIMXsIyBKxaP/GyEL5wP5Pj8bqZmmT8VuooQsMHLZ053rCPyj/t2B+nAvGCDWkLmBa+o7k7pFjk9EQ9F0FNeMBNFoYRoxdTYmcEC69UinnzXnGiEc8z6eGFJFzIqvTqu+m7piC/MGNyr45GBfBiDUjvyDFfXqQxsHcd2IQZsrnp7JJiGPGvUipce54lJTSeLPGSYs919OxGj/WHuPb8H3ilk78aMtrp799MXnv2Xs+q6e0ae0Pee+35jJHTr3tx5q/neF+ru+9VGzVbzDfs+NVp1AoKPHhGRk7v1opUgLxnQFj1c199yjW23fj56G4kjnZSngFh7s7G/1fb8p7dAxe0SIwwod6iuqecsU2FFqYC3t6rmCez3FkoOuTziQ/p0nigYPMViASxvpoOiO2EGfaDdtKcmr2LmG/aUubDbdEBZDfVzxz7qnrLXWo5qK24PJDBqAJjlWueenyLxnxVdL1KocKsnH+pee/26o1e+X+Mx+L26g0w28kF87IqfzouquMQj1jahMslNsW8R7xx8posOV7UYsk4ww9lUFOk5a7i+EERwvCxC9PPOV+qLfXK2PmJjm/3FTcF01lR4qsRpMS3hdLl5wDbJTQuaOf1HkHnyljWoufQJnBKyyeRy1XUGiLpPhoPVDp8GBHqFVsHkyk3RdvFaC/cv5Ni70e4lGA6HMdDx3mJR1wQPfRKn5D9EIS8R2+/DWjrMdGeBCm8iD0kHm9D6u+tMxdwlKD07jMl8fmvNIttgj5foH5h749AUB+lLBZ/jI5w/96+iGTCNBr2I2EW2cXDhVyrHeqMiRFqDIQmHPewa3C0rMxVZlsXW1jgwpOuwyNJKaHY5xuQ775UIBWjb8RluCKOt8+iyfXZw1Ez1RRsZCzyP6pAXYFqkHTFJhjK7+LCGTfmZdmU1yKx4sCAEztWfYANlATHmU81xcjIess/epzsAOrzrhUPSWc+VOLgJnZE84xs5DYoDUlrCF6uTUEqJPgLeLiJxLZqnrPl1UVV2en0smH3RyIOCuN5FrPnuLD1/CUVctKvN60WCJ4owTvPef9x0Kz7poHT6GD5uNlCmDqvZVS6y+kyjsuy7lFpkQsDTV5Hnq0HjTsvGb/IvQIRCbaw+bIelWc9YNI9hRgly87mb08SyCPdZDXZSJ7PaFLn4GLIa6yG7UY7qKWbuFFF+Gyc3NJOIAii2FstZYNdQT4/74zLmpX5fIqBRCt0XhmlbVxe+xnqeXHAvPxNvqOZcXDcDeZDigOsbRt/Mcud/i6NS1yHil8p8pT/hQxeyaQ0hJCmgkrdvoS8tTNI6kSPIoOihIneQ7aVlBMknVi93Zt56CQ9Pfyj1y0Gm+QywzbqZ5ybz32/tVMzh9dtZ1f8h7wl68ld8xED/rBMwg+A09KhfZf8vjjMwoD3b0q6eGTxJnKtpythIk6X4ETElAxER6NuVje6dhv5l6fmu00HK6Qj7XtNwiQVFPy2wAtcoHN0ZsDjYRuJJIZ1826EwOyHrM8DTt5fJA2wZRGYIPLMjthR7q1JDOiaDGhuXwPXdZqLEu2fjYI6Z2YoDtcvFyCAD0oooVUYHwHPt3NWhaaevBLwVn1bqHdJWLe41H9d6H0fbBoriM3XmsR7A5VpCotg71CZT+orcX0eRGMhDUXJU0qc1iSB8H3LYl0zrSdByApE6o9nPZVs/n7Eo8XOBcQTFZAYNSkCkj0XSbG58m/85JSoavw8S3+sihbl/POsORp3gSfsN/1R6tYIGhL9Uiz2RARTJMz898JV3kc0YV5/7cD4mMCvT4Q3V314yo43ug6afP8m1BDtinGT7oxsa9DQ5c1hr1rZlJ+242T7A/xMq/l3xNWMHigIZJy4Y+Mu6GePrM8ueuDr7pIEEA7vE4rAYxNFmKvhSC1gCdCHltJAykFxxFNfavRe4LV3o0p0WXJPOoXmoNXbccTbACUaatM/pRw/tF+aDPs1WbxhEvh054+SG1l0lk0RSxsA+/dWT5taCrXAVa1IjPGJnNz22XWr4DMyz8MgMUYN2RcUNtMs44KjPZkSzt9izBeYfzERYz5ho5Qj48KV5C3uWDz3YyTjKRN3EvfaGirUcONbB1GunzLTYTsm1j19e97to13zH/myGh/xZJKeBogYamv84UGVthWaDjBzcz4DBbVPOatpZsq9FTCa/1ETwkOyLXvetP/+qX1z1ZYMwdBCWDvep/nEzu+SmjmWYbGtqhtcD0vf5zZS2nLgMTqWpKugX7Wso+FEmR5Of5OvdTbmSYCyIwg5EUhEkSs829espVXnrqfwftWNtdUfYY8W2392h6qv/vzpTt3o9MW/PtMs+7f7b/dyPG4+R4T+c7I2DNMXW1W2+Cs6bVrbpE34lkvkG3h1zDxNm1aQfGea+eyuolRvw29P0ZF+JwnJ14XHu49T4iyHUE952q4m8F9MXh9T5kG+uI4JjbxVaKsXDaD+9iC0PeETyBLVfusoWW68yeGdINGxeFaL80Kl8xi7WyJZWNNoquFg7uX+Q3OupSpg8s8EJkKSDf9aOGPy6bl1/NczAd27svq/ajijv931AdXBMxduDpc4A3AH7yp4NaYU0PNpIgIw7ZKexcs0JJaYJqkwCi+mPeSwlcRbBeFoEgTjIPP90LDwewKP8k/kMzUHeVBJvNfApTo4HccB9C73kUCJIsBl6qfgg5Y+arGO6EmrjX6lWB3r9BkbPmJUZisl/iGfnhYfFpcO1ope+cBNH35ynjJNosyOHA4yHypIiZVTHHIon8eI4/8CPp3NB+4d0ZusvhXNmKVKIpS/J+A931+9r0LWuxNz3lQkvZCx0iyXhDgRzV8FCnFpajDEkWXo5HNycpk4rYAtAWwCdBB12frNRnot9nsHMFo64TK8ioPkRJaRynzvvLQWSkKzo5tcmaLhkfCliRQuMIAXkfhJnOMXcs+o3tTY7ugW0EXvDJB+LOBOja+FhyyEC221PTWxd8kmTY6IxmQWfmIaaM86mfBXC+xA22DTNSMXXChyHaYDaxMZ7jJth6/JuBGx2cMWu78GMQEhOBxjFUx99Px92y5yJjCBNlvv5SvMhVL1GsaQAmKgJSLJ4BgUf7+0eKO8D2pMXL8R2TwnxIAeYotbUW/0Fr7MzElMVZ0nGKwbIQD3RAjEDTaF/TsQiluQeyKYkmZ434xY83vuMTYo/Dz/hvT9cZVrH/AXDIeOxaVN7FignEaYA6rtkU6Us6kJgqYj6HI28Qahm+xfIqyjPoi3K1On3hFBUo4E5AYHX7SdcJTbVOjx/No8r0GwYH30y8Z8VQkQICNGEzMXoP8YAUVOABBd5lvHRWoe5qx6+WNaLTskwFhaMtbGXrlNx0Gh+Tu/HN+7B5y6rarPe5sFmIeOQvu5WwhpYmUKy2phh7/Y5sMJNhweq3NIYHt9VeoD//B8ohYC+BM+WKBjb13TIY90+0rwPAvPhWilcsfx7l5kcPxKAsQqCGGkr02XDBp4HON1SXV5J0o69LRR54VYz9kD91R+M/XWguQkLlsEq6eRevR4dKAtQ17Ue3k+/81whADOnYdDabUqreValUVtrGOb0bhZ2zkY6zrmLx9r3Z20r/PZ9oYe3zl2zhnerM4VrgVUXyaBGJT4kTCD054UEzhEoaXhdvhIcyGUzuTZ0oBoVVml/Kq6pA93ojPw1Gby8OL1emCO1iXobrYGuyJJ0XO1A/odZe3SgrZU13hnygK7GET8nhl37e4laUepr4UUW99MSBZbhqBNmHRwXH7t4jd1++8k/V5HK3n+GCKM+ZPxIB6Mhe90UTdNQyIVPj+QyIiN0PQzLcIiNGiE3m3yNnRl/5FLGM7Z8Yb8ljt5LzAttwNxzm4niIkQIaBOU0GTd4pjFlWtCWI071yqQ1MTLa6xU2pXu8JUul0RJcwzNxHdg6U+slxvZa6iTqszoUgcn02x/j7jC77WpO5cE7yEzrdZMxL50fQKikxi40jvRuTLJsWPPlLzqt81nVRTy5OIc64tIz5rnw7RioA76KC4bMI3gDI6VQ+pvYJF1XIVC4zJtwTcwHKu4qFO40NTXGC6WXNcKLrg5Fv7X3Vm5NXCrlUVJHz/g1fVs9obT2T6MqGVPJPOyiXCUQl16uuI+Tg9NQvEfpwAzuZi0jsPeggH0byAPfGBfjEwJnw0i9nVlwMULOHNU8qLyR35KY9GTTEOH2sU0gdllrNMnSKeWUut5BQgr9ISyNgN41T5jmf5mkrV1+hILku8R7XxECesdadj+ay1nlDMfnmPrMZuD3sWzxOek6pQBlHReigDuFpEo/HumQi5luyRULFIJhIUWmIYw9SaGZKJhT16gQjE9RJLauJnwckAo6EJA6WiPqthnfaXH2x2CLUTFuy4ysax0LSnKngBf9wBSWpnWuW4xpZzTOb0bE8daEKYnk2gBe1BUguyEmi/hWWANWaaz0ItAs+4HYM4MjGOqLhPoOCPL42qBjqQtKM+R3b5lZSK7G3UgYNJ29ZzkRP3akY1aYg5tu5il7vhzHkS1cgVz7WhnLJ2tni2DytJQ8gBRQNEgF05i4VjSCd7eC3gexNZj4vSLBYHTwKK+ZrYmxWaxe4D6bWt2aKeZpNdukR+u6thpWJ477bH4tLl12RdmiF9vdeAylJBZRxbo08QPPg9lz5LUSee0lnsyChK8WOp0KTTufpl4WDrkU0PLRiVTrHI8IEO7jdIwLmIIMiclQQyweQiQ3LAGCesNJ3b7+UL8aUX5rSmDP8Ca3YPl9nYqxQh/00tLdJNjiryQ1PGZGf3s4HDux1meqHSbXR3wjPLtAyb5KtzV/FH93+SnhmtBSCk5X4LFsfYlPMPcVQsxhywe+ZMYQCNBfCk3GeL/o6QkpUA+6tY/VJqeB2qDUKUJWjZXIxlgujV7HKhEZ9pUQNhD5rxuylFc02iYU4NfL5dVnSRiCBm1zGDi7MWSsEcVhkhQgXkHUxVoMV2BFYMsDCLWpKShb1+qWCS/GqYmy7cOjtR4mUqFQK2hh40fZh4LcgQX2lLqHEDuDL2QD/BsaJNws0qnvdd0fjb/Z+2q0lA6e7G/1mwZt4aEyYIvArTHMFtjmF/IWMj62ABWYii48o/0SdHKd/drZsJkUiL1TPwpv6qypIIz/+zbfMXssNt+lWQenWJh+jg0oRGGLFeqFDZNWTcnDXehsoSuoGwjyocXS+OaApil4DpOGnmJ+ElCj9LWZ4xlAmtEFTvDg5fYrhsVvc0dI4E8HO9Rz0FnZ6J9bAZ4sMAHQxWV0+WBouM3FZgUVggD0DzVJBZg6hVi8r0JPDIYQFSJyV/IOTQC48+9y2LBVIdpoiZCb7KV0aSvT9/SLLHWFU+eNBY1NXDaMzJSXMrnc4vWmczQ1qc8vU3YEBnhcksee7iDDQmGK6Y7I9VMpj7+bS22Pi3s7+OuhoMaRkJRcunaekTmFI0ak0WXPWcNLLwxgZfY0g7TlUyKeaBvOBnoUNjG2IqMKAvMA2Di44RdldKnsHCr6rSkBOuG/4jOdYrmF2AzkHcnZMVzIw6YdvjBcg7WUfYhrUmCHaPiLBK2fiI3UD8PhEkfTPrrRn79xUunNgksjaWt3PHJc5qihE5QjE4SRyxJPvO/N4agwDNVe5slDDhlWZRJjWcAQB88MydMa0N6nzTn5KiaF0NqZbIHG4LJC8Uj9dqUA7c09n64vCILpRUXIezJ4ZD66SKsGgGbRZUGulAM0vQsUqPqrdmayWdgPhAbhmzcH0GN2Cmpbxbd5RCmVOrAEgyTwtkMnca5XNnhRH0Ns3iezODakYLW3iJkpYi/6Pp9GFD7tLjHPjmbvPkdEyIr0uNX4c0B2SoRBMF9ac6C20ZELe0VhNUhB4SHPxKy0aLa+uMEtYIqtWInW2q/+LJxp2WJ6ghW+PlLs6canXkpTRe/RH7salGkvDWrN3lJidl6hEBsqoXv++3uruRYqBjVSoVrLem4CiqslXdXlPRRk8XtIM4TxqoNU0gams8g+YY11QYXyE7uT/NfUy7GBFQggoT0E8OSNQBIdfC0F6kaCpdCcD3DA0Ydu+wD65T57jOV8vG0hr36RkuxXk2hc9sI9ShrJBl2CtVtDJq+DVQCEEltj7dItbas3/Jo+ZjHHlQph6ZdHbdTjpokTNjPiU7ppcalnDeSIuEEDiJpPwjnlFS/khPCa9E2DU5Uf6dkx4d77umYYGMb2CaKknf2Y/Sh/X/o5QA7hJI6XfXhYuYRKMlPm/badZMrUs9qqkyhAGOgU+wgHNkfNs6IVowq8VpkCuWE62Ee5yn6Q6YUg6Aqo3GaRxKq/rDAJ4rO6OQS53nNMFBoQS/Cl+rG6r2qLvCVufbCRfBkk+4yul7ABLGYLAz0dbNzcYC2RiFBoQvvCbClPqpHFYpUKsCKb5HaXUz4Y7qVXlgjvVR6qhqS63RdOQLD9QfuooPRLjtAvfmtR7zk9LepKFEAkCPgvAyys0mMqdVfex1o/NWTaLIsbamMNye4XM8OmD5xNa40VKLEOqJeirG9WyqPAVkA0ZoBQIdSutXShrKjHrs5jZ4GygARFDazscdEac8PXs7NkdAETmlx7wKK47rcYohZmoqegqq+9xXv1i6dynZhRXdjUCxPtSHMdJfITt+mNu2fPo9f+yUFehzak7h6ryUxP+E8OKNQpo6eYXDNpI+k4AUhRMJWg54H6G50IwD2ETMjxAERQcLIu7dk6bYRnddR4+6vgiEsT0VSQnhIES7V4b6UTdRlxIOgst1QftbnHPW+ggCAzW1qWuthS6yOgv8O02dUtMjwc0gKKJZCsmwgU6Uoitrs6luWNQk9UrDOuc6+5uoYwL/FISHyEYFq9Sjuwq0miwWCmxGr7oOC8IRMlVhpNuF/lrDCT6uPkz4a0AMkZ9sq812jYibemACSk/Din2WRqujZdVtNwqxP3NJl5wtNMc8LpgbMbd+4h0PDTLNBl9djnS/rGTXN2d9Pw0GO+0je8QT/zq2jHAiZ/YEPXGhWSoYI0BCoCvt1FfqL3fhpgGMMAT63Y7/KxOQS7z3ZD2GNp8HRz/p76L6Hb0ysBmGCbbBo0bay6QwZVc5F8gTz4LtwJUUNYHMwwXYTrGDmBSg9WS5+dffwWfN4ulXv5jfdlh8Bvp0mnU+M5Egcm6gUiIrkGwM/hgBwdh30CpjAQOpO5oQNMAMzuOJ6qbaZcFV1oc6WqwFFL8W0Uj3LNFa7otDOtydO/zLLBXQ7a6DwZ5lesU0uBJokqzBynrpXi5QCobgg5PG6ZWgLtbyb8/ouSCDNfWLS+MwYTtTOvBkOKp1VJlmVVGXIBG+XiIl5Wp+fPh1NAot35sc4ggkvWoRFR8Kem3yDhCVPwohHbK/sGXqdkk0tk4kXws+WEJPfpVepNNCPlim1S02D4jw6n26C/mQExdfG0I7mshC2ArDBKCEdNFHrZhd7IxDNCjCgO332ai5WqolscQLV94S2c++AABkWV5rxssuLnsqK+hTaGtEdhb/DYm7zICq16Zsf/FTAcExS6IEFE27UnmDnCIA1rZfuIcqidpq2W47Pm4ICDANHWDbSqaUTBJxRDxPkqs/awJPjW3vBphgLgeGofmNPqsTCb5crLkddE4tPiwARWn7eNckrrUGnAjzKt+ZNDLU+2NUZT60dfKAzOUT//AAtswD1ANB1n86adeC1CFocCBYmp7fLXrWHfP1Ps61mmikol4hu/G2cxXAVaBQ9GkHUPOnKtcmEStlCFPJvUUDLu54VKfOniYz0J7rZmWxtIEKYP9wQm+bV04C+9LRi9GHvBInl4N2PlIgYqfXKLVchAPYr4yPqSDWA7gAlOwKKteJdnIDGYL1k/QkB7IBDQD9HXSaLrNmK99qgJhmOAU+P0C7FvxyEqi/1+Att9n51QNjSnIEG3AVJCqDPJCTlFAbkOPt5RBtPpz9HBi7QvZ0GoE9kH0d5JJFQQOrO1Ao/w1kP2SSo9sYphssdE/ykSSiV7bbPdFeHlDvzzOvZ5RJ1XoEvHb33LXyVrMLMq9YYBJbCHX+OvxNdDCL7y/TBLQ6+Z7qiRiwdVESWJlwxX6xBiGjCiazVHYoJkxsWyvQpgC0+lQsDr7P/SQ5poADwDXXwClmI+hz1JguJmRClHtiJKfim3M5nDAJEy//A5MxCeDMTPhbJQuTOQGlFxa3CiCC+rZi+GRoHwSsNUzLf8aXJrHnMsqn+gTiG0u1aXfdWix4iLyXrFthTC7044X8nyvU1IvHcZI+YgiZnpIhhbqir8U5ZUyxdTUjIHJxNZ2fEPsZ2dOAj2kXmlSoOid8q+yD1fUopBExrh40fEXtz48Sa5TbGdjqSp+55EgVDURgUVSauNj5xP1Z/OZaxA5Z5KSzZpXu4FoFpusTAIqLRK70hj7kRInMfKBp4M6FPboF7fQ4n8WO0R2hlD2VlJ7JcGrmVhPDFlyh1DHsLOIkN+MNCRQ5Yv57r1/bEBQMzYjGCW+idpbwIlzOYZqr8Oeob9WgihRCjtQxEQ9utUldPviHBeM07Ufqj8WXMIi2Wk0TwyKSNDROjGEr/Aw6jXY4BKRpCjYMVmyG1WahvlrIwdPftJtmwN1azTnN9U1bae3uu165wlEt79iEheMz1ZQ8IOMNEGYt6jYdARogy5F0PUPyXwvqOeR+vpVGCkPZgWp2kWnVvPe4yzHdDZWNFAEWPk4tf5wwyvdwojCIItor+4lc0xXitNTRk+r+KXHVUGxFp2bpFBua1BnscQLVGu8sgAL/HGKOw0Xcbbp7zpyeOGY9mT5woiI2OAayRxXUpUfx+1hmzOfMilxFE7361BSLFslPMqKePWo2HeM5jJKKDxnu9jGxupt2FDA0WwOEsRKtSUOv+5ngmWm/mLsGHiyw6/bhe1hDFmrSZvL8+Q50ffQ1S/i8DIZ79DNF29ndMMZp9NhxEPuHcF9H5ySQLVmivHIaxZ6SJpMvcMPiewXFaesqvc+GHq0uZDVIP9JYw+hozLkcwL2VM8sM3DIC76sDDaD5DCdlyzniA3ulP2wbV8uG24FOpeae5bdfnLcn0QeyNb8LtL9R5FyyI5dnM5LAKb3WouRTQ9EpXcovaSulYRyLbdIFQDupMMvxjwVwe7EaaUBuzB/O8cqsrPsmLxupEU9NWjxAhMppoj5Sc2uelmkNB6CEs02KrJTnjnQVXyZo7OnLA7QgjoqwQ506lYIvebty4dfm5Xkii0DO1dPZ5tJ2rF6hATCh4uwTYcNSkSo7uCykrvHImtDDIyiPlRe2NEZcImyeCkGKwDMP+lAcCdlK0Ov5JttB3IaR1+VSxReaeQYqO++HQL/G6wIyLchMqj+RpHqakulE+cDXyhtVlqwUtaCv1TT//Yzuhml/w0HnT/gX+IundKsWwu+TQPgy9Uo8yolTuMw1t42a8hp9SoPeVcNnsmTJQlhfja6K5mWXTAf4SS6hQm1Ulu1qJpYTy1UV60D6y8vDyJKOdHg5ecs2eQ9KkWJBPjbzHAXdb47oLJWCfQI2Qd1hbXueVarS9PoCRbkcJ2l3cKkqB9lFfG11MAlsiQrhiqO2Tjaq+WWmllVr9jpyrtH00y8ZsJsfWZJE00CHL89pJ0G1RJz+6Xn+g6R5gjkgAiBKkg3AyVFCr4CoqwAY7uk2+7nUai22aphLvITmSr7p4rGpkUBAxn2YRvGIeLBUJIm25sfttDRsd8qAmFyOZDMs/ygEoGENMjq5GA8/EGwNUi8tVg5aEOiEAD8QMNiuP+rrhIC60jOv8N5LNmxPndDNQ4bWZg9UaQfJ5ycgyNYQjsU0/bHJM+kz2yWxyUERE2KP1ucz+pgHu/s4oQ32XcT+mv9v93FyGtiTLywvqi+so4UwosI7FLZqbqENMdSZhdhefXQRxwpS8+EvNhljwGUgVviVHAFMO3x/xk2zD046JEpeMFTXoxDLIwIiK8wjW8hV5iRUh1WurKRDXkr9vVUTqPfWHJxBRDs4IvAbMAtYpDJjKQMjZetFvViFWunDBdhxPWSEGDTKUKJII6NpjhDghdxdE34XxrMrMnN8DmSZGZkYfLkVYyw30zSAjAzLDJhrwNJQhIav33XXqbmeMxNqAiHyA3Z4UNYBgZcTZAn34JcmOF4LrNIHRcGAHOfSTNf4DBokhdgSFSEukXFquC7EnY/8Lh6md32tXkz0E+5ySOa5AQmBQPKpCPkPxDAD5TbQJady6RCFAIM1P1/MIyZNbRSTdeXDRKBqVPAC3kR2VW4A1YEJVNlAm4UvNe5PaTjJSSGHzcuSTnaaDD6wdYBG2S9v4t2olorHk2w0w6D1DUq4BcI/s+RkKhKOFCIzB4ToWHAj9eX9IKyByEWF6dZCpExXSLYhU9Uod7kvog+Si2ZXgynaxwjEvuvYHH4Hat4G++qtt0zBtDI1Gp0q99Yh2oIDJZoP23LWYIeUcu0ZprbHygIYdXVJPqOAUkCGGKh64V5C2thetd8am2M93MsuRSnbIzx9JHR4LLBm/flfsSVe6wiLhK8EY8liZgebT45k+5M461hTuYH9Q6EL30mFBdiC4JayzKjpgvOxegyPqnnpCcapXNWOrruJMr/U5fGzMmm+nVd1MT7n67r4tpXK79+8Gv3K8XaMFyvSsBXjFSxBgWZAerhbAP7S/sgMhcGZQn/MTo00XRB0EYjps/ky84x0q64EIA+6lAK/TBAICCRK47qPQgRdFzgQH8MSTcKTLN+aFTDzTapkgtVfANKvl3zqtTevEn6vVknhPKhN0TqMPdzgr5vqiZYDs6jg4VcvfV9VYuWnagICJoOfKL2bdpxczyzKzJnVQUZNIkAGgkzprsqR1YYT0CC+Zuo4paN5QSVKNqCDW2QuhKSyP0w1Q194R0maxXzpoOmueIofBtUNMQrm1A5cNsOwZjHbrQOhRma+J/yX56NV3+Kewvdg0jLpJZU36J6bq1mlI4M8rjXKkrxE+qtmCtclr/QsSKBpK8uojdNMxW0XCTK0K3YFs5PQ/i1LPZeQi2G5HDNoPnrS7iBEE4lZ2FS/hLQttg5JM0DajDm5qvJxylOf0pQNjSbjYfzb/PTY+BX4r68xYMN/jzPevCe6ubJQZz3CJwc7kDrihHgZd4TDuPqvAUW1hU/atSXN7ecGtDfBtdVjDxw85UIQqG0YBYOoN9JXwYe/OlG3IpFknKC8Lmy4Lulj2R6xzNhKG5ZZJ/UJ+RNwWW0dtLOs8clUSn9VDWubrDIjp7v+8AHGt6FJoymGjG7GdP1Vf2G82kl++JphxNiPCLhADQXpKSo3RdDt1R23Cbdw5qXNHRdwIIWb7iM8eXj7epLqR+UvL8t/VtYmACRec/jYwQbTsOcH17nU5T/IBW7YOafp0R1aoQVOGpr1N1DWq1EBlc7tkCTd/IbR1c9EkKDDgJDZncArBAJnPQiEsUdvqBDlU/b7xClnQfi7LYawvV2d3272iROfVp+kduaOX7Hykg7uKip7jgRc0UbcWyUnZJezElL7mpdTjWxoq9WgVw8rgxOkJm07YQzDbEoLdNuoDNk0XtdSdspCEaeEl2JtrVUz2ySn9+Mqp/7Rtia5KjTGjrhR0ARfK8yu7diLiBK5KhWI4GkD+Zgt4ABzn7zHgeHB1iDfj34sAPZ6Gh+IJUU+3Rx1KAwBFYKZmv4++64aHctCYBgrL1X9kBhQEYFo/eAfhLVDH5q1cz4yYhva5FvgyNk1U0VkWji3RYgJWos4q86TmIdGnyUCtKb9NENYIgLVrAc5G3dJOBhVha8Qj2E7eTqXqdW43VRH3SI/juByulfUh8dV+TEi0VxFmSsJ53LgH8XQQSE0cj3/dBOAFp6bNiyMolLgyh7im4CsQFOlMV5t3se8tPYAM9UQhCXHOEuEpqgCgmaUi4Cz5I/4MGuNYLKbvXTo3L+ZzgS4a9HqRE3czgb50mfel5rnZdZAXecKG386M4HzUlkgEDNGme2JdCFjgfZ4u8KA9nsFRB2M8Rgc+pxFuk0e3qGSJS5x1+MM3m+L1V3igUVE+jtTss6m3v7lfTzk/pQsC4j00jxB5ArzjWJVEgnZiNIw0OPIfXDiqCA9hxNRsOj1oZ6vOrFxUYVS7FyK23obO0odoraGVhUYydVUIQkomi0u/EsTlNAZCwEaL2Mmgygb0a78p9Gth+JxbOmoggyvyN5rVy059g9AD6BoIK/0UT5e2CpqKITfSeKNw2pAMCdpwlwRP+O0zQVu26+c4CpMfn8nqTG8PeqLCxp28z2Gih+SZXTL9RSSL+UqDhNf2UjVhPEbPKaLNzg/W3lohNay5zRx3zerbftz6JEzr9SqBV3xnY5Hzh3CCzs972FS95RGo50ChxXmih66XLcM25BXU+x4Xvb/woWTQhjoQMp9499uDUylIx2oXl04/jcCV9qpkHLCG5/WA5syYP7vDhILGDE2kDoYWcW2u67MSaXCD5Vku6GgrULZWiMaaS6+CWrJAN0VqH0mkchGh1jOJYNFNe/JcElHnOUe6tZgRubhsVhzWbiLZWiAQO/Jtnt7RzZKd153Z6rdCkhrV3aARTDfhyrHzSeHjg5orthP+B6gNlooS/Bk3eFOY5u3qBd9Z3waOZ+Nw+UG7ubTQvX5F2HRjlyM89Qkk6ZbVSLJFUrmVCI3IyRhVR/upE29HWNmpdu97oUO0v82IKhR6r7lWeBa/S9lrw3zcb0S72pSrtPfNRYBjQQGblUEz3DPwVBixc33vXveFh9qzAq0jaiL+zRZD7u/MqBfXvcrFX7IWhg4NsZAPYQ7UdZlRe3np0JhOuvQ0px8C7kqa9FDsjmKauUTMxFuWM2UVy51HZtXxCFJpmUxiLBpJyyFlCxg3VkCNoSvu5P9C7ldPRRYf8IKle5kNnKmeNHCgJZfHq18Z3yi0S4BN+hYBSGxxt1TMybc4r7deQTd6IKBbGB6j6KbnteaJklkOab6GPeKkM0XVoUt4+AGJvq86uP+D0mTqrUrJPPOdkhc8dUwhs0e0vs2A6xwYXq81Ygn/JaiGFzW0PlhIuzUiHTXIr/0sMb1Tmj+CTWH26KqhiTnylU639vJgc2SLB7xsrCo1/4fRwnEqtEC1V7Nsn0m4Yz1yh0jJM+YJjWF2dPdSDBQTFK4DULewvmSIfx7tBH7+mOwUV0hzvI3H5rP6q5oOs63Zl7hysjXV0LzUTy08+msJxjsmXKe4F5cKeY2LOAmt5Z8LQT+OXO6e1L7s4f+1yJL3h6t3TH74wmmZ88yOhPP7j5eHnqUcyVF+ZNq0QcnphM1u8S0K0640qrLOfCD5m3VW3vKzXZFntPMphKNzqglcskazSPQh3LoY1l2Rk9lHX/ncNj1wpbdiRx2W09xHuktNgO5087LzLXv20GR8TM9gulFhom8h3QnVR3uilh97penl0EKl1/ULrNP1X7CPHN5bAJBNqOXIOHVYfozWlA3HRKej1SaRBkbNDMUxQxb4Ao6FmPIX8+BS0CFUkF1Imer/spQzHEnA+4bNxA9nPxpzlZL/nHiWxQOWnXE9YrX7jfHKxUftX7+Ut+mYE9+JVw7IvIgthktCtrzdKKHZ6xfhj25MJYlpN+VzKNLsqTOWwm19tN9L6tL91QF49M+3fOyyiyDwDsJWcA0rmefqhe7or+sZGj3pXdXDOxpzelMZjd62/lorzQZEynhYwIpK7mroY7GakdBU4iUN6Q7WtXa7B3OD6zRqnHGUk74JVY51xeKv4EMeNqWijJHaC/qi79ryZLE6a225tPIZJ/XdGG79hK53agwNZFSoXRZzcI/e+KalEIo9zThQ/UuowVF51vUDoqqeL6kKnltkt4rkfswRpBpnOJ40p/mxTUHf/j8EKLYFOVTA3/wtldto583b33T1jd77UZMGcPxp9eT6VWq8n6hJq1ONVFmj2Zm4Oof3MLdeBqFdvrDQ0+43mWBRFVxMm2hJw0bhezw8h+Pp5oPlTV+y9asBzXBe1Oz916FWuhAVi2ZpH4acn4TOv+Q67cTFm9OsrJd6Cj31kp+856ifoV+yxzeVc7OLG8Ik5ckr6dB62hEu7OnztEQWy3v23DCwz7jEoOB+qcT0a4r4279RS3J2JVEnvaTbR5DSyZk2N8hLXDGaSg+tyMDJUVUuCPK6Sy0veRg8JnqJhMv++P9tIofaVdvc0KCy23m/dBTkNgvstMoi3ILelVLHcNFTQuncgd91EZ/1Fa8woTQLz5tCjtKtoM1D1rZZeDH3IVDYpHhaF+2lR4UuQ9ul+v/2F5QQTw69Dm06XVavITECdowNHYafP/NrJH+SYgysJiv1n1bg4al1UTe260TT1nU9ZxjmJC7LzQ5UcXHlul9vVCjNjBsiBdRdoSGGFDQKW7CeLffayOaIteQJqocMOFdeTBmYWEnTN1BC+LtKHVvI/AwJdgleu9J4cqWx7r0gwW89/epqz2gtf7AZaZoy1aKyeOExVmnpDqkjskKo8xZa0hZSo/uaSKfdXvBot/xJrBFZ4iQHjwdBiKUZ9xObvy4DNc6s/ACtbOI3SBPPnmlOx89XnzA+XMWKIdkV80TtrmFpef9gPTqbkNo0ORee7QlUPLleh9AD7Zoc8KcMjNGvIkQ2vAP1bogPLQB+p/VbUNhOyGXhnX2hnEjzAjWzt2MGGJO8lQHZgEtEp0/XlTDITnKuBeP6rjRipw0aIU9O3ZwZMiRLHAHk4gCr4JIOwWo2VtvhOOm032TNXEaeLgDh6lGQhZ6zdq9tF5MlnS7e58oKI2KyOum7013uLu8JFReAz2TU6E3fKNuU0qozhPo/y4714BOTdggjfWMWCvuRJkrtiBTfCj4MK7a4QSF6XZ+a6HxoWq6BIAdxWpn1E7tmoG5fEZRTygN031rf+5KTs877mNdX9g0/Ywwuri1qEiBWvtJ+zuD3V7oupdoC1ENPjBlusZ117DTXy7YuNtfdx7MBuBjmB6Ru7snR2VnKotrree3uQGH+NJpTL43zZ0Ls/XL/+ZZFW5Y2kOPkX7fxwagt0z5/MuQL6V2yBRpTaZuR/8nK3anRM/gOSK3qbTdJy08fe+D6dcRLPDxR1TAhZm/CS+Uxj+O/j4OmObLZxoKXA78OKaZQbqtAx+lfVJxNB07Jbt8qMEs1H5BPD2i/mXmRQYz8vAQfjnm+6SxMxNPnrc2PsBXA/EaDXsgoPjj1RJqZeHlkx4zPhv/a5Ow2l3XMBfk7Nei+KLndjGEc0BvvBa+vbDHdhncW17uRUuPkAnnzVnNluNxH1w+kVH9yH6dbX6FjIhzNlyoTVX6qfrtmQzAo5PbcYiyTa/GwJWFWD3Cg6BukrG/mjkBF5culXu8W+0nzcedB+AyuU+cdz+lP2L/b4pmrtuR9dCBsAFHtt+/c4kuVprLI+W0cGn+sw94AmEcISmGZAgY93aBiNEOwmWJ44EZmlrjOHvJCq3mOD2e2ZLAYYXCbJLe5+DKv6hLKol37695sEW35s+0VZG+e/CteH9GEuO/Vd8W3YnzndtyslZdYSHedQeM9ZnnZffntNu0YYH9l8efbOWU3ZZL00CrUJ5gjoQTX04BvzL4sEBRyozZOTdLlLU6BzT8INF2GGiE1NMzlljVdn0gdz6MQ46H9s8fHI/3onUA5tKFquzTI7mLeCjYdGAIebzkcTqW+fQe6E7/lwcYOOxF6H9woqXgPoccGPI5j26JajFWAe2OLiOq6xekLEfinOMWd8tFQAEJqVbOuQ1/lYINMr7s21OS+idGjyL/kXvvFJVBjqVRTbSzZU6N7YcwB+Zp/AXoaVNUWbq6eXThkJeHCi+R4yW/scdUYoOevIDRMfNa1UiTdi1q8GuMyXSgx2ECGXv8KqBIwPseCe6aB28izXLf3HcABUglXIK9JtkSz942idbIkuElWYEXJtgfyA2c8dKoWdEgaZZ7emfjz2ALEiyl/8B/VYI8Gr9GMfpTCqIgt2ryKvdvt/q/A96uyzibes7aGFeRN/vIxafmbLwxCoAkjXD66wxxnAMXqjzDeBWhEKFTKR8xmdOAniiKBSF5xy9Er1DRolGzzlDCeQSoqHKVAWeQTOpC8nlaLUw/T38FaGMskF7YBHBLkjw3yzXnodCOkwrnBQPKbwcEHRSsRpFvlbylIwn91KxyLAp1AHopd+rbfO0J6pMVspfHRSXTQgRvVYGYJGDkNTB6Uodf+FsOLhVBfkSEeOY/6XYcyVvMNBUas8PVpG9uWV0qESwUlsoo5dv4HwWh2AvIpUSQ8ZQdRmfyqXq6qT+27zK+Q3dYP3ZqRHc/UXC8iUJijO+ETlnmhdSRVTZr+cmRp4I8zCAqct2H4WCbeeKhXLCCu6AUShcVGIKU1JGQXM0gA8EuaVm63aBguUy2f+8wkjAPK0AAbtJpcApttNej4maTuBjAVIC3xJ6iEk7kNgGYLGK5eWiQCfPZWGLsMm1qrvZ3Z3rzbW8crTfYA1ri9u26HCFuFt27QDLr17fOZvciCLNLrTrCwgLwcb7vQmosbD7GOMWCIdZhyZe9sMIOubqU9gAalHcBAc1BZ2Exegq0SnJ8Wc+tXo1YQMAZHJ4BJiHZowUdtzRLnoQ/iQzDAWuXWKkuU53Ciu8YGwZQpuzjXL96eSF6D7KGeYVOg06Q1DeYaTl9g25B1AwaFKBRpjO5HBMNQap81sCGImyG29uMg9zKacdp6yI/IQmN2GSLtiutes+HOhXxYTP+U1Q0MbRsmodjuWogE+kEiABIUqkUyXxD/fEeIzMbgjy5mLva4fh3wxzlMUXTMi0ismVKqOCECTRaqi7mCVSrW4vzW/TmjWvYlOzPuZBLjZee/LF2rZ09N4HnwB5/e5VK//I9GSzCojcpHUcEZS14L/Yekhtf7ODa1oQPfd7UMBtw4iNn+/v8qb2yENfiKllxfJQN/UIUG8KeE1MjVGw7KN5HzcYT4aW2HhyB5InUKd1DxQoEjRTdKXs12Ysi6U1OubTHWHxiGRtPgsxmaFFwr20VW1ECYjIcSdfv1ycCFy7NTDm+YQKj2IjE9bCbwKIkokbV0wpx56qwe1t2B/224sqrINIU6IdFYU7usGOcAWuqR79k/2Ny8AEKUjjHiwiaV3FrP8xB+wPkGjexIeUTVQMOW+IWsw5LmiAyJRJHczuPrcPwJSs04t3ryJj7XCNZd7y0cTJBl1eeDKOGTt2+pfsv7T95i3r36c+cqYTH7fPy5JX8ZXq31k3nRyoC8OaKOP3lBGOmzDyLsiT6Eu9M6L7quwqUaXQcui63em8HkD+SikKgYxQmmPrC2S8LaDCbfJbB8npPVRhOY2YT3LzZOkZDJ9RpsEmQA3pUus6YuRijJh3M7w94ETk9TttF9tbeaZleUZQdEbKDjXsZUHgGH6tAaFSo5QSZV7HkchC5L0JZphG5fDeDUa5B3H/scww7L/1q3PKt0QypbwPJvNx7uO3ZiP/4yiT9Nv16ODUf89gBtPaPGFrl72DHllamnLUi/u3ISJn/mAqE4Wj03BZEJUFiBEBvW+X4PBgEEpMkl8XjutDTVG3yso59Zc9K5404YSJO1Q0g8GnVZVFJ00VViQDVs5Up7lAYLN4XvJ6LSXfVbgi/qOt8nxid8pafb+1iJRJRHiJeNyrFtasFoPTWN40XoObpnmzSQWciyztGbZMN+KSjpTs3OdIZxxPZpBotAlL65WAfhhLL/lSogWBCbN6tPpSl4vaY0ib6XMksm0S7NZ4frNh8YTVHJ7gH1+5Gt8bH4lnH3GS9Aw39d1KfHUOq9JDu6vE4zNtgkXM384nUo9Z7AzwC5SOYD9E8e4/d8fPzAVcEUczQ7Sa4eREkMpqpzFVY1WsRRDny1llfZXgqKfIo3v6JgrdEgOMcPVR/1jl4MFBjypiBkDgY+XMQFHOB/SmvUkJBWyuP+zjAiatSKLUtGlPLV5xMspRHGvBBTyUnbflPViGcMGP7oOe1El2phCviWErZCNnUNCk9ubs/+kHbfBD6/Z1nfekrc1Q909ABOxXbYcwX/uv+otPX6rTMwOuL5v+DSZARcA9sibvWqqVm59u/nKH+Wa7Hopw6nLLMLxgJeTJMGIV+snM+/NitTju2kykD+0nmuLNA0llqRQfzk2k/vP1zPwEGx1XOM3jMtYqPXAJPeRa/l/T1ef6xHD5aVCCxparexIr85a4v6/6KQwu+yGlhp2gyn38WBqwmRb0oF0wou1fBIUep4Ai0jE97EkfxjwbJOE2yGg+g9PmE2gGlcIETlRqtkeBlpGJ455GO/B8lp8l7qpYaKfc55KRHql4ZA/QrTGIDVP9LQGu+SpJwqYkdoLJ53FXyorHKWNpyds3cGVv3V2uAGV3HaWPmvWW1xtaAsMTgNWrY33DOZD7RyRRPaovD470AdnHRS834rdc0tFsZ5FUMmNWyTAuNY1gbi5RMGle7bmouKUHXUHDVgUnZ6VvySz2+nu82yTZsZDJ+ui8h7j4J+xsR//1ZiOEtykYNelyV5scEosQtlHFGAzfPJ9kqT+zGD68EuInDO327cmgZE9VPsrApw5rwVCBqxMXu35Ej5CfkhPzBQf4IoK7n6OBcsvfGaHzCf/hhmDu17Myr+2T59LujFSd/xc3Oiu+/oZzbsH2Fr5L3z+SZKITbBOUU6t09cY6o+XhvLiJuFaYes6dyJVjpv/3vKqS3FMr43yuBMZGHYyLjJkSijInghj1v5Qq4DgPyo5fXqlfozkCbhfd1Yyx3tFkMlujno6aep218eObQPUe+o8ndtISIrk1TjtUm9QXuuN08omD/v5wxTTg6xDn1SlREYyJH8xXvsD+Yi5DMsXdVn8lZc8p0DzXngIxHAOVDPdMS6B69NOp2fj6fJCboZkrQPjEq7LWvENbeI9N7lsYzQwnzS79eQ9pwdvJHNn2w7MjOaGXE7jC8+5k7UUHKbCK4tHPTZqhlCN7fpSV7CoiG7yrB/+3RBXxInz2SBclrqq+ZGq26XKV5AxYg9kYef7sHcXwONjm964eVKc09+cdEdOW8KAQqjhXaZYwPT7cp0wYVkaPRhOmMk0aPEUIbtKDHMhQ88cMZ42Tvp8zc6k5WOnva6bsTpgcDJCwdZpjuiQ63o66lbtJmrMeIYXJ/qgTCRlcLYT6jorklmV4gIEwcqGDDRNSKtbijFWr55u0WkNtnB7Vi2837YAe8xN9Sjod55cZMDRNhcR7BU3PEryxYl0WcmzPKyIymiu9ITzwdQA3H7QBsPUDIKBM0yqSuW1GHygrvATlyQvcq8e6W3D4jeR/IfWPf2QwPalVjJLI5CWOr40fW8U9el0YZpjwsfaqYgXbbn48crPs/PQwoI0T54Efj8tv2m30cTkGVD/+9n6kz0Z9ZbU+JMp0QFXalnFTwBhO8oyb4d1rFjrzWMrOl+O1B+ieFNVufTM7m+GYS1v/mMTebLdZhz6ofamtxzjbWoQ5Mx9Kngk4zw3RcReb1sCnT0IzcHwC58jK18QPuj/Hcjqt2OSmg0e37CfXgEvBtQgpno8scbKg9bHqe1rqbc410lf2DslyWmErMVDdlf/tKERBpcruyg1BWZ8yyyVcLJiPkBL4LqUXP61hSB/THogGuxTic/hU/nENrMbWrleOeM9pLZlFK2iwm7QNaT71MNUB6KaP2EJvFthVoESM0bSSoGsm+AKiSsi5U54mVC7nefPEmmYbYJOsLWutaDqhSCGYrkVgLEe4YzQqDDB++jEiP21eFEj1LRkUzy10iHf70pa6gaA5Rd3Kto3bOha983Wq5eUppwLqw3nMEcr3Al6gTOieRE2xeLSTKBTbKS+N6+7gsGlnFfysKpOPOkGgKXgP2ctvSj12iAm+qFIhOkkQUosXdUTZGaSlY+RFzjVY7RR0SN1Z1mdCL2zV7NgTfBa70EhEVUCr16kykLIAz1hXsPOW+JggSP5VXH6+wLGDRKuQ1V+e3Ze1TCPXMMBsOOEhhKXMSmqvqmk3p5NsJNMPu0G8WOv/DTEfmYsYd3n2DcArNxHmyob9FYqZ/n4kPzkdkULxeRpwS0QGAPDlpNXWbONXDm40WjmQ1w8vxGgY8Y64lkjgyICCZrbvit0SNIiu8vadEqdQQw+YhVqXBXHwfmsljoYMeUtM5mu3RFtYcI6whT5k5sbt395iGixfWlNZQcJbslNQOkkhGmlmvWg0P8/IHJR66t+soNZrrf0B3Y96FOuZCelsYbZUR3TWstFlAwXEriNtVPD7/KuIC6yE2TVx0r+P6IRJXrYJln7HPSg6IeRJVnpwodwGC4BUTYzVaSUGE6bmwC17TxIoNW/4FNcmRDVFULk7r5c1+BEbZcgmSd0LVBne1oEhP/yud1qiEbFQwkLNOtIEhyTQ0LFnYaQf6+sv5f3ql/qveh2YWAxv0swDjXVD5nhRl5F+zQoiCAcRIK6mL8afo2efzOHtKF5xfESBygyHisrLknBFmjutU2ZWASUFvlZ+INGWIxxK3MfCPq3SAUjAtkoTtyKIZrkiAyulN59kW5EmCBQRUGhHor/EP+M55BOUIgPPwCthsDpYU73rkBPWI9GBlJF8ffDBzKIaQdckYVnWxNmLrD0pwcAMhHVpdQj+zE+/qhcJRPoz4iM9A1rrrMQuRxMAgFifuBchSw+OCP27XZkGk+w/cp5uixxJSHqV4hmYF0O9fCxbpE60luKkM+1ym24g+gygCln01BzaVrOcWbgDqkCE35jDioaCPyZf/xNSG7sHDqCyfYdIoIh1hlrCaG1whw9iIkxIR3n3QE+83hv3knbTD0CsJWUP6qgcngZkhvE9KoDiGfMeQl/GXBuaIQO3hoV1WXO80EdCEaulJ5xWFCfk/lHJnioIKkn0jKKWk6qlMSMDT826fHBmJe2zFlG9KgQ4dSN5wigvTUNlB1Dvhd+7zM1EzOSMQq6dLJDe6TxA2l8QIhy4PkCjdVH68I3Y4xB+98kxmdsaWBPaVIPzPv9YY14Jw4c9wdHTY+evB/e1B/qZvkU/ISuZJgvCBqALhEYQYBXJD3Rni0DT7PjD8lrbumF+NQCjRl8MdVAoaBzIK100mOIgKEFEg+kDfkllCUbEgPD7HgwRfQH5qlJ8lf3M/1idq2DsocwQn8dw29185Sx3WMJcTVm03x4xCSG/Rb57sm7It2dHQtr0r1Dug7BlsYuAlI23n00pnxNpb9962vwVgsvO26Mq3CDYK1jSztcmK24RKE9e/PwcmcJpBHVP9Di2CAk1ffBJwXYqlXKRNa1B2E4CWAO2m71qwdhPmauB2+O4bxO2/2QKxmoZYe4zrouWoPSzbkwpnE9HMbzrce4N8lF3NghzCq+GU8UottZabGeJNIJRH1NyVwUI5e6PsZLyGDpxNEZUYbS2GXcE2P2LszdGsSceEgbPRxlI3XS7GIuNEMvwaEGpDsL+aNdgDSzKhbpVbhxzblFENGlwD22l9qSmO48zumKPI7baDWO+qdGhFfG1Z8zra3ko2Mcf0Dbx0hzmKDzTbmwoVXcpftmxB8Xp5ckajIc876NdnPqao9PEWP0Dn8Qj86Yq4eINsDz08UWBa2sFCunQEzd3cZ5ITsgn4TBK5iv9QT+jidtOU8NjmuEPZBTdVwFYUhcDkMXb5NoR+/xxa3Iqllqj0NBfjFh7V6pn9HveeR+UrD7AgkKxf5WrL24q65A61Jcpp5W6+W+tsFqgl4VhJzXbSVhXEfBUWu9V1EYY3uQCcaD828YjBOE0ZfI9VZWeEUrP9erI4UjWMEWTzbjbd8xW+EUuVLw0EoZxbvS2t3iCw0ZpMAQ72Exnooc+sjI1zHMLUvYVHPr1SpkPc8+FW01HqHgNC8ng8VDDHTSl7yia3LCh69LOLNzz/7MlFkh2Hdu55WHKNRtNfLvxy+ZJ0gMcAMqM8nHM5+newkJfv8yJpIgG6bJtwuq7x8wxj0BxBNl283XPCWroyBsvMz+WoELCSY5lZvC26IV5DnZn5icNBSzp1Obo7bnZfgHZQz+zSjd/+z1FNICSC9VyaG8kNRl6c/TH/mLCC3V8Y5NTyCiuC+O0bdNsfN2MtZ8qJV2E2MkKFjm6wWCFFOEHQ8rBxZapZvf5CHQlORPPMmARi9TslgzNWysgJ4sqYQ36vMvDCZEkHvNOp1f1KTgC8t0H2ZuotdAoW0HEJRqlgXCynX1sUd3nBHniD/3RYvU3WdMZut8P4MkNQj6u8SEsehA8fSSU9Q2/iAk+Ev9jKrdNXd72VHKfR0qkjJMOb/9hA4D/maVNecNYRSVDO32ieqqIEq3rsnszLVc3zYUuLWa0Ac7FGUN2QXCFYsDxWyyatXgCyxklFpai6h2dxQXH2cQUVY0NbfAw8V4suMiEJl84Il5hn/FQiOYR3MLKFWAlPkrtvIJxrHdbljHxymor9iU/Bti+Thf2PVyJanFu7XGBQuaqEPYKJAkb3EiDu1uqiU1n7wMvF3KEKJjFYrkAWuvyEomZq2Mnhxme/+b1wUO8E5ON+ZOy3DzzJnluDL9kuqCG1B7/QjeSrnu9HW7XnmoT6jALrjWzxuxy6ZgvlfgWC17RDergH7k+vyUALn3HW4K7rdum7yldfb8D1dLPNsbTqEW5fhcAGAMvrVzbMnjKd3KtXEF30OIS8d2NpLQUB3u/13v2zshJi3VYHgeEMx/fZ9StY8ykXfucHdfConIw9JLu9NBV5TBKaP8fk4sOPZn85uVB4dRj/vuw6mRMF1KSjKghKMnadYpOBvFENWHdyCYye2SfWaeIa2bxp7OPjSLFf7TY13NUMEGM7Tb/d6PTL7LqFzRECgvRw8Sb7ho3Rai+ywdiYodlfL9213x/Xqya8vMRwysgjhRIwfmodKDm7fiE+PkM+PGREQ8AOkZLjpAnYA21QO1RJxEI/00uTv7ToRMEdtITXODygc6nHaZBU7M3FvsGDetNN8+dMtf3/sexQFdEV25fx20jwRhBBJeWIAEYZRscmPjCet8ZyavXLRSgCu0tIt2Xm7H6CTrSHM7pb66UrQFRhnXLEgTwY0qvLdzj14+VFk168oV6+oX0B0b3XbxgVXwLVtvCetEuw1m5Bf0dzyMuXcPZEeXOUDyfUyG7L/s1gPNIeUg6KM9mTmHldI7Nci9yD9SCUmdJWdBiRqDQYWN1GE8AG+QgBIbs57zr7jwiHNh89Xw4HaIcdztct7OdD6AdhOE6HGI5WNsDoVSy6W5KLIFKBOW11N3QJWvjDtZsuNz5M4mirds06sD7DRLHL1J8LizrbPDJ6HTQqrE+oCIcXy7xegjLBLJJ+JID1xm8+1ypWLnHLwxqHo1oRU194Kzgg71sl9uEMRJeDUEDRjXmmUa2r3v7dvWrCC8mqQ65KIzLSiMw1zrsd6AudxOAEVA2Yz/8Sc2aXkX+2pcxZRu2IhQ2X3WTCjlBjpoSxLus+2ol3ZQc7S5m3VybslG4HJfJMAZ/pr94yMpPWUK9ILPGhjZLIuo3cZKyao5erOPnC8V9UO++DjbXNTlPLaG+bLydyu5iXE9JplSI62FxTPwTnQFUVOdItkcwzYs71e6wr15R7BmK+m/0HmQoB4SpCBP/rwCZGTQpEsbaCI0BVInAVF0l7Eo7coETInE0XXihJiGlE25A7Hbw5+sukKFArXhwwdbkkQBKpAzUNwRJEHyNwHW3fQKlM9m3EQsb8ProjG3UITkl/RbxZMdIrEFJ1kwGCJvop+to8IRH6nXM2yD2xfNoC0cjF+Q7PKQEfistWA/f75Y7J+VSuzzhuVTflJcxzvd3kM0geUrloWg1Vloz77jP/nEvW4foFnAByJzjPlAVhIN8sAY3tlTFAakWkVsH7DMJfURFerNgFE4UDiQvhvz4M1cG39ihirtiThw/uwO9FgrzFRKFVglsZEregg7fafQmvoy67K8mAsR4f87ZAROOghiRtLig9PXMdm1OCNUNpTTXVDKrtq+WrORlY+9fAbOqRCKwBUstHA4BBPvLAIKzCa8FC4qnnVUCDbgUO7T9pDxGAM6txRwG0gYaJilKf9ltQXXjxftFZq+4OEiPNUn7VQjyVnFogp6dIsRDGNBmnj7klKJcRwJSzERbemLUz6EAtQwi1GTmOo0pw4VmM3tGDZuDRiy/UnQXG7kIRZBGZ088Qn9XqpZqlSUFPl2pvlkfEwwgqVSfDUZQ1nqbshJDmpFhmkbazhHY9ozTzJVMjZyfLWFgsrPKxe7fvQFbRWqF5eDTFwWNceVaE/mmcHWGMZdfy3vreK9fEOrWbqO7QxNh53TNSLGVueEWfKzCuU76K4DeOwJgREbE0Z6/Y/hvGNEkkdmMWYLWo+/xLH5zhOFAuihnhIC/FFG+ap2KONs1b0bqSZtJHVjeWofh7Y9JiS1liMYpygZWFSImW0rlbZMk8XXUdVGyYbjGFdxVOjD5Zig8dSVj9OB+7V1Kja65EyZOata5EnA3GXksETTrdK1W6Q3dBYklrcxsuoktsBXdFxkovl+1Szly89Wi7wkhnYSLDsGRpZIcQ207PT1oYI+4wVMYVg44apga3b2qZMzvma1KdBypqJc4Gsk1abIyYij2NsdFmn0wUsnJup/9SFoS9vwgvsHODWDAPecm/jX4KxG4+vjigathjAsBBJaLWKASUKtsV8DiYOXc1u1ecrHgyuGJ52gX+RRkjWaFiXsqxMo9PDZqF/QYOyefrUHfwVEoorEpuktievoO+uwf8+60x9Z8SnTEPTr+6qJwQ60pJyZ/H+faWu2xKTuE6EFmTaIBaM1FZRC+rds8qTGRJzzgvqu7pX3TvLEZBtBzxnqKsh6pNs9PAmwyF1G/Rh3uuSWlIpdYkPnpz4LqapyV3ufyUNWBhPcs7MeZFzw+HXJvNHii5HDMqPQQ7rhh0fWU0/Ej8pKBQTdsLPO4MHZ04o6N8YRlUczQY4umfRKfuh2LOEOtikECdv1F2/5vkc831mpHV877ml2oO2J9aM1r/8DUoQEH9mSWTJtsAZP0CCX70emkAHcUVUnH8eL28a9F+Vot0F62+uYKuocRy+wv4fWD/kIi+nw+74KNSofW4s0gZsfeVfwxmSj3TvXePVgN1XJWsymFzT+BwUEMwsVy88Q6R7ugDMxwReweNnosuZlKTjJasslDFMpTRONQ9UVdeD9IgN45jfqyaK2MlZWNigIiFTSRPwx5Tw/UfLF+VS4RKs+oYh1Botn9ESbY0bKEsZpC0hf9QsCCVEIbhJFRgSqGN/UMVEy1OXlfac/AkGKlVv3mOWzKQkK5Nyn6WRksAXK4YZ0jyYJl0ceVDvuRFOLCoXqD6UA65agQNOQu3l5EdLbCeyY6AJcqPROPbt7brOnotWw7VnXJBp3ROxPL24UEWCl63JRORHG4s6ve9dmjfs7eyV3pPtWOOxwVPaOSz1pzv8eMq5cQsgS31H0DmKfM0QJgik4KHQ3U8hPjwtm5kB8+yHYhi6HSeeBOJygn8vMB1ehAyywrjM0OR13vpWkE2tBXEvWraKU78NZfuT4wFXUtjQparaD+FnyhZAovnsdQTR/6KZITbF9HT/Y1Cmxh9KMM8cPyNtVXEN0i1WmTL7URwS0WLM3+2Ry5k8v7CIj5x9liWI3hZkzDTKYvmk4hGggeLyOUv9PtxL/T7kRf6ncgYr2aQuVgxEbA5y14P/EiBpGk03kumHLbPRJ2vYhp203pbtN0qBYooUUUG9DuzRgXv3qwqZIuEhp5LJWrsAy+OJwG2BUEY8IPKfyQgSsqc+ODsaR22EvjREeN8/q9kUP25aAygetB+kHMe2MlJgjqL1xNj9FTj8iyI12sSFI8CLqoIkKswOGTo4QO9FNvdP/Y+h2NKPWiAUw8pgEMSpYiJWhFjJGT16p7EiKPhRu6BLK0IR3PjRAFVSzdeWbGHLk5BjW3o2r6qSI+H0/4VQBWycvj2b/2ab0X5v/359tKcudj01IWIXYCjRWqlvXKjjYNW14UmsVdFJnXTqTwgL77lllKl9S9HhHtOjtuidNeUDM6mwzdfZ6xPZbnleVXN18wyRk3D907A9jpxLa8beVNxTUhyO755HNpvOQZeQc3SOHffdI6eFdWfbLOoLlacP2uVIVFTCQgO9SFbmYEYEWmsrSxEvMvWoBoD0vYgXrlPaxgi/OQna59me0ffG2jPx8soKk4GOdNmvZDoA9jqCZkyH1Yi1CvDpuVq1TYd4iN2X5AApy3gJu650GFr6p4yJF/7UiGXk03RdSOatoUNy/qanuezqhLbWE4xVMWXtoUMG9L1cUrtNja7vYSYUQVtuCWXYGihzItggRpyUvZKqOdygkNLUglZ/1dCRpy+3Ew5NRhFD9Nd2xrccmrG/KzOq1sCWLXVLxrRjq0JAiztjR42iAVHpS25BniFYgXiuGxBhBUyMn6kJ6UN197kmM94GCn3BBFesVXrguENFxWIqKOhD0vY6LZr81ddPBoZAkRnAAGpaLSYi2cWJ1mB5yOpAt0Ra0sn1sfGGQJXlaoMY2drNWpF2aKIU9p7ccn+rC6xU/kLrgqUgVmCnt8BIhT7KuM9GMsKcyLon4dsfU9NdZBDs4DQj4ZSN0FFUSjhXBCQu8Ys61Vy0sz69ggJMQJMCAuLyZ0rpSncsBlwE2gF/Zl0hnHeZS8DndKQHctirpd/ylUiR68K9JRUQXuFefoA8PKhSUpmGmIUoQkB6GWqTaWivO6G6YU0M+8WRmBtqrkuD7O3cvxe9gVOos4ILEl0tbafz/va7ZopEGwqYgfWgZKV7kxFZrcRHBMQRqF8PyoCgUR16sIK2EK58F6nGs829ezQTdRIm/aXNu3pRweVx94e0qgNZBH1By2vg1EAzQ8joceuhj7Xxm+d7MiiDSC9PMm1U2GygL1IoDfrMe/Xp8LvoaQTo0p/hV0gyvE/Cm5u7Q7pJHmdUgH4PiAA23NCLAVsSGT851Rsqzd82AUDHb5gLRvKnrvif56geL1yhoYwRpN+SZBJu/id4HWkEyfCS6dXedkqDkoeLyw3KCrfiiQq+olIoNahQ6o/mhZL0nNxKqO/aocHa2tbmd1bf0A4Y8B2ODjW6wm8dR3O3q+JT4Nzwjn79byi/kS2TuUCZ+R5Z+tkZ4U1Gp7sFCo2F95+qtuR7G/UlZLRQv6CWkWmL19ugEY0dI5CNm2CtPnEYfamp6japtj4qaPIHJVXuOqRKl7BszKfQavhk0GiFcdV0KWNgc/w8y8sJvT+WltLU1WrMplGXa7UxvuASbrm1lHmy9EC0X92cZZCoPR86PFdny1iR4Qfe5wt9fSZFdxvrbh/AJXf4/xgTG7+AD5+2j88Rs81/essR+b4q2+141fdn00LojnafOmjVoELOW4NRu0ervxMUX3zz8/Vet8bO9SWavIZhtVCsXPyDAH8L7vNpJimEKiz/8Q8soCHrWIB81ms5mmKM6cx21UI0/5+Vkx3HmP2v4X69yJTqoCfkogInuUkihRWobyo7sqLmH28WR4KC/LZtX1RtUTbkwlmdtuu4nhRoTEAph9nn1/PHpqQZQ0fIZJrNsEOxwHVLo1zuT8cq02ccoJA9Vcl22xKT7ckTYhsXC0fu2hAS+e/X4QqtbL3EFIoHfaRqvcMaDgx1Lh/as/zOKSVz5R6DLFXC3euDXVv4wuqyRKU4tEVZLE7rMXrhLfUeWzjhuvapKaNPVpcovzcVFagMhsjX5M/NcAJtdcfnhZPl2cBKZz4CzikHfk38x40J/d9GtgOKywyLsZ8f+wYsIpaZ+HacnkrnDyENrkjVqLAtIqGxF9Q6JYAjVQKEb0+Lem05JPy7P0oIfUIXr5pMOunwML2Po0phm9xG6q6w2PNjtEbOHrzUwbduh3id60unv1kZ8IuKkffOv8y8ItyYff1phuKsO0T1/Lg/pSBpYGcOeNA2Pevo3UNHSeM5xu5U5IkApkTQ3kI07sB2kP0IuIzdURi2+V23ZLXnD0pm/9D/+xLRJe9xonnzF/00qnvb0TRhnNO6oGrG5pwuhL6h5KgTmtSSIZsNy+Mg0C4JVaZDBb6NI/cEug2oDDN3AUrl5No70vQEa3MvHXzFpFa8i9GU6ZPKLCy0HMGccvW1XZWJPlL7i81kxC6T4jC3j9PH1FlXVXK+8PhXR1Twp5MgTf6NvgPbHAwqDRhlz4Dvd+xknDgVayaxfXuEZg/7/kE8FQT4bOXQiU7GBrrBIyJX0NTxI5AnFUz4M3kqteag52mYZnhUcvcufazzM+96u/HBWZNZ0NhhE4H8MscOtOJL6h+jkmxxJLhqz578/a817LEF669DPWMxwMuWqd8gsVKK7+1Aos61UI9qqf8knQP3IBhSU29lmrygTPlHVjoDng+3OxN63VWOoGd5XXJVskSY3ptjKhz75i2lJ7HXUVFoTw9c9Et1MMvE7tPS5UUyOh1yDQdIJVZagIL7hsWauTJMtZWstJnevNenXuWZlkxkkV2vHyM3KqkeHPctI4mcJX5GsyJm3uM5zrMlX0AA+rmPluhXNMeZur6xcIWWC5ngzBsbRbY2/OSriMTz0mXOLN2Kiy8WOdXt0vHbf4pnLSzV3LuBBO11eCx70DhmUeRlDe1Fr0EE6x8ygecxaU5xNjG9lgrUl1YXQvYMPltxxYvZTSYwiuWH7ek8dqZ62dN0Z7sPOWMbX62sCgVFbhsuNgFK323uBFmUDq1pKC51K/7BYhpG+5MFu4w73Lp+LGd8uYGcPb5fyNQ2N/NmPhO8xrV9QUqM4WvOEDtCFtHepOkwWaaebo9WeIUMz8P2PzEk/bZI65J3fZIKLtrwow6H+N1IGesN1zT0K73ygVfrEsNzbKfSJ0+NANvnLHMaKPLHw/Vn7pb3WPUQse9lodOmy5mtqKPP4jO355PDxtPPC32Rhtgtl5vrCshJJ5NWDsNpXVHcbYTu/wtZzjDDD+t4OX4IoCW40ZUTxBzPPlR4HoTLCLK85PhkzUufJ70YjSt2bqR7akbXE0zNocBcGUJ7fSZSJqRF6S7QRpSvni9GGFrzhfPX5C6VkFXGbO6SImVJTSjdxHtCuLhswbmlYKzx1HLcba2cylvus3jTe/qDsU0zw9ZNcjLxNabnBpt3+jcqbfR6+uqV0765e2t1tqUK9P93KbtFKyW2WYArKZiSdStjkIuKZ11nhqq7RKnLDUppVidseQUYb0rD+f0gneAUNyA4zH7tkRpCBWgz65YlATliMCj/1qkskvN0Ejgps6M0LwiCAtKwNdwUn5UqKDzl8dROz5eG0TXs3QW1J1ua02l5+tgKGWZVl9O5Q8jkA9FH/4cScjWoXJjPxYQ1D9NWNU1OOlLnzoLIf3b9641QmKD5DySSq5bcjF+5LuGbGYX5RUA34u0c6WpFPUZjer2nyLoUab6idg/gUigT/Z21gcGFHgwXqESBRMwTtJClRznPm99u2IlRKU55xJ3+rWR0JRJNQCjcWbG5Hr9ibjNBTubGx4KJ1ZUErvYiDPtQg12OPHyD8cgPX16bDMp9w29y21e1j1IsD31FXHRaReYE8ShA2x6/TPNNrMjkq7R0NTVpntKk2z67Pp0YrnL/lamd9VKYh6z09WlwOgtPJGs356f5sLxDGO3h6NuIw8yMzSNYgIlrqTjmkDrB5/qt8GNqEEMU2ESFtVJP0VlNG6DR3Jk2F0WyBHXdVNg+yG2bpzxtx3RFt6BxUIvLrB2KhfA3fgh1G3r3YNN03XoyLjEqQReMm7lUACjjDf0Waw2ygZZObvG2+3hq59xO48SaPXawAmCg1+7/R7tsG6LG3IV2flLyxzU+Utxb0X3lWjOenFEY0brxgWlaljGuwa16KenGs5+6t8eZaY/MuH7HeVt3RvjK/PiY79YWxEfDgBFHCNpYDFFj37kqiUy6EoW23fDfWg+WG3w7UI0nq3orHSU7a/UPMsvyO9eyLbwrDnxg2NokH+6yWbMO2pr/Y2UQUpZuJ0A4UYWgyFM7M3nRqL091tpnuXB4fKKMN/T3MQHVlyPusZWma+JuBf6xMv1jmo8XNM2w0VB8yo2w0I9VLKPIHKrNf8R3hVX7VIEJHovWkScaACl1woPyVWvEp1SvAHz/WCZ4G7Efs/6U4CFPsTH0LZTK88YbWE+WhilLgCb+HyZp84HwUr2CbS4rip1mIqxefxOJUGYs1Mq7qYMXbVuQOTwJHpyEmPXeADSKLf8hWDj0zZZlTPrlZLcvEy8dSn2M7B6LzZu7P2xah66k3fdXbAUryztZpU27BkMWr7RE9R4d7Hqm/G6xjdtgHQA5Edn5hvhYWdr8MFqfea5XPPI13Nr50vOYTjDRJQ5sX+8D3KWa8l0M96hqUazo9YuxvXFzrl3ItkkzhDtAaqoWW0N88e45tbj54pl4VXXRjDMG9kHP3tm4Q71MlvDLc6c04gPsO60ZrhxR4l3fz/0w2n5mnplQiBV46e1dvGLlBcn5BxzxPnSCH230oQOdLYuJtaGAe9b1n0dYUPFXatr1JFL8VCxhYhmvKyTgmqw4eaaAsPqPjnEWVhypbR8v9G4LkXZtXw7E+DqKd4YFEIk2UXAUkBijGBMRjK9C1ghudMaGpfDRd2sCTL21Apq1xYKjp4rmQtB+hmed7W+phYAKsp28exeP0SPbhB4+SxGZ1krg4vzFuq6D0/Ycg7ek/BNxsIjez5RGf9alcajg2W3nFL9C1vixBcsHC2UzGlEqvMOF9CZHPKlPUIU9uGV4Bmuh7D9nPFjH7LHJvN9Mj/WTZg42XpStcyA7+1rNOn33R7FBUrXYv1MJET6T4rN112TAv8vRIZ2nV4BANjvfQm3F8+on3qO7h+hBXQq9sGeJlH4KKwQ6ugJb/SubX5F+TbGwiDSvLvhuJPh6Q3Irhkkd1Phm5w0H1TPqRk6hJOR3DGk04BG5cjbXA/IgoPAp2tHlNqOScXBs8vRBa9g/bo+nNA6jFSPLA19RYordo8rtrlTldfm/jftHBByU77FSj0Beq+nsy0W4SLo3nqBCSzRZl5NjIAFW7WBQyusVfkVvofxW9+VbmJ1qE04yWaPOVP2YuNsH6YfROGI/7DhQeMPI+rbG6sDAXW7OF+/i1VALNoCr3LfnE6AregOPJLqYWfy+3HJs0+IqNvR75enu2TWtef2lqT54QYECHmcdzsfSUaoJJ5rRPnm73C6Xmbq7iQ+qmP0vg7hxDlKVXHbpnWISUEYn1m/qnPntjzynnOaJO4rLAdW3dtktFBVZS9r5lTLvzEoKqA/Rp/A2N684272MygXHChYg5bEWDBRncDJaQwLQU7oGEd3+UiwkMcVWzlheDRXuxPr29bZTFDMN2lOoQbvHmP/UXNCFavXaBWdOlTuw7B2DLphV0GekQDnp+FDRBcyZCuiDXNX4MUMv0wRroShop9fXY6KhfPTl/EiUyWUnb3hlcele1IZzaKO6twuFWsrH3QuyqOcz1tByt7XQpXYFQw7f5imsu9b0SIsrV+Ucic3bSCZx8codOIvRi42fJOoSBPEVQ8tH8vCxY2lolzsM7yURJgSK+QKTsdeAtdSPGFgGvAWMinT3D/WIyXpDO4LbjY6THAC7xnFqQHTl6D+M60IPB9EVvZ0rWZ1tVZJLlRg/k/+yeT5AxhLMcoicZQgVMheIEwH1oq5gPOugwjiYp/48gRE05LgcqTAqtF07nTf6YhqkfoZkcu9NhmjsBLbAGcWWFodkAyykZ5T6pPwxQjojj1aDKm1DJtmzgIpf5kAupvrgH0Y+YbxlcgJBinnjBznYB6+5JUFrBIG9Xz8BL8863x+Nla4Wtl3MNl6yOpGzkArxZEIED01wYazqq4AApyg+KZCRhWPjZ7CTXSc8nJ+GXMNYmfDPJ/Y2B5GtkYd2fLh8clBXWty7TX3F1IOOJYNID9m1lM5Q2lKUJlI5NKqC5zlU7nMHtOYRVjD4GKK5Mk5hGuz1noO8VhM3EDk1fBqL8D/nqp9CafqUMCni5BijPFTeMnCTZrLmp0g5qKZQD/pLZX/EEUneAAvPOis8hfRY++ri0NwAER5y+suhyg37Ij01BtbZ5iUdg63cpplGYAmnxAFKsQ5SsW01KxMZZKznzPxMByLYDlv09Z0vNHcpkJ4CTDW6Ef5gjql/6+YPQ3yBAUexGRFz/Nhu05vjNH1FMkAHuI+9wgtT4dZg4kAnt8/93B+6FW9OlaW2RJ8EzrAJGcOhbJMSvSbe0Gd4NFvKBaNAhv8XcsdaEftcVkmzHixivDdsbJMv/cvjeDA6pWoPLxclCg0jpUZ4E0UkRH0rEiqBY/DIhertDw4+IaSkDxBOPh9hVCT7BLqGyG73OhQCtmuWaU2rii/2gmRLvQQKj0PwGCNpotYCQphWD894fP3sAlyDv0z/elMCHyN0SEhTjnN06RBFUK+FWIvj+HVcDKdWbpK6fBMjm3J0ZAT4AVEyCrtWm3p2es2N7dG1Kgetfnxfovt6A2OyfRIk4H1eu7tDdT/fmb97kVxTpJXEktlO8RTejtM5nL1sPixd3eogLuMlUDLLkic3L+Y0k9HhEj7riVF6I+Dkmnze/fehbKzO/9ThNCHoHzlRXjYeZhMk0KsszN2kFuipNeg0+aEz/txnLxcvIDLQtpGl9A2PPRxxOGbnSbjWKkYd7mm0DZKYqi9SO+eBOWuRUM50YOCuZ/pv0TsQMa6z1IEM6M6snXJc8Glkr016KSrqWCt4pfk9msqm+dx1hZxJcyeAVTd4YTF5sx1XLLVhiq+b2/5CyNPOtKaiBw9cYTOZQWmPz1OLNrYqe4SJhskYKAxy5h3+J6TDbfKP+dcqHLdM/Rsvu6vdoAtB7p0DYKQ/AgXRtHvRnPNdzSUF0sMBPGMxwMPBTX9qXPumFMdDwU3401lhwVQ8VQ6ddl40bBfM8sqbgC4oLVFGgCgSkTFP1HVn11O4Oo/PSelPpdykpZnznTCquR1LzlAjoPCcZeC8SVWbEF6NT+2sQyP2u7KfmsI8gvHTpubz1RgAQ4xoBpxE9WMv9ZiMXscy9q1gxIExT+CmvMIgkMM4fmeCta4cIy4gBk8U/C/gDyIxL6Itz18cJ+VYOY8SGmwWcDLm8TASZ4HSFwqdopy15sFPm3SZvJq20puyi7RyEy9cTN6/CrSXApu3r5aJot6RtpaAgVMhaM+2PohKuu+GNnozQ+9VoVMznlW1iASHbUAdrLCdiyKmr8+gCVpLDbO2KZzAZC6uJvGGJUTOC+dFNi/tqCRQC/gFZSbiQp+4XNsaxTqIXE4kxpIryXUpZyZOH2HkSZ5UnVkC2WqFApldshYosy1yTCEFaZaCuTW4NgtAcZ37NKNYMxomeIwVyYVcSyeA5KaExBt89Dgsu3RZhxPLO1wshObGgS3Ow9+20b2ucrvuXgytpOfi6WGKy7U3dZ7zinDr3P5+pxdnbVOi7sRPhrskCIHFbjUQ2HiRNBZN8MG27Vs3BSpOrxQ9Eoy5waHGtf2V1deDYZzvTWrIVCEHpnNYbGyCBmMq90Zc6tM7N75ZV0U/PtbD1hyfyNdJHwscoRTuHusTFA9FBisdNcsnlNF5InDC2GUfMjVEE2hXJFmFnQ8FNqC8cJzeWXi+hEiE7NKzAOV0VWuxSKoo3trYki4vb6CY0eZEMBEBDRV3ajGpuh2XKc+ejlbE+WsWVnPHGzwXROHOPzVI7Cu6dbId3pF+x9/TGtgG6/LfWeEHIYhtq582/d4cJgzT5YfyA7MRuDUxl6tXDSd/ljhl6PR03uty7GtSbMdvH/NQYnBTaQ41JE8r8lH3O617NJevdj5+uiOvNc8bnEV7wTN31wUIh7k34Jk2jNOzwqOEdFTomdJIqpF5ow26B+HKe71RKybma2KUDMXK49gpZGZnaudtq5+QZHbWvS6sCh/RKIqVxhlm4oT+aNqRRHEU9uL2xMkEQlTgRTcPRt4wQw2xdR43xbwSXsfpzPOWDzHEnjROZJyt5FSNoRcy+6N3X/Ko6hjdvFhMjKAePAZecL5NaFPMeTQH1WA9BH8m9CTUWdUXMDYI6+EmeDEn0SYrXFtMuKZ6MdcEEVUIIMrfE/wz/mVJIdNdUgGMFNr47Q/AHbAMjVoPXrFHUqCyXZ/bY1kPstnGcbLJ5hCK87VXJwFZ1NrgaW0dcFncXy1fjkhPSznbVELHUmnOT8gkohtoCDBthZL6qpdb3R6TJrjzERDH92KBujImU8VQUy98KlCw7wYUxOxzps2DggYaQPpP5RbCv9BdnIzqP4KvQ20mIQzWvIJHuDeOXt9sPVeJGvZGaU2m3Qy9zo4rLCA9HJUGcp1Qs52XeHdo1WuBZYiVK/E7qzXFNB7tMQuG97WLvcqmTuTglhA/byESmCjTZw2M2+FQxTjvM2R/zoLk46sFFt7QsVtPJD19Cz0zP1Cv/20TUpzZY/SZiFNrf8I/QIKfB+p3o4Kai5spru0qiqBzFHfc4vqXp006WdECxdPKbycuWsGdZRoFVPx1TjOCKNI0pRR4COszTp/r1x1Ceh8rlNSgmYZpeAlUZDWL6MrEdAyluT6CBKfroPNmuO0zT1KOKJdkrzgNXHWfw/ZIRtemiUusnqZ1tHtLr95DEki8fVeGJwQl/yAGKoSh9JNifePdro9dGoRlvq9XKGej2D7FmSUtSWZ35rJp5BvEhRkRBIDfP0D6neNBQDxsqY/dC94kFnzq3ao7oIYA3Lf8+yiuPG0R45pwp09lrWVaT2h+soUrz4qtrkIKv3X00JwaLz6SZAlJ6DonxkqwSH4l0jtj2Gc6x+yo0cdaA42hv7YNNLupcfQeUFsxgTWOtZUIBVvT3DJbPYwc/fSek6EG7tmMoIgAmh74rSJKCyA5aCXVpc4cDLHUmRTzfwDalwMHT2lxHvhGcOU1TkuyaqqpRu3AysiYXKbUlnRL83/Xas6ObVfWEJQrooKkNEwPqQrsBEBpytRKMu4niwKHaLWzFTgyN4qkETQxFtle4SSCEoYVQUNDSYRxFgjynXcGLKYVcOojJt7iE+hxtJKxAsDMjsFQAs+xcfXMQsDoNk1FNcpMkLwnoKkBS9idmGxeEZcbR8ZqcCrJ7YedFqQ4CkJ/opprCeWVzIHx8MioHnHDmMXkh1fOhrEybt8n7pCjFjIi3aM+ObSlMCRvNRyzR/x3vnv6BKs0DOPxO2OW3K+vN7pWLntX/7FMLANchIhwpCedc/NxYnygV5XuLY1vEV2/CyRp62VNVojlrGmcIavXDFEcqATJIrIqNw62A1S6eL6QXH8bExePXX0huT27IbrzdIKJXT0TDr1JReWXO7uLfQMnYtzGVzovk9HkH0ce6Qpnq6SV9PaQHU1SWV5vb5DtcpobNQmy2SERLhEPjQMyC6jCAi/UDsvF3Ql7ZI6VEM4qXpgPjBho3k/S1aVNlYQQ0HkLtaLsRHMCxYQKE6wb5G2RHYkoEZY1Y4Evi0QJifnLohMG5wqrRZAT35spTVMDmjftLoVXmykxihgHY5QEal9TrA+t6QmhZLQZA2uh1o6r8VA7Nc1ayiIns3jztIVxEllquTcBU3Oq5fmdyIPIcfYDFAzHHt0vGjzcbsovosC642U2OZQvRoHKiE2XAv4JJZmUrqLZKAsPnsWdjs2gE5aoiOxJVJWk6caleGM+wFfJRbNQLHHJspAn5LIOKBD6gnMfTUnYZG50zrU7Kg8wxyXFck0OnB7Jl9td+3r+FeD+oieTbLwbqoU2SI3/1giB5UQU9LDQQtmJa1Wh/2bqJYVpdKHqHfUeBagnObyilMhkuJzmduy5uoIWCFJa7Zg4q5376o3fCb2AnZewe0TY+3tAjqIK+hwLqEDOAkWQloDu3JVt98yLeX/tjoIRsc6dPegimscrnQjne5M2VlHiJ8zuSrNI0IIOxJ/PkMJbqrBQtUUpORT3/r7ab3wwoaRmMYo01bNjO9ZTXyriVgMakJ86CeqelEx+1uhSmZiTjwE+/62aU059mXbi508g8GNJYxDNbdt9u27CmE4V5Ddax+uZ5dvvBHImF96Y3WI1dECfvt8OIiR2jm81bIWE7rkpOQdRogkjh3CLhgsLW00mHKliDjnevE+b2v9wNpWqVjQb9fZOA16h9uYTRRe0cW3NQxZNNwUKkbRXZ+q4+vG1mOHTk1Uq8aPEU7LVX1ik/tvWlGR9WO7ZIUOQvvWr+l60eHd2AxV9dM+AZfVDHWsjQS8w1pimZVBCZDyCAhK9Sto1egHAaJfw+Nhq9b5S8CGL5M9ZhylwqQGJZ8HzyWNwsPUgrgoSO6pl78stwhBChdGDooqxZlAuLwblNdt/utpl3xV1CWgdZVPm3nTDs81FZqj2HC6HPNNqd77Cic+F6e1wNnG6nnmsQqzN5cUKIO1w9MPfxhYi46AY1MCm5yJdXZIeosfK7EJpGiwBcveNJWaLYqCNFkCbSExcNsKMfOEiotb0f2m+OabGANIkEbeF44SOs+iO1eqwYpDNRWQKImPR07EJQ6t64wRNTKU/HCcDvUFxcN5WrSlqIQvxPYRItFh6EbS531uJiqdIet9jqjmOPKExHnYO0qc46R4qpaQ+vZ9m0Rw0Q+ZZidHhtCAA6UtHfhBRQrkOy3e8hkZOEKQZC4MWHrIaP9/JEkqpV292sHzujNDpshdtdEEhRXokGNtik5ZAYMvjviS2HD3kK1GhUIw2SypLmkuLxhQ5KgzuQ7IKmbbPaW47eZ4hiUWZzLC8OTF/0Yl1Jp3pernyE+X0fnPI60XYCyU+79N5Xq82g2VOeDOAhaGq6m6Jxt/+cqeIH7KM4oEFKMLy0ZDlcviOHxYMWp9IBYG4ngmu2mEvFkuR8WbhI2F8e2mpmytg2wkwZvG8e/C0OattZLMCiijexQPl7Uk//7YhFSsrdmmOprNhCtmsiWEG07qAX1UOVnNL9boHqAtxMXLyNDayyZwtAEP2bOq/rPPWZuyE/ttVSmCtJdFLiZWc1qnnwY9GggdrjFhxl3ryqdd2zQOhWUhVrnQiIJV9uz4oRDch6/yp48a7yseXCVqzGGheiYjKusuZ2AgahMUTPV4fwUhEXFUC0VQc8uCHIl1GpVHqniMFDik57K66RZJHAyAcC/ilBxXqFNWWTEvlSiScYtTIyrfjgv3JjWH1USBpLl53b8ySG7YM+RkV3v4qy8NavDggo0hlllMo+14u9adPlfePy2JcruYZsiSV8eZ7y6t42JxYfxJEKVPPLvZhVctfBmEI6d+X+tIPjUrfSz4DvlPJZlNJCQNkG8NKLDjBwk5VR3FXqlZ0y3Sww+VCbU69bQoIJ/1jJrHHbN4Qjk2NdlQ++RjhKCeC3kFIlwdcyoCp+XV4kiliku2lPF15IWSNmyCg7vcKQIpW2QRaVU+DZZXd2gQ41HYFC3ujU6gF5eGy/hiqIu7heHYQpfwJHBulNINDJ0lBaaWyz8iXL42rkzaS6UXXv5W3Wg9mfSo8OtbpsmkNZbN7e+CLQV4TmMGsygP+zU9wVqjmOAeLSGbTXbUp6oQdj0nU+iOoZnEBxr5CLvll6JMAor1GzmqDMf0j06fmgFtb3tZhBUKIwYn3DQjiNCHKQAVaMpurHLIS3ySYbWhGbmrmwGvs+fZJiCCS1TTgHyOGvA3yk0+qvIqjt1TiFGh5FWTwkWjKQK+5dXAX12FP53zy7oGMgGTECxUg5/1Bx9W7zUQ0GEXPl/CA+kXQA7c6ZcZ9Q4wZA0F6JIFkadvwDFHP6w23xAtoKt0Tl3SshoQMtzRd53wStV8x3Ta+0nwqL9S9iyuVDILrq9Ism9zlnsG+Whz1RMw2mhb8RVPDmDtGfzyYg7qqgwzNlK04+dxAeq8OeoeC2h/elCGTVz5B4erfp4FqwWCcPcgn8W6PddxFqr7XHuBpgwCuT/+n+MLIvWys1kdAhfFEATHvJMG+mjRIsgzyGxmOXVaWNPw3lUyRABNHz+SJXvK32bEjCGDjbd7UV8O6ZXJs9GTjHMjdI6VWHnC3JnkYY3jNtXX1ZTFilsBFVDplvFmV8sJciQ+y+WNmIHRRkhg/KE1EhCjNUsCf7r2SWSfkVMAgDIjEfLgZ1DB6jIzjt8xrLLN7yQTo855RVZ5vWT5bPquWU1b+HEWM4Au8twifq+ItpauBnxfux326cv9+FJ8wK+vA4mFTdmz194yRJmJjHg3WGpoirrXOrUH1wi+rlDyGl0O7ZjHtkPZ6rUIMVsJcj6DE3Y0oCnE9TlNK/OKURMLX0FRubQMKdaFjaCVL9mnIKp/nlZmTiM2IobNlrUkx8xrgv8QPIRikjW1ZRbJyHN7BVYci08lqQItApHPR1EfHiz1Wpf36nvlfaQPjnA1KZiWf6ao++Su+iydvSvbO1XShcXmZCEkdmuVxYNnF9vktXytfS1IeGB+aA5XsXblPNv9CBTqF9S1HMKvknCo5o0PdLBARzUzqUNPAtu2M83YO7wpVduAH8hvG3Bzv8+KRu8T7XuJ5pOAwL63qjXbxmIMUxekHGhBEBuHzfKFtFjDF19Ice+tXU9RigSvpHVPLXYCaFFLlYBk7dMyJRw/ZUYNzAm/FJaU6+VWPJVA87gqQdrmxzOhWNcsEKGjIG9hxPs5OSyaXEa9cFBlGE/KKNabSlep1Q6u6TAReCBAzTuwxXjZVHM69kJr3NdmGS4daYFywphxfGi3JeyfSTBYVxcoThb26eyGGGQYEXvgVQvqDuP4A3JsGBdgKT954LAnu/mZENFbBEkfnY/amBOelxVHRAQCJfxGjZgKvnLPtkarBlxTjhXtKOm6vbyFe9RyCpD1j8M9Hz1NEOCeca4Te8ilvqhcFeYpVf/ol2sSdS61tjyxGsR3cGWXE94y5Dcl7M6WpAEFp3ubAtddnLUjucGTR+G1PnJUUgeMEj3M1CyttBM54BF1T0RmKHvAI8cDPivMUR1PQpKF7OxHfw5LHIcr/Pohd3gxFNV/yB5/cK5AGFMUfcJWFmmFHKsxwPjE5DXFIGcgGqnd68l+DRE21sXWhJODBfSWGGKHKxOMAq1+k3d8XtmcWXq26HbhRgScynzOM0jXwKMKNuUFnC3sCh+FIKXNzMX5pmv/ynCCrzhH2dW/Emcy/BVzJMXg0evhY+ucVxrp0Zd0EUREIMTAysG0kJHTYa8tclW9QBHitwNKJmLR5+70A4hdmiTuWJSsP/++GsGMarWkJ0hcr3keoP4w4zJ6xo94EjEy7Q7LV6GHphN8FbmvVqWFo7ZZHatZFghXKTI9FT3UdM8dCDIlHtlcU97GzUL1SRY4J9XkP6aJE/WOGYAmYFd+wjBSVGmUBhT/nOQby2D/UlU2llbEmKZkXMqiBdXviEZvcMeootfoBA99OCwtcjQKeJumt3HJEPk6u3rnVWKOQFh53ksRWxNoyGK+HzIcbr7PLLSna5FC/xNDe1Q2jPc2BxhsLGy6aSuBJPr8LriQDPRf5wtk1eV8Apbp5Nv0BAzSGovnfNcH3jFsx2QUqH6kNZok8fjvOZJ/AJiKqPxxGuanonbVJp8ltrHE0gqYcfaoSMAKLoFvCg3+OyobW4kCSKiemGKnUuhKgN6zvK5DKrDIcqh+2/XU02/wQbgGg4E68fgbp2Y++VvtQ2AWGiUoANO66RkDB+wTPVvvgzWRM05Ww7jtoYJf6Tkro2bwumS+Z/XndIaJKR0PuVCzZkFAq/WEcEKYF96m+WkqD01poZ4QziYerOC1re1yIbnaGcIddk7dDdCXh4ZFF55nyyzy5x3fpWOI3uMylxOBGGPkCruMUcg/pvGZXmF2MTuSC6exHicb77/SlzeBa81ZWehWZwQ94VCdK0TD89gXT4W+Tq3HArblGveZDwCQqukTvw43kac3ZXiLy9YjQg8FeOO31U0afCFV7A8Lqh+8docaeFasQNd8NmegjzKj2EsHBEPTdUt2H3XX+fND4ku3HO1UC/ubdHXmPluWdzkX2SopndT3s2RZlnPB7iC+12FBljNu5lirbhm/qaO8yVABd+RB2zH8A7fSGENl0cb4mlLQLH/aLTjbLyfftyCDhR000jM//UBpSNR1oaykzn5KdFi5ug1sQHLkwDXw9K51Z+GJWQBnhEPyfy3nB/wqCQKC38CLIoE8dLcJxzNFQZEbDDkQb5Mi2XoZjXleUHuGeAzEr2ePR0X7rpPQ7Zi9RgpPXDnw4swy2H7QXTfFKa7II8I5ZnOCX4Cb8PgnBmrLIjZVJvDZpXYgTVTVlYmFjnwmKMZxrswSzM1qTRl1FfR0E4NNluh4g7T/9tD7e0Eb6kG8J7qD27Mrb0HhGCHffhvSMoTYZcUEszm2EhuUtQoWolDa64XVkGtW1yE8K2Zg10jEIzmFwLhfBPMiYI4llmCjDMB5WFZU6VsQCyIWQBwPUOWgMQZsVAhGig4WrbBVuawluO03WLpXeIUmZpLSWzK0f7VKZFWmlHUsfO3izqwUXGXB7i2QYwX4pjQ+Ao74kKiF8EOOh5bti5Kd6NrDnkuE1jwGuKc7yULKN/QYlzpuhYsvRsFV+KGa6OqSrYkcSEDLPp8AT/y0oQ2srJynZcDO1keBkUg4BWH8zFh7IzS8BWFdUxYlr255bgEALicLvHeDFCjxwJUyuu/Cf2k3v4n5kOP5P44oOZOeesZu4tU05O4GEy02wAV7z2mYn3g6jcYUZZYm1yIPND4gSHNQVI7Aq56ooLAS4FbLR8tcqKc/osZ3JAyWdL9groixsPnxHyCoC/0mCkIoNKnrVBMFmXG2wb8VLBbh/+OiAK5aE9P6jA8+Xo2CdPjEiJhFV3NdGJrEAtESk2Wx9nZmJF5Jd5TFqFK3GNtF9NmytagtesX0x/6pbOUuy5Kxz7cIZ2Y4L/VyN6XUoMFupBfkzPuBTawbhP2PZ0z4LUNkqwYnshvJBw857uJjDWXCkgwKZnPFcGDWgOuz/mb4z5qbYRA8K1x6zM9BaO4OA2DZKoGeufPBrywjpBCnbnHt8tkc8wYgBk+zjkJWki5ZJWLYwkFXjaJ0Gg2XFjSluuEKUIyCoSFe2+xEsBZ1a0bcIRRzZ0OQuUOHUjl3Nhg5M4Acg8iwLmLJBgFOkgYlRTYQ84NBaLgHvkAND2G458XQb8CgNdxndmWXfc5WYL9I+sB9zhGm1Unk4tvCrD0nRc+ARub+uVBFyqg1ZBFhEy+5A5jMK3m38IS35AdJTOJX9v1k2TQLGOJAGM2ms5Q+R8MVkX5iIb4Zc8ryGnC1Sq5o+NuV4UHOIR0kkfT96+lBLOz3CLhaH3LauxqPbmfSDg85911Pn8LGfp8AX+unGQCu1Ge7mSLJ/0eeKI4binR9B1MCJRk/wcfOYquIJEkP3O1VACg/rRYzyunKsj3BUyLA0FPllJdap1cyipskjYrTNmWhHDSB00DSMKmcyqGAmHnJWtNYtFLPZgc5VypGClZ0lr2EahjizXTWwURcHQ7bLHvcqExwXgaExmJFqbjq6SwxXwRwK9yYWwIsKSPXVKhRlbG6TOimK4M5ET0QmtVxwDa4ywXlc4JiKjo7vmZ1CaZrh1PItlrCSlfSrRNPZdAkTER7oZS3kZFOswwE0hdthwvX1fEBSQRWTYh4nLurwoFdoiXr83uiEHspwXpZ7R08IejgzA2JJIWjNgpSFknzsEV51FjO+U3uPAmlY862C15YzCSRr1Au/Jcu6qJTLnZ/v/dAmHvvcC+Hsxj50vwnD/fo+O4jpyIS3h0otO/W8ywIQWk7f+wEYVTxYRzFDHFdHTndJXBYMSXk8ZjpmXcfulwzk43Q3XMeYF+1Fl1lk8QcFWAgbjXNHtul8UtZQmK+qWcz67eS4NbXHFNYZVpFqJhNDkAlBC8YaL7jfaEZ6kDlnZGd4Fugc1ml+og4MHSIxmterTvTWL6YIhepRoUYy2BnCY3P1kjph8sIZQkVx4pVQmGZGCl9sTRw9PoN6MwdqvRlbl2puIXMi8bEfKSEAnlqzjRMcOBEuQIROX2j+Ff2IRDviEKT3mn6vknJm4/mTB9I/Bp/w/Uh9fdAL8DQ+BK7mSLxJ117ocC01FF0OcT16fJM6JGhuGlivgrxQYpCfkYthw9K5Gbr4KtEZ5AKUH+NA7rOzovzFKIl+qPYzczoN6erVkz6OCiAoT9QHvitgl3HPDNKZr4PThPRTQLttwGQEJlJHXYwQBsO48kqEeJdb/oAxeH11AsXSBKOpzIoOTzV7iPZ0Zdhh/jjA/rWeOPbfHdVYwMF5+LmjD8o6TG3D/gkjZqvJemDhY1n2raDMT/5q+O7nS+3oq79Okkfd3U2VWRpnftFKrD6ydMibRaMVJ5T5rU+EBHN0h5pKzwy8ShJfi090KISkCCtytCkoP6hI5i8/WO/ZiRClgbx1BKHGAFRz6bCcBKodZOrpViof3AqXshZhZRLIOAGaeIJ0aOoRFrTp1z/FaJRilzrOBMGq1OALmo5FTv4ABJwiiLB/KcGka89kGAUmD7CNoEWNWPN8EtGZtA6l1wPMOAuuTQlFeJ5HtFMQGV1EYzUhA8e5SpaMzfnpHpSjriE7PkRA2fLz5Ifg/TbsA5mR1+ZWq6gdb1FPbg0YkDHdpM/WGhAKfVFkfUwXPZEzeR5Oa6KblgubAD0gTN2FBPYHXSPN9z6GnxcR7BxfD3TdKpzJJmnc1IE+2ATTMFMSCv0yiYnQq4Xf/OyESeda3Qa13w350DXJ6JuLFB9fqFysut480KVZYQHHoXhqNrHjTxgbuZsvUYRdoBy3wjRaJMrcjSsO9QuzKLqLAJs0QfuV0HdwPmw2GnIEooy13Pr3uu0lnraZ0GRqg9xCllu/FD/zYsGPdjUiejLBN6rtS/oeJ74dMM6nd9IpDXe9KiyaASCwNMOKdsOhlWxCQsgZ92Lcmlc7g1/G7m+S/XD5ahLArMxVF/AdJNqyStSkiuXUwjlMhLQ+wKowHZouqjEIFs6tnIzssl69YoIiVLDlVEH/h9vqhZHYjh41PkBySR0BLfnBjOilqsxulPcJoMB88D3+TbrmDSeWQvpzho/X/wLSfzEO/hRw1r/Z3Fdhqc7bJms1QrRoy5Uvc0Eglavk664bSVx8kN03dnXMKaX/Pobl9JGUQoB3CfsmMp6vSADZM1SZSleB0LzcZcWerChPEF/tz5Y/qr1pOy+A0tTKGodNz5xsbh1S3UPfpvBXc3MK1HyIG++xKCx9+GMrd1CaNxVZdgPBFCwVwFIhQFHa8RUFqDM7hKmnrvN3Gacr6TvL5xpRHSFUviP2gm6ukUJ/FuXk1uPCSEBUlcZWqB0qXICwQWi14pQorgZdIyIaH1x4abo5jDL1ccMqg2kIoT1kvDvjNwoIgNcuwTiyV0xRhz7B64sYiPe9YHxmFNTiGom3mQrXZM4IjUGt7JD2m1nQHrCC4GkYqKDizPNq3St65ntTp4kqpHxBOyFuE21GxMr59pEER4HIquxHremmgP2TEUHyI+UWyE0rMQVjYGhdw5f9F7vw3WGUdzSShQYathKMY57fXMxBfKmqQLKMjS76BytYNuMNJJEcmhYi6ViVGRyR3p6GsJMxSKdMmE28t094X+izFW2iHWSpcqWznWH9IjRtZZjxDV10ruEJCttyaWonAkpUfOEBYGNruRx2eG6npA1SxjVeOYFKTo2aRFOhXUs6EdLKn39z1BFO9WOlwHA7J4mGPCDBYP8EMZ3bwh3pYBrN3I2PAZI5qVyjOYHXWKnm42NA/kInSHNM4PLU1hVud3m6X+ht9SvyEWi9t88KJWLrMo2Q03fllzoMQppsBS71rSSL9hdySv+uyHelt8Oy9UMQxOuozyTHkds38nTcmaQ4JsmuUdhIgu8I/a+/20o+8BKVYd1dmxU+YvvXCGy7dp9Sqx8C7sv4gULnM7HTB4LSA91n8h9P+nreDURfvywY0Ufq/LbdADUPGP2KzJvu9TVCSvI4UNb5h7VcQ8veGTGegAvAFWpdOa/y7Qy1dAwgf+mywqVCoTB6Nb3Bm3WxwBCoVQH3QMp7HoPlr/9s8KFMH9dtLgkLrqjVAc856ykCyCPRtAC8C+8EnJ0UtJ6bjXW84Emz3Q1QfiAREpmp2ITC3NUFxMyGs83GH/Lh8dzhUNsBsbMkuRjOyrLa++fkl0/1y7AMHb87aqYlFtBzLeqzUfVdiU/HhcOEeG0Kb0dgb9dt3Tdv8knBsApP2TYvuYgguLEgkq9ON04LP2dkCBgUtRZo0f0uBeIvz//agf+w0sSlciOyou7iQE8dpUUYX8Brvb2h3NFPEuc6HlQiyNf7vNIVH1nz4N3jKUNHhIdPApu7+BA/vKujSOkLDLIojsg6BxJVDlGufhKA1ERebfonADm3r8BIe2/AbYev8FnFIrcu3PGucZInADGfpmmiabo0sEFY9BJrs38BgOGuPknW741aOpWBec6pFss3Fa5cCBv7easaeTcEWFCWGcv//2s2PGrkbQY7HEm1xTpZz4JhCQ5A1BHHXy+d2M9pIzDcxrSKPB5jgfnt382rtPdNvBoxqS2kCu2Ug5RweIdIPRLwjhkPR8oA9mbcQSMM5toJu7EjA7g/HoVjRxKCTTYAgJ/Jkx/5QXH4+xertIVCYRFpFpF5csN9gCOmbCZ3ZDjvNfIE+qLEsBZqRhWsJOzSZovRqivV7NP2ZCZRkBke0Yai9rmai52kZxvDB4icuwUU+NgJ2KsdXeNpHFtlMsG03Oqwcy0lOFWvHeOSx21Lc/JoIshGnzJuvpvatSUzkmhUCd84pE60VBjs/TIhuIeW2fpsm3r23udRsD/AMZ2nrwM7Zy/InW+XksLjVueybOtDPOUNq0xhnujSdi3GzEbhFMnZz4Mx25BJoAT6c/IX5GWeL9CeKaEMyaqIExJPcLig3PcHoJdFqbt/Zyvg7IJhyNkWZYATrqQrH92JYbA0oRAWP/f6SZsZ+HnPYI8WvLTXlDteeUdB6dTNhZC0ZWOdq9S3zEwJq9Eq5pAb+5GfjoOes41aAtdqC0+eDnmyqmCgBuprfESdt0CLKHpdj1ryyXXkkrUZGIerf/ApxgWA+SdbjhmbB2ZVSLtMx+v6t9aIi6eoBL+aootrZ8Kz0vOLIdkIad929qn+YN/JZHChNhDtQgzLQafrbQtl7pounGToUMdPDxOyHL6iRohHK7SfOyLlOgcwYFsifXzt41KSJVIKdROsciX/OIKksBLEGPWaUyI4Efq5BIPTU21PVBt1+0zW+rWVGxyLKgdyHEaXEax+Yfv21QcYdwHQEWdDa0r5SZV5SdU8ls+FS2ZXS5G9S/k34N0EE7moE3Yi0ePIExNHTRPFfpsDfX705MyilylsFyEBqS4VWQfzjhrMFmn99fcODBRPkUEGXcEHnvwY3QjK86Bxg3OLgGHhLRylw6EmS1kPrjcQTlBA5k3g7TCa24m60jhYEJcvABkY2aIFXe5yMrgSkqxJ9f0/MM6Phe6RvHqobRwDDD0gMjprF8GumI2a1J48yBEs/jsPO1Lgyg5CHihbwx431WKvAmIlnWmXwWBZsgN512yRF+SzTi9OmstHnRO7FK5zHnZSL4uTUQOAa1gALy8u9YUuW5eKMxy3Ei8ZDrr8sYMQvK/gxXFeSz2pvjgje+Ba7EWXCVHH55OIGvL4BqmdonzTpEu08Mpl3mnWyQ0Ap3DRONQE0FoqMkQxLXTojJNl7xTaTk+Veq0n6ixG4aRob2Ez1Whati3qXYYCGrOsmd0sfuC7LZy5s7eLflfiMjw5NLj2vzlKh8g4OSQPoZRev0OqudM74Dy8b9VjYJXDlUydxVqDxTu28RYG/dL+Vqw9T32l/SWnZpJQyZp+kuMM2E1awVGRB/9d16Uqdl6KO+p7sv9IXqNhBS0Ao2ofR34KK/Otq8e6HOw3VGQ2ACiLtn4T5Z1vR46XySzqP3Ra40fmRTjJ4MGcy1q3/pX+wm0R1+GvXnIBHgMbMOoTwPQhOcjjUBl1i5kkVFyUshVSOnN3A7NO/Qql7PA2ByFJbgGl0saB03eRbcFs69KKehY5riUlB6fymbMgIprQUuR46bV8k7JpCLmcCPg8Io6WdihZZSP5pCVfDjdVJuouYkEMIQGFWtwpUwSatyR9NBRafJQB7IcNggIP+Z/53q1ou7wydKOLgyVGGmcwr7Xh0UuetnEufKNyMEhPaGr6ACfPT1frLdsjVggbE9ZYC+A8FTocjVzUeojR0PKz4blRjiX2Q3XS7R4OtoJzcjhgUCwG9QV6nuh1zZKuTjCfcawfo9ipC3zVNrFxjyTOjkMIUCFn3RPdTJ7M8kOlDUVq7GfTIbMV7ByXYMW/vmCTKBZkv15k3+0ZPiiQtSxzG6pJ87guYSMK1r9zjq1/oU1fwKXR1u79K0SIhXg1NfiMZynekSeVI/LKZA5RPksO6hekN/VoD8AEUc5Wy0pbTrZAty1gruTaYsPU12K0DsE4RiJy8M46g0lUS2gs5air4HUL/FMEg4wOROfU8r9M9Jrqi8pMwBiEvjHt43OJj+s3rB7fcSTQFFiIY8od7Gb44er4Oz0o1TPC0NyrFl3bNQRh/+nqgk+O2g9RsZWoic7f1ULBzKFoSZelbDBzPsO9Toam83Ky3KBE5fr6sQMBjJuDMtxmV0HwrivlF6Uo9sqM4LIICYtkJketXjo6OzxGitXDaQ8bge/VSSsFVCfx6AH15lbXbYAxf2XIoszYEg8VNraCBpP1cczpChcV39dhuXDVOW4TWImVESWkoDGQCjHUcp1FHshK1obFnQoJHOhuzaKK5gVTtQIg3/CnV0uSgTVNRvoMIjQtUzw45hVKPuroeS71d4Qf6cTt6qTsbS+MSw2z0kYz+6ELsN3lBcCafYhfdgxIxBujqM4/ODVSHT8qJE/utvVVZmEPIz7piqA8+Fl17vTL/2sCcmxR4xeMxib2dVc2BQz3j5ntrtGMIInUQowxRSlIdLKMP+9kyJ5Doglv2DCg074bq7P+o5r8PNr02mtMgdmIMqw7YSRXZRp3eFDQPiI3uonYFN0oKAHnDAZy11yOw/RuKzJGr1BmAy+W8vhjkm3pmb6/Jt3amgFm9tLBaGU0WHb2womAqf3bGXZJieopIrcKZH+Gb4Xh6zU7zyd2oUV+qB3TIKb8WAVRqyY3StZrUzJ9J06reGgCzPK47etwXc0PB5AwZcOItWCTKNYh7pPgNhnFMssJ88xNA7S7OiWYnL/Nr1oTb/pPqJv4vFpTi5XHMUabRxqSjWBnItakfHI6VlBm7RXKrf9atDpEoyncrorm1IqhdNHQFZhFdK4WwgKrEwdeVaN7HpII2vWjGxwf1Ujh7doDWmtIbW2RKRkBWGK0ZcYe6G55kWxehO5q7osDyw9sae68uXVIcAK5Rx6FXZrNld9Eu3myS5t/xjMmADwnLRoNAWn1dSgqx1HtN2Pw/Er3NuTQTuJSKFSih9oED0+ZU1gNjdxw4Lvn0VZUFFwY4qxG3a5wa3EyiFUujY6RE/DvLW4XWXBiLR2rhYMM+o/WR6jz0GXEgVakvTZlBbkN8PSyqqiK2sChz52gIcKR9WaIOsJ7j5OyX0OuUbQBoqOosqmUqVTUDQxEdwX+9ELBXnLqK/SfMVksSsbtyincojzcY9kok50w1HDZNYn9rUrJuztDCuKtb3ZXwInfPkSNAcM8mpbOauiuBU+7o6pp/E9uicWhJxze50u1YcTKVNSmBEFBtYhnB8v7e8HCFz60UG3yrWNZzxlYhG1E3MZOKgMEYmgIFKadgDTol7RQI3rhTR5zGLzTqNmHfRxtcpFYyLgybyD3/V+9qs/6Eue40eM+M9E7cC3ZAdsAexqNuZhjsysjO2uAdnc7P7oC6/PzUEDqPCAF0boy49kr5FUsGNj0oJqXO7+aZQ+713nXUzC8H7pdNm0KuGTmN2M4ou2FpkqVO2jPxvo30S/aS8uVU2y/RUSKk2SHcdohH71Z5p34Oreypt5Wcg5mPY8bxWwrhPoc3BN9MoP15WbTKHrNpetkS09JCTYQs+HzEn6S+mCQOesOGYKO1esyAeix1lqVB7oVu4b4ivaD1S+pIErCtJke8F7QBDwbrNJwsn8sC0Qo2PDbP0zHNmFT5lPNOvv/th37/Y7JeFJtNnHLhneAqdB1vrZ6HyHMUGz/Ozt2x4YulxDGuPajsoawIMwfFWmCkPW8+K/qI3uGrj5okZK8iradFmC577+z99FkNF8g9GZ7h7SCPVVIsCgzylXK81at6lyicPjHsN4XoNM6I4W5ZmPLErvs+q7ItBvpQW/pT4Aei9KV9jfvJKeDp4Dwf/4lB/g9/4RyfzpF55oFIHYLWX7mBOxwGjitYlWFPzgjkuOdVxPLrYdI8BTou6sARF03G0TINGsjoec1Av5byyLjsvwrAEaYwXYQxv133I+GdWSTpN5/FcpD//IjkOoyzul84v/+G2CF+dZWqrInuLgYonQQU9P4ZitcdujJEXM5PZRlUSjbhmlhuiWZg00EC+defk1MOGbCi/v6062sj5xh27OjpOLanHQLq8P1gJmQoZcOfX5Is7i3tpJTadr1M1KJAGtrk0/KxEV9Tr8HotX5AgJL/VY3DRs6NdQv6AO3aO3FSPrkHl3i1Q5627gVmulY6/6BEisT6DjozZII5rtdJGtSH5iBq7Xvq1KvHluWSAaF69AkFqk4La4LUaM3p6Z2bIIBfhJXAOdCc3TVTXHIch1JxDnDGLlR3B/misvb+wtqo/yFzrHN57aTUkl5pPEpaR0UVAqTxUQ/u5rCgugYhGZyehky4dNWiao1pq1DBe5Ba9XyCfnCB7CsTY6SQ4w6ZWS3zEy5hW4WwQt9YRTz7E2DmexLktRLTcvhi6a0hb1og6qLIYsScSi+Zm5rlCQhh7Pz01y5aMCkGceGTVqRK2UeNRbJW7XYI6CX0g7ZRDwKHmatFOIoI+K50qNuhyGS7xfTCM1+EUU/GfKPjCN283V+IW9utO1NMTtgem+mhLxaczWqxCp77d2hSrH8jz60akZdfNrWgDMXivrW8glZagQkOXaesbDgbySyTs5aVokAVjr1SiOO+epNGfHV4zjtWqcbW1Ik27snyXQRsIirpNAmWHDIFquJt2ukyCXLYSYoN+DJxbQZkG4HVgGxp4JjNqy3+jRw6cnF4qpwFvgqxkNKIpVcyaGrlYrZ5FwzK1hiVvnkYM+aX4SUNArtXqRepA2t564mnM6iKqS0CBCnTFJ8zPWxzRlcRmCZUoaa9wlRAGY3rxREiNXoHNPj729UPsQR/cWS2/AyqFo/vcO75oyMbtX6865NA9AIN3R0muOw8E/7gg53q2L0YrRrr5PF8Nro5e95m8suK1BoL4nwjFPZenYpJ6G+5m9iM8i00sHN6ueh6tJ9FS2bur7rocL9nI+VLmOzHZSnObTfOX5Qd6XlL8gR7BmV/Ec5AXnBzFJV+t9DReeHzeQC1QnsNTWa41N/Yh61ppMwlIHUn1rSoaJuhVwdrmjBTtIVelCxCUJX1Qb4mOsF6cPXEeogWivap1ZE0GzsYx0uWDOEIclsIzhMFoVDhqNmhDDFWu41iLPqFu4+rlWnn8TCryV1o/gNp+8S3wPplLkQvXu7QbquEW3aPnISBQe1wRygQet0LwBpMnqZ69wo6bt8h7nVX3y+Zo2O3gajnl957jsVB13dNfPmP9FwgcBZnz8e5FY96LJsHuyVLt/Iufu7iXfTWBV8CSRcriprC44KxWZeS0DX5VYl4xWqFHw4Ct782chaLsAXDaBFcP8vcJ/640Vp2ZEy5wvu/uJ55yW4satK5kL++Pr8msRV1iXlLAnLaVQa/nXZmB5iMWxvtwReX5MvLoBXmDvpdrX3wS4uppdr6HfGtv4kEqa9rZEbxPqBfZbM+pRjI6udfLKFWtLM5fWfRqUP9xm4hEkK6Y0O5YCz2zUeavhwBEd81tcO0a2xknrQNsTvRj3gl0YYS/VCCVumzQm0NXN1la7vwyHffaOtstvMeAqUtkTznxmpVOLZx7etaIQGtZVx7qu7vby+cUymLXDPJvnbvB3hy3uTQgCb/d52klV1kTeIozD/BHP4Zw+5Q3/xWU8NAbFamxq5m24Q3DAK/S6a2yd0zt1m+K54NdxzxSdjtBE7Q6D1DjsC+wIb8d5vElhLSCC29Fs4arAEc0RE1V19js5CKwIILwbgysu14cUGwDQpmLo/JOUrFdXKLqLaz5zMWQAsCyqoucR6LrYkQDVPgtbWuC5JFoAWT5eaJVnozj8QFNmwTAp7+LR7Cb9T3hAio1QpXvoJcCUNgDBQl9rWpyKdWBTgj2HkBx9PhYWIvlc2QWEVhhcqbTy8oFtLbiE7BgMJSumjjxUUp/Wm1Gtj/XJvCOm8Zu7wc6lat0pj+OBIaBKTcn3pYlYi+VEHHSNVbakQYS7fl7UZ40ZLaifpmyZ+rj+pHcuwuyaS8tUNNC5E+3CDV6IaV2Sg+X5V3N9IIBOmWeFjziJU+RIHUypO7plOEZyo0Nje3PW9ihycHGYZq2wwl4O0Ifqm6aweVmYLxOpNPO6gNN+ztK5Y0MYjOwV3KsDkaNkwdijI87gERjds6u+h8VENVDiAezsksGe8z0RkZ6OmV68+7jdOhsQceOO6sm/Mdk7TzaJuGIZzrLF9e1sBfn0iga4V6mQeq8JUj4flfryDwU++QJ/aJ69a8Sw5mWFK8SL/uOcl5KMnRal1+NRMBzRBFzCV2eeunLLg+aBQyVHMvK6tig3mepBLnaHYCoACnFWYRP6GayaCREJ2yrKr4fO9Ucw9VdrMPVyRBfsJFfhZvYkddPBzZFLNIY33tDG8TrBXSD9ZA/oVgazNqKkizLVwwqA3A+WTZsgiKqaJo72AeZjyFCjAcbLYxqJrWNLwZq1m68PPaHLtakvlgl3nN2toIYALLdyXfvFq7qXxAW07jw90ndT5Q7W4MkIWoBl5Q4ghgEVRC5+nX2hEoIW/FLjF+DRdQph4OHn3+sG0gBqNI86cRIRnuSlLcGvrwn2oWzSQ/wlc/AyxmA4iDKKAR+84gUdCKY0akR/3Iw360mm27UKZGFSdBiSozSQL7SnNqbqUP+IHjQNZ6IMtKzjxZjQlejaKUgNIecAVvf3imMjlkn5ijR8CYw9wyCsWhJ6qru9JKYad75HFDHyShsHkJSlX1j+U144qUStkewaMueJaViPLk4fsFT0bIf7BDujqwZMzSLIOk/qwRNqhDkKb6/eY5XKa+G7JIYG6Ju5++w1HpIZJK2B8IZh+cCXdXIJUXINwfS9fif2dhcQvgfIjt2RBu9OdtdPHaFVHT2bJfK3M3tcCYNO/50eYfChJr6VRg2gHwj4kKmyMs9BAI+8dbUhI5hBvwsHxLJcCdC5Zn3FgORWPccQOjaW3cqgqjyaDqeRdp+gqSizWbbDLmq7SINZ67ILPBJdiDjJx7HYmbRgTY/ohvUxXaPhOpl9JRPREJuaorGwRdJrzfmAaa0eLbxTuc+XVJBD+Mj544j9oeIv12HfLCmkqZiikXtdDewOh6v9FPIVeNP3eIgS+bnPSe2sH9/o9DvuQ+P6iXxQFmBntETFdNS/Z85Lr50d8v4LCXo2SgeFm+yXQmOpCFh9sgSCiCq6EQFKlg+tA9XykpHI5zHqhLg0A0EDnhIdeiWLUzTooaKg1C2EC6OpROBcJEUmvgAre7ZLpTiLuAU1kNRvMjrsQolFIVxtwllCexijFxBdfGQo6KKkczmbWWJ8T5jDcYSFGXtLTEalZzIcuCFjf6kT52XQ40SliqbhRlnqhV/1agWc7AOXabGaUw+ZNjANcHhOzlOVcvwwdwLfgP43DV91mxu+1gA2YotOXKGSYQO7yLzkhiRa1qIqNusSg2ZFT0hF/yZKhUQHHlQyIvI4GvCbqSt/AztjAfFBzmMfLnI0o69VrRa71WpodKHu4/syN6CNkt4HjCkstQYHwIPlTLYSzU8hlmr+aTpbbIwHfvGiRcBKSU3gGvYY4yg4aWiVgo4LJUczxa9FNIYqjjQmAqrIDmssv1wltOUXf0Z6W9DNoLFD8liL4eCpXYKEI0Yd4jWU/IagEQ1HRp0V5yD7M3KsKRN2rs+BGsTF15bFVSd7IAB6CcOlo+vsMX9tuX97TtToZ75eS2B821ZuyZxWnYiogOtFEhE6VMEqMx/oyzP87jGUE3SP/fW1kUrJmIuEsWA8oNDdmj8X+47sOYp4QlQ7SYzlkf9Oh96+07t/HLdZmiIzx1eCJ2pttwzuPYkDn5pZddaM9n0JwITHVDDMMpGjl9vIdVsKERx6K+yJYC8ROcVuJjuIW6gy/4k2fUfpRWfjfHwclw9JEy1KIwP5eZ8CXKl61GjE9KNykq8Z7qzHR/UIYmli2ItvFH8EEKLTIo/bwiBGUpNwxEhgmcXgJLSeln7t/J8nEaRmT14v2WcumFdBBMJe29pY8Cmtp5oViVodj4wtC7PWGWnldxFq0URUASsCH3S59cSFpKVDuzZE0ya2AlAp18iVQVrzP4gbFKOtuOPeqE5ylUwbsnjr4dZqO/mj28wHXQs1roUA9vxwrQki2e9CIcN0k76Zxs4EOt/BIkzv7hoFQUHE9Jfm+e740VYLaiJjQqoI73YNcOfFiqRTzV/0CarpXz3K41Xs/sJS0MW9EY2kx46418YmwcXDo74RLkf+/c36LIkckeS/UlJcAI+a9YJw/q0upgeNwnMhsD2hTAq/VL7LbD3urzC/GfkrLF5ZQTlIlR80U9uDM/+vC1Bm60Fd/UM+0oUuLRMHr+4jLg+CxqpjGvdsGBKRhtS1GbV2DAUb9CI0Ojklm/qH9XoHm5jtYMu/wi1/2fTxBHA6xD0aZzPAQOddGA7+GSlzrKdtaYNH/O5m2++zauWN+bJUyIuSl4bWLNAf1tOV7mmdXrMhQ+z5DY4EsdXk4/gw2CWBuk1smYY7sK3gvUDNpu6JdXid33eijOak63a0dx8RJn+WPy9FSzKz89ccoT+LnMyN+ad5qCGea0M7v+L85efPf/7rt1r5LzT5s8mPrKrTMZkbJ59OfhRVk/GKi5dfPP/Fr99CFr/Q52d7v2VYDA3+vHHv070fLYshowWZhkdYGIKPlIpNPeCy1iYer55sMDf/XBV0+ueqoP7DF9QYDhQw0cEkjIuNg26+jYtH8+2seFwfOluWqhueYOkcPgvUbOqZLP5v4vnSLTiok4bV6rdW2x8r34LyvSrft1bbT7dyhzDjC/oLPWpecL7Qn+MFg3XnjLxg+BKDF1y8xO9jLgWzQySnOLixHa0OsaQu3yzf/b5nVmN4m3N7JQDEPtN/Ob0bn3/m/OX0bzz4zODlnN94+Jnhyxnc+OIzFy/n9wWuOPLy7GrkaPPasPNecfHyi+e/+PWjr/hFh/2/nMpBvpIRVoRsbTtLauaf5pAjNM/TZZP3WAVBkLEfYtZqVoHJobiVutZVt//vfV7iaShJnlll1tYS1ils1RUSGBbQ8NejZK/6z85jgZDD5g1yl1hjsLHEUv56QK5jn/9wWL0VJQ/wUXkSfiz4/17Kv5R/KR/H4rGq9xB+kMRomcT0ArYdYNPpqF2yTniJH3rm1Ky6qm+qymSJRjXvMEhDTgBdYsJkd01EIyaJ8rXUQpFOmvFfa8gmOiSonLF8Qa76KY/+clIe5SU4K3wglEzyaC6J/Be2jJVNHdtSizAiFn8tynqLG1MQq3sAZYO29ja+MQz8acMK6AxhcLjERpWpZnVPYvrfm5RpBAxUimAiyHEtYMoba/PJIKltn5Q4HD5YSgJcSI0jU7p/DJ0wZ4sJfh1Oeqyk9K2n89kGviNfleH45mfIFLlpaqYTYhYNkrNhz7wZssye9jeUdb4LN/qYACGzBr91P9nWMHUaJQUQHh7MuHndRBearWQYsq1s6PBw7JsQ8q8OachUkKmeOCjmDshwjWMuJ72eTUQosOlKb/ROuwlrHs9j2LYPTumq1VMoNToam95v58NH3fOkmp4O8F6VrldjD3KcqQyv5zK19SYgdPWtGTXfDUn9DV5WbajpbuqG3ExsSNJvZiZF2yg6R8POIDGz0HSMcThDs7D/B05fyFjYN+HObwehCbmyVR/n1I7tvjb5kgyRO5EKZ3N39inw/TtGtUJw3YeTasYmoyPylRfRCllHaH7vbwDB0tR3IINmw8O2UVXZ9H2XelcxJbFi073UClVefbjxW8twf/gj7HpKZdRk2fTqTTDIbuGElpxbbVXLuiG2quoFSy0FOFHJJcqZqPyuSwA3TjPI5Cj50Q9CGd1k1oxudnwfK6Ais/e5nSBLgl8DWIzstQK3wB7MVu4EyYowzknvfvockH9ZaWoDcfttlZqJ0wloZzU2Onhte/uTi80AvV0hJBCLek+orvIZr3tqCQxVy2JHsnjfZso5by+dZeA74DRevK9bEjap3wL+INaG6qlSlnM8Cy/fj/AFQMOmMgNz1pXy+izN8BygLwJ6eRb2O0nRavJrweEfhZaWvPCC0CflK12yEMMylZqWINupSQg8eRFeP94ViQqlTHfG2Kamaq4LKXmNQ6bL980j3y1eKHf8p9d+ydOBRZhET+g0tv7s86Y2bjwj+W7b2k3s1pLVG1pG27ht17+DW8Er5Tja4Q8E7OGUhh9tAL8EjTcMDOM7R4zXhZmSWX5n5eXR1KVm/EHsw2m6yoPd2Z9zMGgezQyXgEdSuj45JYzQidqPz9QDvMe0Gd9DEa70od62062MIg38ImA0O3K31P2P8lXZhVNeyrENmIQ9EHHUBSXXM2TKXY7D/1FK+f29tix049B+/x/fVvvbYXm6Bmy4id7gC0+EInful1bOuDTfzmh2xGw/fElzCrI1WX1hUtzQMO25pG7qHt67s8AjezlikKSt3dn/4lz2BemtLFQCTJTeQI9ESa4RAIvAmVyKCZWkduEYnZsQ1NzhA4/1uvMyTyA+FZEzE2VO3lwcA0bavmD+2RBJmpLwtfBFf/oEpws3jg9V31PDeWjw9VGFShiaVUkgUEh/f9L8Q/MpmIfzzvsanhil4m9avUnDDbPFGqhunTH3dfcr3Wn2VYEu/x3OeX6RW4G3eYtvnEQTN/Rmp7u7xa4OV5iKiBtn/RDmD5bQHqh8/OlN8/BHIgsY5jJWmWbKVhsZwwi8evXVGl5uHphHJS737f0mTY9StnTnr7fs2pHT/H/Wwagahy8y5xs71dhwO0N2bOhmDjfK22C2QdxEIaFOI3AKEfP9cOcP15Tb6Cx7tngXW4ccApu6yfWmDi+Vu7JhkkxuYogmMs+wyO/QIvPItgmXY9pmi0pHHkVWbL6vnlBiKva5zoCHE4i9s7HiP/cI/KELiUdYBa/2Fcs+v68+8Clbuiy24n9O1Luwb8Z7/vmRJP5SJ+r3XULk+mwXzSQ8AtuoKAGKvuH+ZT3RT49c+JtGpgn+L0dqCUARKDZMSJp4if3LfEKKAfF6wkzFZnbmtySMoXF08fFzTgxo3Kmjz2EgnKLgKQoM4VznJw+6PbDSAUlEWK87KgpB0kQ2lrH++3MTHNOlQnZ5ZQV6tJCsAFxa/rQl+1iH4sBKrcdZgYxnkoTiX17g2oT87m5j8OyR8q2p3887LZMRPwMa0vdr573r34OA9KMZK59YLnbGF3g9TiGbw//WoSjWf7n5dz2re0X5CAnqUChs9VH21cfqhGN16rF62eZlm1dgXqlB/sE2I2wS+sVZ3avf07epGCTMfGoYOeX1eb+tbQtovZmt9VlP+MgOxYETlazZxB+3RQ287SD3DTparnm0EQ22fmhCwBdGC0qZC8Ktl5rPvLikPa3soLl6ZGaBl/Cju0SF+eZluLYPiKQ/U5LJGhDiEE05+cJ5ML+W+nEopt7eOPjShTNstRPVTWHYZdctyNCNMmoKlSPJMENdd7s1mwwYhMKORFsusGPXUEQApzTqeBXi8y1YvBgu9IDsYpRDZU0jq2dt3hOXsF0vG6D62pd9XXEoD2s9pHuLfYjuOC/QMMmzjg6SZEvQIMsEbsGTOa//sRoBElYG7uCJztLFxiTMKm2JLFhfYVjoUrLHUtc2cRCkK4DsMEVbTveqpuex3F8C1WsEj7azzxoV7c4kUMdv5Aav3wJL7CgyDt+79JUxtXkNmbdJoH4f3py4ZkXxjvg5QGK5rvFpZNOw0+x87fpAINhF9WwbdRNXqr3xBRiPDN84ggXCqWtVBvx2zSBWoEqSTf7KdWHe3CPuNJEIm5Gj6pSVbPNiizGIo3L6lQVj0sgsl+L14EUBS5M2C1YadPTq83/6UWRBrhoiUgX865pRnfw5iYFdZMr7Halk/TyyEBChSNd0lx8vg5K0byq0Al8yPA2IR2YqI+3EiR60bskSwhHmOGeLwqD18ZHxkIgiI5cNlMCP2rzfKcMz6bHwRIKp1wEQlAboIMMWkTxHXWXlHmTIzj6VeG0iET9gwUtiJdLxzRYgPdk1o+drbY0OXyMf5aPawmGzeZ25Mb41pjTd19DmScbFDWBqyOy4vSxPn2PS3yTYYVsaqj2fJN9b/l5Lc9iFEnJq9tJ4G+qMmuyBBw7u/QaNlmwjT+Rw12ziyqT+vdmIVqOF1LSJ7GVpkRLzNnW32oobjHOh4RrexWEo/WyEh7Zc7bduiYLuzo+5RtM3nD7j7vvG0g30dg4WIkbr9vDMj9Cirb8zvni6briGf0aEQMcRuBVxtds7Ih2ADEk6MNy3zeqWYBGMYHQDMQYyBaBXPto7AG9qkX6Kyf6gcxoDFJRY8++UxQxIUCAhddWKSlBeNuxrbECOKxOaQ4XVihmcrmZYlom/XG0qMCp5qsFYK0cLhbcgXjbKxCQ3sLXMous8DMJaOR90jtguEf6s+Lzuyc1E3icpe1iNEEocVSsBz5oIw5FIOaEOKWMcR0AVILDpEalO3R8+8PhldDjycR4wZzTCZHWjsk1Jv2T6tK/PtjPG4d2/O7bEVYYPIwm4/2bW3axsD45RNuzloT3r+U6P+/qUCOZmNzRnARtJT3akvf2h0XHvIGhcW2aCXS7eIEdirGFEUJu4a/uzqT3F/7wuvTRvADbLJR0ta39ZE3ODRdIGKQYNJirDCb0PECcUhUsGdsakDYB4CVYHfTHhOnkWSkzKHRVQlutpkEGbNrKr8VNPtxSUYh5e5uEl7FNIlOdm9BZ8dU6+uin9JagWoILWOsNjzvhzWBnYYxgXj86FSrFWun6JDXrclXZiLRcxhv6ZpMXrX3C0lDmTcbx8SSUaL18S+uHeV6Qm3c3H1mMynwYJTpWJTXSdQnOznEsGR8dioQWebNfBFGJjPm5J7NAAeMJVIiAXDW5tCtfwmScxa2x6Se8AouRu2iAJ4tASd0E8LnTxpfNrVp04tyjREm9OXTq65vaC6zdDp5QWGYLboRQk/JuGvxAgIZCLxXLOTQvSOMUrMdoPIJxXJp2BiCX7mzxycb/+z++Ou61bycNY5m+LszN+4tEpppzXXfEi/+ABH/d1roSoQ3AG3tlr5SglMS2DSkUHq9qdD1Aaw78mMem74z79UPSdt3vHxXB9PC+TeopknI1DMx0sQdzjIWqUPYjjEt7ldAP+QEl182rZfEW39D4eryr9IekuqNu5Vx9boBGQHvqaz2XxEZ5NSr1rfUEDjbTOnkxVfUy4jzYl6DOjtlB/W4JKad0wBW2s4bjNj5SzgNsotT1rTiAcPgVx3odW2FcZ0d28xSW1wlxxrfr74Qav7tw0rYJkHl5YE3l72dHTcwzf1uNXZQLmo+BVxtJnc0ayeR5NsoRoFL3OkETsGwxe7TXWdz3FMIXrtyMYHG36zKNtQkIPJ6a25DObaMA1o34mdtGiI5VwnXT14gaDbN7BM+mFBkodOCXMP/+0/FMSj+Az1Fkqb1o3xw0TvJJ9ZZcxpGdeD5QcLPgVjIUnUhQ3a0ojj4U6QMKrWUhUDDGBUctCKMygAjDsqXGt0cwKb2KRBBMqBHAjaLgeLKytLRMZsMfIYFROdRMxj67x72WVvhbtG4KhXFBiyICc6fuxcX3rBr5s98VNiL9e5v8yIiA5S0XB/ryESnlvZDM5o9loVizNy+S8IBnM1EGaGVuGEZvFHLGAKeoYY0HMOOH1ox3Xvu7jVNg/f4DScICu2rlmtt40MuOeRJaATNBneIGmrff3eAepB4yhPJbpHc0EJsEcDOoISZPRnZ4U/j2bjnM8pATAPzHnSw2CX0Ycw+A/y+zRS8cvJMlh/uddIUualLylRaj//DDAqrhLwkw4tiaCTo0zgDCO6cAkt4cDJzzjh7AjPOurMOihIkHb842N+7GxfbB24XbHkaWEubia/dux2XDMdB6xBSg728gVqPkMM4HY7wegoTPtXXivkHiBAd5V5hwT6JQqViQ2ogALN3aRwHfBQ9iytKdSv4QIQVggGnpdlKTO9Zxsm0dG+j3+Y9n9PuQADVDrcRKJ3L5d+WMuqSYBJbS5rBG+hzbJFxa+JPdiPdHPT8OpRJvasu8gNPuKP+ScK9lcS4Am2qqm/vAmanLyXDX9/nFjXHtmTh1s60jftIgC11uSbte78elaPiA2eAwSANGf2C8RHVIhmsOmaGCzfikrU9T/GVnIbz8BqBfzqI+rTFHjbLT+6DyO9NkeyMVEyN4hl+HthYQ6nq0J3X9rX8/ivF+7XABq66gLB1R0jBHnoBaB68a6l3WcChH4DKR7qZGretvqXYFyObBEkdsz9lwPV9Vo+6mpZHyRj7V9j4co1wYnTuchvyAoXl9/LUUenD0ZvhJbnm4d0dBFOIwP3k6Pg7xynXQjr0LnEXWOnsWrhvf+HR7UsYi9Mt6xbHAn0QndbW5Rc3dRjGdfE2l1BIHasCv3uPjAC6RSdCAOCAFXatt6cJIPof7zLl24yZygoFWJZKecrIgIFuLU6TObS/cM0A/wzOn/32DZGO3WneGqjxO5Jt/alUm3CnjpmpyvNdEdoyVhDCHvxly3uwQlYLopDKc+8zlxIB2q2tqSq6HKBbKLTBe2wD+gcMq2V3pYe338H9S3RbcHHm93xHrJWaG/sR5P2V2FSmGs7jOcY/aksuShPuoqbthXdJhdqhRGHv02BVpX2xtEsL0+bt/i2yKALi9eXjV3NumzvDc8xnK/MT4NbN3Nwkj0lebN4QYScdmw7OW8h2kvPlIMcxkOoMCjhLvPirAdS6hPddVsnRSk9JnUVkSyfrViTvawgS2XyfZvoHfq333jYkYitQGsTS512FztkRbIudJuVJwb3Y2/tPrw6cgfUpWr3gUi6AH8EcupnYmMWTEUpC0IrzlEqSeIXOM5cPcqgmkMXopCHFg+23nWIMV1Gez65zxyVW+UrHURcBiaCxkeT/VCl1JE4T26zlvoFrzy4UlO/TEpbVUmj750hliTeaZI+K8raHA8XLWCL+oBbNlnykA4ZYulrp/zaHXNtMRD+rLPvk6+8XamGlu/bf2afB/g3TWVPu1uyP22y6LW889jhrfIYXfOLQobBDoURHPJLG0fPwZNlIIeUN0nIGoi7/wXmDZr6E6zxUlGmdhoPHifHX1e96Pr6izoH+04CWEbvZvtn8RhqXh5RcEXFhTrIsz2bX8itE6C6uFRV+J5V7fEd0s4kq5kOKEDAu/ExAMSJS6RPrMuSv5A4ve15rRs9VZ3Yq5hhyJk1d/DvUDZ83d0JAJXyw+4FDG8uyG8b3c+TCsN52b0E0otvW0sLiSoW7fgUzOilLknBmRjGxaTEm+fPmaSDXuoqj1fIoyQRFZzmW3BEmomVlxm10DUZWHDX5Ixs0hsPJosItVp/rWVEnAaZ6KBWjbvSK3YZM6kN6mNiIkuc6MlIImo+LQ2B+PW0+hRAFItcmpypWOm30bkOHvLijJZHDu+RIPQNCwgDRgEpl36f5ddKLjwx/kgDqIfBIu9jSLlXZLvkKGPPJ7x/4FkPJiJPlTf2ISX5qMs1dWzOK57W6tkWYTIphwH78Yqj3eylVQbUMqhOfYJy2QDeah47s3e96Ygi5UKi/QFFlWpTkv8fbFaxUvTo7TJ2AXaO1HcJHElYWK93zpr5WIbpih1LXpBZLGLhujKQCPd7oUgfYmZiOhGtZgkQVgnj/aGZyOTYcm/wVZFNmpmV9ZNUlnhZqt1HQysXJ3kqZcacTLZilS5NLRNbg/PrW2zKmbo5iga0znXwAN0FK08SbADSfO7g54DfTy8FBVe9RgBBJsIzzYPdkHzeLXZe/R3J86FqAjHr3NF/rwZr7lZLQrConSZgrOYeNWlnFJTWbHVmYKcPGJ5mPem2Bw0f1YUft2EyIIrxjBTL5yQPa3G/zK4igSEvlDUIIllPEJIi9N/3jFmq9QCrSwJGfLiAGBixt8ldq0yuk5IRCV/JeMG0RqcZf5T72IWhm7ewTic30kAXTHpdFbfVxAkjfwVhBCGBFBH+9whKzPDZF9CAyisY0aQk6Uj8/skz4KJUSENbiUvkO2wORlkxdrsWD6k+GZMYFKaycDDfQz1CewU3HfTCI6HRdiG/z/mQFj103hL2bHK8RIQy7ixEF9WfSH5auroizviwTxQiP+UDa/pvj4Q7WK7/RZJqOY+MN+5ixF0w4le7fuo0Xmhsiw59YiCdRQewMoqkUXLm6JqS06ZjBX7IfZyyPClNwTLmawWAS22RinwzoJf7EXKP2utQWF8LArE+fZt5GxuFEH3+oJJmTDju6BGl4NiyDgTo2MGylT9jVQjJipT6l4hP+oZA7Nah+TLIxGcHgPbw2RAflp889X0f6OkRKNxBi6S6u77rp+0KlnY7YAMWoVdR/cG0EGr6mPNc7CCEFoDN46hb7oh5TafNyYVpxHOM7r+Xa3CaK8m+qPmu3IKN9OiQ5Rosy4Wnzb7vO589TC3kQEIx1xSxYMP+J1BrD6H41wpagqVHupBnK4r0UqtRS+zDm7BSEdhhg2Cf9vqaMwmB3vZmcf6ZZuiivItkadzp56kLFzouFcYWanfgsle2hmUYwJq3OsC0KxpEK4e1JDA3ap/6ckfA+jCuXB9nNFBJwJECHqIVIlmRPQOpiHnwmVBRfjHHDm7m35I58cezQUoPCi6QyjBTE96DT+BIbczvVDIs3vllBf5c5lxgSRQv4KT2WC1yDr6rjkLhTmbK15TAAH+clNFCjSCJ7/UEHDAkIeD7ivaQTSS3fLWXk5TFRV7Ipuhs95y4JTwUDVZwWKRLw5mPovjfkvnCJqx4EEUuYWscZEagpRZOET1Xozlo6W+Kf5SCrRiWAsED+BdArzizx2h8FN0HN9Lvvh6wzYAMTl0jbuo0oipyhQPJgJeBD/DWWeG1qRWtyDYRpg75SOAzyenQ/TvA9koAIPRX1t35S26O/y91xUo4Y8y/rx8bgR2CRaEMORPzSHYbigw7akXwoUTsQiu+KPoRQT8aJWy2VRIauFIlbFCMJESwqumTr1fmHPJ7Mu4Zuu1WwYoE/HaLIucaYsKs4wru66weChJrqneYbgJECKtxgxylz2zlKpjtDhsusl7DhWWg8VYcIST3qVTB4xXO6GlQA1Tn8R//P6kuS8A1KILiYqKoGn2c48CCy81hIkcRfjVt0V0HPXyewySYnPIfG+XXqTaTvxpmOSJ5Mh91B9DKEqXVCPdjXw30zaAoX42N6W8CNeqn69PY9pHZO5Pm3nRH4IhsA5DmWxviry2HnE9OeC1EEMcG2qxWNbqzvC0sIDAmxRQCUQhOLIsyGtaaKKMkqkXDSzzrj0i3GPZYPxrDHlNp1IE+IOeNfw0jZ82d/np1lKhesyiomr01iDeRJxzZoDlulGARgt3zjQKAk3Ov2JJScmM6XF5FEDKavLjdASySjFF4mxhDMbsdl0fcRaChtbv1PUYLSnY6i0R4HIt55SJ9F++nP6vivs36dctYXJdwCmV+rAtSDFeufkT+FP1U5A+3QYVVLwBbnlc1qLXcFKxMXdKjEjKp7b7Z7dBvaWM1nRIPNeObUGkKwwLmxF0UEDH5InJ6OV1qsibEf+5B1EfuK5Kmp2CiPH1SoimOtvnEgs1jqvWK5dTVFUrcsHRAyQsR1bVbCPEggAs4+o73GxgqdZNE/RiBjmK20QJtqiJc96STqEeJejjA9Dz9pSsMWMk2uCGhGwi572ufBM9ig/ayR2vElWJr2DPahXS07XZzfXOk0v+3hFtIYW6PSoE3SPDgdXxIfN8i2ge7pz+qOctJRhupV0fqLfJvLuHif/pkdHjPKrWtHjiGnlKjWwxAYzUakBiOHb8b0KCqAckMXCWC0GiuL7GDjcM2tYqC7udsVanv9LRdOa9iDQ6i/P5LbKEgdSH9Q7zJYmuhb7apXAui8uvZTFipJm7lYpYb80tJyNoYK30Mm83Xm9+mwm/kfkxfdE34ORE8cOMamUOSw8XxRNEl72qEaaN0+0mvAOE72981P94B1dJi8MBjc9bcBdtBLbJU3AWXWUQ7fTao6Jcem4li7Ilx7ZmZkqP8wBcNji3mH6uJEuvQx47i765eBJOeTwi9xq8dPFZvzDScXQ/hIhvCnuFL8Surth5oZNSJ6xGY2Y2kI2Vuixhtto3qFr+zmiCu+jkXD20wL1ksQaNIXURV/CkQtfkUWcu6taLRhkItNIkYwomjLIroALIwc/f+NJdYb5Clj9xDE8mMkt7Qv2u56R8UnAq9uUIIZK1MUwZMDji1MAYW0Tzq5Ecp4aPuWjTAarrUZEHg+5rED7gQ1anu54QrfAP3aKATa1xSNNylIVE570jXKPskfUjlvDz+xd3CASoXTONtjws+45LNpwg7tZqXwACMYQlRKpdLuu3QrU9hmWz1JKh4xR8PNu4ayXKU3mjZRPoMhl/dfKUec43uaqZafCuq3dWkGevQR2HVzfqzmPfk4SbA+t6t103mePhh414LdAEbom1ifjz9jdWFCxqGzQReMgw9OsQlBkU1kfW9hDt4Au2P+4OyPefR7KFcZMdgi+18g4hLV0pcVFNF0pIwaX3jExR80WrWCAjHyfXr6bVXseFEsgGgPY8D+YIjVCFTdCughGqNJxDhGwcxdfc8oWXBABAmZyOEYdKMicWn53R+WNRmZD8mh6uxbZAm00hB8u545/bforC1p06zTtGokvbRovvVLW81k+jZ47qIbiAAqiMaMWaCi5hdAVjIYj0EZCnsKvfv5XV/zmt9YF9Hdr0ycE2DxQN9+5Q75AZDBtKAVK0DmaQBGS5VetGD+c9URY0bT0RA8TNi8Lk67WVWFrfxwV5kdCtT7JFovvti0Qf+AhGK+mImqRfw9/YmGaqIFNTfD1UrXUftQr8sTOUUFYPl0zCU1TyONQxFFCkcsgL3a2P1v/x1gUgmyzrY7QFZCFHUsUXDCkCPX6kkt8dCx5G1dkt0Q5RnBQPT+eVKJiITpmiM4qCwrtO0rtGukA3/dSbi+6Nnm6ipzWj0WY5K6qML97pdbpzM12eiMvFrzBKWEJJIiNxN/kYiejGzsaZbzvTJGB5HDXCTrVs/livPTViaTnR5wrpXJ/P/YHjz6zPrWngBh1Z1ni9MaTiE5HFhYriD0rkJrhmbquaJH9EqIzbUqc93R7NWW3h++CIkbYYavFktkB0ZsiVHkE7gTsMWLk3vg+gppSinzAis9yDMkjsXgNwaBaNWF3BqJOzDDn1QwF/nuwjg+gv0Skp+1HjSoNxmAEg/JMs7vhzvPy4cnFhPeptqFEUZx/wejCum7SxbFPKs0e4zol9QGgWQkGXsfqJVernIVa6Uxbp9KjRS7fyfswxZWo2gn04EpVRJCXPUQnRoulkdEx1O5k29ZKexIWq68okLnJg+wzqavi13gTmoEB7hR8gyskZslr3Bx5mQOwTRHN3USKYd0ci0AL+eD3AVkzVso7pF3fTC/4s0FAcE584kV2kgmQUeKmwOP/ZY0N/FzHRQSoxSBuil/yf2WipkGUs5tXbaEW7yAgtUC/NcuDWAlNiGSC4RyWvsezRkmCwkRKLueselbQHhQciDAjiDo6lI44SPNWSUGUfEQlDVhA3ZEY0WdhHPgZNlKTIPXNSNbce2JoQulgxXQMoga1ZC5NsDHYsZ9iV6FFxMuS1I1wsDsQIMEkyp9Ts2Q9OFAk8E/lF88WjKhFlA5AsDJIxFnJeUZcCNolE9KUgNz+VbAjBL9QYnzhTHTksBFvjB8TdiTrBVj7FB06YIdcoqmYgbYXP06TviCTUUcbwsnEwj9i2XcvbNqUUWrS86oakcCt8TPq6oqbmA8s3WeFRPZLQF9oI95IcxAVESubbKDoQl7sm42CSZzBCWUrJhtYN4KUJs5oFqL43VCSX8TStgLyIxATnFMrhASVPRblUtmGH1xZkqzs6SmzWeSrtwsIUkrhuDYHrCle5gVBCLKQbWgrlROcJhQ9yqsxvVjOOmbenUCE4owDo9DO886xmFJzgu6Bef+hzm2W9iWxvIC1y+E2AYvhPUiUmbs+QV912BsSFEhULqNkF0KYqUZaDhfzyMNKOrmdtVxZVHHztuWIiK+Zw2qoU/3OXF25XMsaDnPnc7UzPrthC9tIg33h0MP8AQUuEAZ6roafCSEusEAVHWbJtoGIEdP6G/M4mViCf/5Q6himKBtWUAq7BFrWfEuVQ0spmYTldTysEXoBJTSrghRak1EIWyGLUa5jSUjJ4IXnumA1dsE6VkgY1gSwiaeZpbSjOPy3EL8wfLLIGVTk3OMIxwFwhhZvUk4Fs49FIXxTH2fsXQgh1x0jwT6Bnok9jALgSKJRDi2jbFn1FUlwERlpHDW4J5xFmCFD2WL2pEHWBZAaJBTnQD1bYs7QuC8YNzvqcfoyTe9rYCBJ7kM6O7BzlXMxK8ttvUW8xLq2SEP6c3fRAEZAuCae5gj/jqHb+PNMpxvnRMLcABmEuYKFB8y/RytOA1nbJoyIDb6L44RKDBQh4lZT5ZuLGdnE2hKNJXuA0hYrIOyq+XCm2/fv9X66C7tDx8DhSCMzAL0ULEARUh316PYZNCvuU4aTd+4C0zR13BV5LMeFSyvlBL/LbuC7e1OY/w4bQo09XAoNJA5Bh3zfgopSJ4zE98rwcPbfV+5WWw5iuozClSc8wYWObn06QEBzqu3OY1Wkmg+4UvaO4omzZWlZe2uDSLZ41oByzK2h5j7lCtxJYGtcRIi5ANrnrxSqpMV+jbcYsl/Ix0bknoj5IAHqe8sB9Llus5E7nAQSBAeU5/34nOpz5JCLbqOC83tMeDYPO5AJeXdDYrwhwrqRVpU37zajp06kBmDQz7aHYgu7mXBstKWZdGnace/z/Qq6liS+8RO7bT/fFbIynnLky/XPfuYr2boA5fFqIEZPnNRJLbK0G3d6tLkelrsrve+cOXVWHqQz+S+X+WSvcj2eIeUWQ+hxnZyQKJwGLrDpJYwe/CmBph1M3V+EUKCb707dpbSr4k2+F3H37lWAZ8WmNW5bVBGNgBz+pzHU2dvBvxAPHHBivnmMSluJ9WNYGCxbWfpPa6DW87rzHpDjfmLgFi+k14VcpPTp48phyy2ZdG1LxVy8sbAWgR387Tf+ZQxaRxf/DtBheaaoUdapOreU0z/TQdtEvS0q1WiGmZWGZCHtNqXdJh1IVmpNFYB1OVgxWdP6ilAl3YzCwYVVp2/QULXRJH/7YkGRrXZV9Lt08e12CyHUKGzoDRRMwbnJ82SI3UedY+RxOdg9FPZRWygq6shgdAfAanud5lCprUNDdJUkme2JS3oVc9JJfVF6cAFwBmQFIp9LhbsfjGZHkPOlqJISKOBytgT8Qd3wDhKOWhhUqWyjFb9p13Zym3ApKeuZQGzotHCQZiBXyeTeOoAygJZDJ0UiZjd0Veng5FalFn9QqBY+DAMo3Z4Cs4vPs0pg0QRjye3+5Su8CTGBcFQlC1cJXvs/Q8UtAefN9L2osLFQQgRbBdLMn1LSMd48AW8IVT6FdAe7zkOIMKWtABNxeCYi37WWXyydTbSb1nnycsY0bYjRMSY2qUms6Lv8MgLaifzgMC9blX2IErFaSLHd4Gaci8xuftxoKZdHhbjrJN1M8iNiPSMFtnvXckPSsJEKs+IKR0ThUYlLGXjgec6PRLsuTqUhfB8XPZjp9YBrKVwPOEjL1U5LBO3FEpTR0bjib4qtHTJVc2Yxb3VxFMc3J+Spf6X4kXVZSNBdMDhTLRfIXkYOhIj7T8+WJKF0/40IEnhzcCcPLypzLTY3gxKBQnPRqU7x0ujI2UwL1me31kMNN0yeMo2kaHLFvh8Rb0mXhOCqfOWWdj45yczdYRII9pJnowSkvEoBWKM6wKBr98QHattar2LvFuzgdlPCVcqwUdJGEEZz7j0kUYBv+7kYzyDx2xTMKXSvKQEkXOMmEC89hHScDLrSHcTR4CUK88Mt+fdiZncRJp6WmDaWtcPD+UC8mvanTdrVgS/IVTfwrHckK84itCqjBStrKbbeECrrbFxqnTaW+EPeef5SomLBJpd9HYWbs1iD+RwjHY1SUlEDySttWnQNBY6/bs0L1sz3eBHoqid2kDGHJ1Iik4uLqwQ51R/6lGa5SuwzKAVJfuRmbUmmZZwKO7V2gckOc3ZTAP1B75cTuJA63dLFD7WQlKiic7JvShbsIqLls2oblW3+YIJCpAmlEL9uCvDRk2SE2XsxMNIneB/MoI9xNpae+K4l+N9iuNOoqEtbNquiYJQm1UlBnGGAdJXAVYfeBVCRyYNUs+CG//6Or9GgG2Wx6HMQxKRl85MPXigwwz6s3Wjeg9O7yrsGVkv3Q0JprF/+EcvNwdsdwu8b1QQI6RZi6WjrWnDyz6fzlqNCeBRSw9E+AaqYnu5nbEXYNwYmVkVdWyHKTuyhq50p1l5UkKY3JbLW/wtynCPk61YM6eYo51ASFeRph31zjag/hO4h6NauVKLbUr1rp3dWzGX9uo9bTOGLfK2QoCtjLuqFi/stVc5rVLJrXbA21h1aJBOlSJXgX2T3fWDF4Tab3ylQU3t/gIhzbOX8Z2nmy/cuu2kmL3AhYAlLdA2vS8EnybS3gQc76hu3VpjWvr7q8K11MqR74PdWyPSpUcRX/EkBRg7bZlSVNBn1KXaO3HQzDhRBXEYD7fLp9lNa7YjxGO7ZCKa7KiGOd5VSxUIWA3h6JAvBVEwgjCUPCKwV3vS73RWF4lwIzhx2VdrA4Btk5sHdsGoAvpiSOyDZ5TTJhSOmGuO5Mc1PiUSYaxTW3nKhnDig4d8ANV0uB9Agh+gsnu5wD2itoeuK3hBFO5zD9G9p65tE4AtTmHr4ynC7MLvh63EN0y1U0zOXYgY5aBntHrqKIslnEkjzcLqYR1Td7wf/gVkKJGOZfQEAdKDh14qovtye+6SQybytWTeTFjC9d5YaWVMdh8ETzYOwtVd7UIql+WD3usKw4loFwQqCa7IymQY8blpxY4ejJEaYVqoSpg5QO50SIAnHoaFJf/82//NVSCBotXii0ToVYCnIT5Cc9q4+GVPFJgFbxB9gIQmvdzPlVv2vY6BHlavxZB8/HuEHKea/lD9TvdAEa3kZcIKItP2sg4rbgE13B1hZYfgzzmWpbM9wdMoM9zMlXpgyh+o5+aXSHtXCvYEPHFsAN+e6r1do4kGwa6GC+rB0/kv/z0qoS6tbBf+0pxJxBcI/uvgM2KqLfSUJ/lF1CTlG1Kd5nJc1eiG7AUoAsblAHzG3mQphpMwpRhdsU6qIxkkZqiwMzux1c45h7hjXqS4wIOJaqy2WZPBxDnpQkbLcgkRQtEBt9j4oEa2xucrb97rY5Fo456qPy6yXKvLtQVtWU3LHbyAbJ6zs56loEOPohtFZJJzkJzba8IdpxEx+Tzy/uHlQgvEsZwzIpe0Z9H6/llYCu2wXbWWZl25sVxcY7z9WsSM3S77XURv9pyE7UwyXNW5FvLCmpItWkY4myUpKiWPS7um2QNqVw1jB+S6g8xf9kU6fqlidPdQK5l1hmLUWcAlffWE3KmrQE+uHVuFAKGMaV4v5NSB6CHJCiblAXlJjqJL1OOofGPBXn5aAS0Qibjjmfh558+QTeGxn8vjS3De427LY3IOzVuuJIsYL8FXsCqxJpYFc48MDCtsGRVelt+mOUVUWq1QTJAP6bv1l5e/dY8mukjb3XI2xygA+ZKor6sQE2voyueF+JWDizrUZLp5ubAfPyfNUGq2nNXeNDLAxFDI565HkDmwcVH64tFNHZIiKbZ/92MvOHs3QSL4fyLU64QZUNaFmh57BuXlYo9eIw1snFzd1thpFDo4TrWE2ZmlVSQ26oCrpFlU2D6P5SFYbDsKY27IWCJmEky0XzCmEZ1MU4EkApUrJnXWZXtlvtZbv8SLZ19x3OIxIPiUhmy89NSGC+5OpdkdtB5HXHeBGNmiD3JCrX7SK4K/Y1eRvRJztjX8ZB31fo6AyC79osu5wVB7DmxOqB7j8VRTUlki82hf4/d+IIeNs6MBme6FCVgIBJaobQZHL5GkSqwtPJ5LapFQV2sFvZQqFRyJ6ppUtVSL8KgxCpaaHqTqU9s0Z3FuD60zkhJCWClCeb/advfExryYbLGIrbPywDmY0JsRFUqie+YZMnZ0QIlW+c2AhLfWAb3JbnqHc9NUh5rd+ylrFEbpNAOk9gN4KTNHlHY3oXksoK3L/fKkFYe/jXKNiYbaX3GxMSWfEoEEWNI8OnZK0zNoIvsDEDQmKbAEnqZwS8YMbwABwBIn65FEFkL3ilvZFEXBok/gKMv7t+oHT/f2uVArsTPTm0RqbZ6NZqw4ZssDgKyV+ADUhqmPHVZzmUQmr518ql9qovGeSsXlHAiHGtIF3VkjGTwVcrKmpe14B0r4QYcEcR6gSjvlbQM81MTT+LxWln1XJTrBGW0R9d2KRBiYeylvLcyz9dcRTgm6U0tUjJNlvJLXIzkKDT2Fwm9iSJ61iyfvS38+Lc1OErvIXTtFyhRa510mzC64FlHls1jabA1W46y1xu6aRqFFyR52Mncrk2Gclu6uS0BjKAuLvTOoG6Ly0nivOwfWkWf7qVy5Me6aJ2LvmrfOu2UbAXklHl3d/47W4bNuBZZbpklQJX5winQhj0JBYs1Y0TVMMGymzsPK95OZAjfZhC0bA2flYRwdcwz9T53w4GPf3H56+dCMz0qWZ+4KTF3kvfRI/rmaOU3TIyiIu2zstCl5CcoOSMGhNgpFV9gCGqhosJMk4yUl8hlL1A8kyoQTXtH5p5tAWa8iy1X3kPY1LtaVbCBwFdf2ag8awsboi7pwMldOimRNodsu5MKDhf/K0wZQ5NSpAX0MzLL9/vxUKyEgko+rLoUuL54VjTuz0b3rlCkll6rTKOI99vC+x8OhFAUO4DEkuUn+HkbIc+Au5urB9MOAAPyt2ViDDCKOJgSYsuWrUFvFD9sD27MNlBoC/83SXh8+7dmWBS6ssZmPLpfuwoTb46qe265lWPxptRNs0E3+487olqTOFVeVF2UIUMqsBmLoNEFez+aIsozUpSrllimCl05SD/1iRP/T8lMCV9z8zDJgeTuI0Ka/8C90glEBkuKB9KzaulAPn17+Lntj9ChJW3Jn/RTcSHQZO02IlwUDdF5WGARoKVHXjdb+MSuCp/DIN7nKwGSY4rE3zbQmrTMvFnOJT1dODrTE7mFLn+xo9SC8X/640eepAUbExwhj/TyH+EMuiUtTmwIiu/qR4lRz9k37nRJ8b+s7T6wdIReBU8jaKHIYAlBdFZbCE9t0tbkqlTTulm/z+W4HH3IimfjKvCvwJhWRzjBXAk7pZ7SJqKveHNhTEpye3BbtSqNREyk0pu0lsO4Gx0rvk88ES3M1/B/drQTibQcQbtP0MByQGoz8I8HUuvd61jlpVS5UZ9TWCR+TzDoDnYWZ3J0dpZH8BTbOFNYWcIjtqd39pXvtO+vThVotounnd0rTIEYLdJTosqG5ffjfMW8GnXP7qBW3/14+Lm7pqQY5twz+HqR0/A4fDo+8MPtKpj5hxdbKPeeGrcRx8L7qTSiq/MpcSiiDPqZ4iMe87bAR6NB3o/CQ84F/8ZY9f7ASJB/6GksbvfKIk15xcpePDd+p5u74i8Me7d0+baH7Hd+LDWiztSQjoczAxlNSO1L0DHxh3J/MZ3xLEMI/vUWyuFOPfhpqGyvoa5vKPLvL+f1hfg2IgDw6L4F92ixt9rEq/nMnBeD/ZbDwofeo+xXxhwItOh6j6Yi5MJVMBkLQCnuIXRlXCTFrYnWswU88o4D68XZx66RWdRIXiCqbD0Wj0CDCESUB0BwS4JT5pc32pN01tRYxKMSO4SREHYAW41Z6HS25jxwdQxPSlpdUfuTmuYeq2se1MWOPQYpnY/Xs7kZVaZcbMZhXyljNwDBFgF0w28DkXgflq2aCDLh+0kT1kfAFULgHYIBB3SxbJl91Srao8OXLu34z0Lfa5tUXh1bLBk88Et3r8RupmKRqsVgzEdj62pTDXFtoxLEmRVzWSWlGoCZkffI5Tc1Nf4V1xyXeHVBudAi7ujnE0BxTZRw0M8E9obxrGf45ZNsMxSDMEFmfkd0pDek02gUqcueokxRcVicRM/MEb/vE04kDbu+LFnBIHzerl/RoujGBw5/mQ5V4MTwYRYF5E37SMCI90QP3PIZfusoGZwKUX+MLZv1xd9qWRNqT2WfOuY29aqjoUVopSW+Aecn7tTqcnHMCRisB1p95j0XhXZS+koOroMR3sQmw6HnqlwHtHMwIesSMaq51Ur14eu8EcwrGKQykh7ezSSGY40xoiYaGNNWZ5UoQmf8OJzN8ifcwEwDqdF8puK338tIHNErGnkUhFCB95YI/1LZo5fO+qgj6lrhtp7poBlnkdgRJb4oqs2mylBFOuh9X2u7AYX7tI3ccyXuSDmNXMNgYvDrcL2HYOpHhn+DY5EGwZELuGfVRu6Te1q43vy7zscDgckz70Jj5sFM1M0539On62pERE+cIS1sR9LZaNCiYfp5MndWYgS80kHUWXX4hr1pJoAbMEuOAWogE8T6LRAZIvKX3E7uhRrWKeLgs7MKEXbJ1M2JQTxe7Q2a/NV639r0/FwxljpZOpgSZ2fD8ca7mPwFgMJKbg6xxUHwY5+v0ArH+Q8OIj6hadhB4+ZIwqfjODOiYZHIIPITyS+7zxsr5OnV6THmA3+G1uBSMdBI1/Y2G0c/2De2diYlmWAiH4X2Y3OPZCM+mvf9x/n4eCmPNpuRaUjz6/QESYv0f6mpHqfCRyV5zzn54GY/KzX1cDczn9cFqdmuYbTOqkGqeqxk3udIsNh17thhgyu3Zst77f26Dt5aDo/JV1mDJ6Nt63t8C7wbydfyGe1lRzaeFGwBWvvpY5DBPDItTdsuY1ps8N+c0wYj24zHaZrWYsV0llG8Uie45qkGuK8KeGtXQQXAVxvqmFU3i5d3a9Fd9Cjw30Zkpu70aCy57HDvkjQWYfPjLsgDk9LwopfWiy6K+s5LilJUyGmr++yFf/JnnaK/7S9JT/ZjaKoJCLdAm9XNqjkMqlvcr4FOFpTzC3AWoi6q1ZcdtGLq0acL9/p8LGEWi9M0UxBL2hBlRSKUbLpCfGdGGVPQs/oo1VlILIywB4sg2tgUZtTZx2WEGRczG0E1oFVng2QNdzO4ZWkdOxJDbcGnoUr/Jcl9ZMcHx4e+54j1HnACtEP+ewqHF1XMu6oOern91hvR8LKNVvhI/MiTsQK+9UmOu6i0J/LEwj5MaxzPz+mz3zhwV1s5F70s85vYEG7aiebLBknBPBZOWbXKxlP3yIMa8Ihgux22l9fR2Wba/OKNVXYPYJd9Lt7k+f2gCkuWVkkl13x7PyE7cmCHZZuAQMWLFzhcOQw0Vo6pvjnURjfIBHx4IwDO7b2jdP5FlQtA/u1y6EMPwcdwbnF3P8nkP51i8ELQRAqlRkDUKB559K/xEohEPiHk27n0gRcdg3bWSB+9G9PlXmbPgMJkfc8tSiWOHcGEmKvHShcncYEhEfTuSeQv/1rGsT2CGAyDAQp/TGscKIEwh8wqwaS2z/7XSdgkg7kCcNvhRlCkJMMfmFGYtHEXJg2BOXUSyznYnK3VHSFaUhQTp2+Q43C5kO2QCZ7hP1dwFb5b1Jt6OGQmCvZMF3CbzWBxzIEdDGcGFvszAex/rTI5ye0HThGbqa22OgZn2prgAB3EX++j1J17J5fJ6YxKCW7Ksl8s1gidoXxEdhXIYaLXKtM3VERO+9OcO8P9INxacqsV5NsqjAAmG1BUUX13FvqSJ49wwL44DcRFhdJikbHZYv0QvGlCokPuSXmdWsdXTwFtg5MAABZvi6t1CS+OLw61bRNlZCkYK7EXZncwj2vCk6+R9Zqd5B13dLdDXKmNR2OM++gZs8a5S/8kF0Rwf1MRtTv5cN4ZYVKIU5Cv3wb+G9XSFU9Ujc9HciEdwHRS/1sg8Nj5XHzqTfYjF5WZ54HnI0CnsG25Ak3mP9vnEL1ynPUudoSu80osXKmnzgWMGoMgVuhTcFdTroorHjmNiYjW5M3oQd/Iu2GBeR/9dZcLL2qraTbf060lulBSYB0J9HxobN22ouPfLh/FsUtAH3TPOPY7WhibA4xwil3SiNTRhpKQwpGKeDMykg1Bv0yioQaYv4lBbjMvlBb+9erDd0OOdv0Mh+/12gRavRyZXxMaI6RL8vX/Z5fUBBa9/m9Lhy5AiJQc+iUR/nG+y/jkmqRn820/6A2aKNxMTMolXygmQVdkXiDkcv2EWXgNG62JrwToJl+Jobb2cgcWi/48PewoyLi3nieUZKpaTBfv40w4ROzAAMIVhLTwXD8MEfIKAK9EwUcCtUqs/9CwIQh+Epm9zEO1mu4nB1n5Q5n1HrlLsQQ+nLtybeUFydfuZzDjS7Imwzj2apaKUEgM2sCaPGjmszeYVaFk/zwGSR48XUI6YxkkHwtocp+L2+yPTQnuxg/QDoi3P9uxO5syKBO5vN70Yx+Ze9M80WTHv5vYtoKmfYVVWp8Y8LuOGTzxv+50ATJBnu2fBXEjJkaN+cbUbmYTeanZUDC/JPQPm9KBgGcL6y0RjMtwBhbolJ9ulgJAjkvvtl1ZdFCCzzjDJe4YJEbnon5vHPjgDZwZp83CqPh11xUr8hDyKeCxq5PoOxJdX0kJYaAaLKTCe7SYs9GlQ5o5OQBXxuqXCUzR//kotq67LTgb+oHz6iIR5D5F8EHC7a/ko1O14gbFTU/o1fc0rIEAnEEjeEW4ugSNJzjhiHHdCu4HlOJcsH5g/6gWC+Mithy2fBLuUSu2No2o5l7XUJfhms64kOm2OY5BQ4AAoTd4RaRHzgcCAzuiIvID37xHgc00JI+ztTSs5xfjm5CApheYJxnWyTJU/16rgmywqM/M90FT1tVkrqswGHwq8CKPVkbjJQMjqfzrwJTgESNPUD08oJgnSbai9JAWgyriODDPveeHFlfEWGswhr8v6MWZ+dIMMTDfPSenF+qnvvnPN+CCXmFYkR1jcRlG4IDQ6hDeCBxo2UqCRlKNlUCUdlgw5amGABsQrTiMIBIeLMl9TVvIeOlAf97aX8SplBP1SrFY9OkbaEP/38n9BZujpKTNvAbTG8clAEJdcJZ7PXP0fDjgjvK8qEv1lLg8LUnW1ssq1daqeLRvVYVbA3ZH1gq283L3POKzqGQrbTZufp6uUVLAr92kQSfUU7R0wL3ErRlnKa70cDgGU/ziGPdqt6+eGtyR1VaXdpIz6yBiHI80kMqAdiVe22pSbPiooPtJfY3euMv8GuBHGUprgVQCrfdvPdZ9BqajpiSmniD7W19mMW8Jj5g/mCtrMR/PVkjuENwY3/mevGmheNlVmAXoFc5XY9ez4bd+dQqGjNEzOShB2Wp//pO8y0QYZ0hfD9D+OcUYnlvC+0xTlqLnmg4+BHHIq2lKkMBFr94E7FE/bIOGk8ajddHS56x7QiXAr99gBZEt+FSmIHqiXdf2YshoZGyyy8LUmGBh1SSqLBpIBtmxDTXR5N84aq37uqTUnBYBFppWFqU4+tSZ2nO561LfX0LloVDtSUkt/o4ijP7dkKs+c3xTjRfN+WcKFilOtinAZonpbbiOnONNuMqpwxo+63j1mIDrv4MbcstYVew6YK2peK0K67nD6mMpA1/k5Oe7axGxVAI2N64zN+YjXwj8r8B2ee7Ux2p4Gd0XV2SrDmhc89yf+Ce1wXTNrWS/kCKG/y5wioG236CaYTTyeQkcVqZEryFaeqWElBIvPjlw0jB4tjfYLl4UhTS0m5vu3RMlnp9dt0+sAI8cclaf445CBheY48pRRcKs7gPfbF3pTM/7+/T2PpUtMFAbAtWuOwNxjFZNm75TQSarBF54m1iKae33hJ2y5bnD1GWmoPeU5ZaUAoE3zPfgkhX+RtLjBjUVlRbucgAw+KqBSPVNCEk52v32TzssHyPiNah52orkQIddKcmnaleAlASfZRK9LJYobpWdp2bV+xLBEAKRQsVYEkLUG6PRarJBTcQIdlAWtlM/EK3AGhr4guokimOU3peQmMnolHiGHDpr4tBviQsnET+rDi4Q4JKIw/lTasYH7k3V1J5ek2LtgfkXaJOreOfdhDLOljl98MczDiF2M3HJqeZeF/sdmvgA38WX7RHdNIC3cDJMG38sdmNiwOpqq0T2+cLdmILbxu9ak+8pEqFCdGcv+OLhnWjlEJZmoN0ubwsWiRUVawW2lg2cLLw3oabUSYBjqfdZyEJfS8wQ5OYR/2qXKb65/7NKJOZ91dL79MZyS78QWwySvf4fxP+9lRtnuxtizYZ4yenDrlu8H/Wo19A0gk/S+wUzB6YzllHt7cCDjMR60pp0webxnoDg4GUgUcvG45ztj29Njls7/mbG5YtC+tYLEUXNvK9BqeCZZaQVPrsXR8B0dCpYaORG8QoHhVMo9gQGh5GJ71lDDQfg17csBtvyaDCwDFgImgCDpFpSgJkvjkVCPpjr8XYofOTiVFIfjqpDX30p1foZ4zgiv1ERfpXe+yaIP9lYB3PcOVPez3Jrul2vD+3C9p8O968ZGXXCPsX4LVC/up06/WBX4Cza4J/zlefHWD/ghCCSIlv0W8WsP4WPCb58n5CXcRNyJZNNVG9Bqenbug75m6DBmXVgHuJrG1hRy0Fr1rl4DYDuK1b3ZYot3ag28hPrA336Km8MRm75eevCbqbF+lTrPFKmvQD7bLyykBLA2WVNOE3WkNHSP7gY/4Fvr+gvKCuWUxqxS9bSAx6ydDmfqT7jPqz+qP/4iZXpbKB6+EZNb0aEwNd3UjZiJcWdedx/bskTw3NdNFHOBeJFXX99KnfaoYy7KOZUEtc75qhRud7iXabwg9eJNpVx/rfBUB2bwdtfO1ATVO43TxLKnQzTudVF0T3oUK7z6A8cwUdtoLBRu4eqGXxziOZOW/HJQaRdtkp5SmSu6fy65y56yJLBUPUk4osDK6i1D35cFC4tuHw9TCRaFUcvyUo6Ur5m2S0lXEZuKEF4PhuP7mQj6uHXAPZ+xoK/8HetdExq1preNm64EUFFppDiXWhOfVip5qTluww8Nw0JmN8rkS+mhs98LtrH7uU2nvW0nx8M2tpPiw/vm7qotuozjGkbDTjpazAOINBx9wpHPoGFWRlRq33Gn7nPKyXI3dwXtfNWtgb3aQv0FGeUOjeCS2kz/cVMncMILffET5SwRur9XnaEGHT71OJ5QPNukXMsCpez+DYnyc4od0AnyLCCkJ3tqJBSMFF50HDyvvkWsB7HtZUYKaEQ2fI6DbrAf+srM0/L6zN7Cu9k18/CakwQCyxLtBvbFuuE+5Jv9/1grUZZl6pnPb6KXl/BqzaNIJq+BQofxn1tgYkzWu9tcEewqvpEWOopGQsj6TLZrd2ZZvRBpKLMRVK8oby2S4oObDxYeiFI+J3wdpl6Z1sKwcnfr30pqpCxz90BB2P/6qOyR52deP8rJODqIyQ2qBUsrxRkt8sAzMj4ulGdTlNuVFLTx0Hj1ltB0GbApylIL8qwqeWp5cR9SjMmHj4ta3vHewuIuKmRxwWEyCEWP2IyyJZjM8cedglXQDPURoG8E6JGQoQKHZGBiZ/zqVzKEcTnAfO90E0RtkHciwxDXZwgsHtPtDBgd0UYODN8zG8+ogWNlA0Bj94xSK6HBMYoxz1zyFWUhczwCe4iZ/gr3KscwxdZTnrqesJ2p0hZbZdwmfImpkSb2DmcQ/rhQwsOKx5fQoZaLNR+sR8vj2Dk/G5l0NkzXxRIcG5hi+AFvgk2pxvXYN9MctqMaRG5JHXYxJIGwh+HD/p1pYxt9zJ/MwtHtISovdGjSLwaakYAoWjaa5guiNK32ySM1CcWvnKh5ianeQfvXV/MxWitx2eKVMCHfsqz5lp1bAnXhGu3woDlwhhP6UctyVxpk2vELkeejR1Ahke8pdzis4lmi/pLGelxKHOzqMo5zfgsRTBhmR5Ed3y8sWL7F9jsXonCqt0IrMZ+m1SAi9bgdaagjS9bMJ4iY2dV+n8baYV7Y0QykREwBU58ky25UtdhP1X67CGVpFsyCOGyO6GNDKkf/Eo7wm23fxVgXjU71//nPJ1qeX08OgvtmnIWeJiTSEtjYEzqaU/UQK0rDiGzw94tCQcA8hNyAtRPTXhFdbRRU3UtJyIsT/dygVUZqNHcwNnNoeX2Pp8CWaFLFPh0lmpj87Hxa+D/RAmAgxOUUzEcg2siF2zGv8oLSk5CPe33CzU4YT+KP81ka1Cjl8Do8J0nFM7B15xS7tiW4S0v8goD9R2dK4xIW0uyHwiXGiGqUow2XH/1lVPFa2bWDnKJokTT3MuZaTuKvqkPZf88lu3l0o6FWWFUR4M/IBQ0/mK4pmuC3ocLbv5Y72/JIyJ0ZY1jcS374UjqCQZ8jWF7QWxvP/squR4371w+hYXC3JIDtup0wLxpHgxz6+EZKJ7BiilPASTkv6kuPCfYQMiuP5GjFKdCshWLvrkg/fC+dro1QPejGwGraBHB6vWVKKdyWpqoDzLMLwuo4/UoHlBUnQQ+1SwWopZLAgAKjUu4exOZcwoh00ohHC+irf93nG2TUORdDbYXqbBo0YXZDO8r3gsYRoUdqiLSNxirBt5XGLXDIymCqqx/M+70VbqNFQPqWYDBXKKjNGUMJ7bu2yhl0lKBypl012g4l/3VrLdB8WVew2nEWOviFvZjRdPndo3/Mt0R3A+2R/KqmDevTq2QRllGxmk9/killOA0L1YwxgwVMV8fURiEPYx8u1vw+qgajP0WZ6O8GAAxyZCeySPxLfcjjWQayoV5LnuDiWwHueaDkzbO8Tb5+I03DBsVWH7ay1BRwhjAyv536YpJEqfSh53YLM1fXC10z8t6AUiQnObrlgjAHPBBPLlknFSl5KeQQxHWOWJ+k3HL3NnBQxuFKeExcRKIw/EfLfnsYsP6pPVsdF6avjGuLPHH1lr0sdt091SYcv/Tc6VbNALhMcViyxlX8tN7/jKshIv3RBn6oJ/n7M/FQ1ejrySshFOkAmUTrR95WmFuHwhB0du48ISarLF1qS3TLorVuGPJ9bN7k8V0p0qki26yuOEE0Ed2e1f18VppiAfg7AeyuqnfzJdncaI4ctZeFPtW1MAIGt6EVVq/vZL3hXKpP0BZDykBguQL3DwEUKphpUCGQw01Fz8s6+rzW6K2HvvRUCezbeYYwkqJBXzUSBpURzX3I0EClQ0GgUpVLW5s9Hjp5RkhkoC72mP35akGW2qiN7LGm8TA8Jw4hSB4LGO9vcOEhfa1oHQcTnegfmHQORusP/Jkw8RZTTUL1MZVfEK6KwYCiEUAVaCQl1vpdIrPIc4KJGbxltOQEnfAIQiByGWdLm9Nho2y1P9476jErRhJsciLEpgBTdGAjGugqDpI6Q2Llvrd0Ha/cqD7YJf1aO6fHX8inYFpImUVFO0jiV85C8rOEkT1wC5h9Y5pYvzAdDnCrrUDl2/hkllRUx00V7ZmsALou6mfEjkAZsC6vfXVsZuxFBC4RcTM5eqClVJiH62mfMnunw24pHiuc58JrSIjH4OJx4O31Y1gwzvTjxp2we4vGMFtQCLLN5tIbHdtOqp0op9bj0oMZlNgf2wvgG75xWyEstaQYG1pcKEJW3XKsEBMVMLY0DztiQR2s2hTCMsG0gvTp5k4xsAFTML6qAYQNb6E+mbMdE0RML1nzjrzxbE3if8nJ/EsoqO42sJTM/auXOUmQAdayJgO3vbggDJI1C88LJtfyqWTAaiZBtzhGwODDWuxWYTio6jS3UawaUin4NtbBYXd8NOaixIdYrWDe2BUhKWvfrM+vlx3Bjj7uVfTJxEJMAIP4QWFaE+0rOdJl8E0AYUFAyYg/xrGJgSyWDCdpgN3H9+sUIe0tsQXjse3Qbr5Z0rX9XOZ90adrZhcdUqoPFdRJEVpI7EjSI5dOr1LaegcTbZ5ARFxRckGKuNwxRnpQxGPs5FkiSbopINITsJ5mWV2bY4SJIaEVSOQPUQ3+BdYqaGUzoL0jJQodUExJDiGhqGubRRfaVXRvU0rSwwxhPqaDZRC66hxibfG2nMLfbcsIXLnNF0ECwz4bDiAMy5Q5UW7zU1TLYvUCzMwfmC+Yr+6AUyDxUwMsjGc+AFVSJ/AYJhvyExSZFCMTy5UtGwMYNceyqkFXcs0J+WR8LKORCqluVta/27Bdro65Gycm3Ytd9XJkM4AlbOb1993oq1b16+f/hLf7rHY2Tlyv1rEpxbYv8VmPAr5+cC91pDFUXyVrnNKg+28F4CAW8mtlwdNWucUueOhLZvxyBPD9Qc1FOZfFuCWXMLWaUapBk8SkkgrZ8UEKAI0Am2bgO8GAbX9eYA8RXwmcEeTddvE5HGzG5l+lKhrPHwSzAkvbozuaTmV4de212ivOM2/YxxFD7Qx5k43OQwRlp7e516LeWo9G0RCFdZWQ0fsI2boIAE+spD33Hx+nvgluxVxz8qk8GY60LkFQyf2Sb+gE5zP6FDeEYzHfg8MxvInFDm05fxUDMLW+uAvVnl/+sNZz6cQTVT3VBep0C7F1f1zMzAANIkBX0NITPGXWZkJ+6SyXgYQI9Mt78MUnWQvvK8OsPNGV6d7We29lHIaSCgcYngPmS3UmzkEBR1FYlpu5+RuC1REbpviX7QfSOUg53aX7Mx4elMtunCuysmgNFKTC+cACI7swnMMrBIbQMQ9/tuPzzr8d6qEY3fo8ifz/nNMstx89NXmD4EUMQ5AXsIwJaBVS25ApXnhL3u5t1f0QME5DwzOfMU61WIrvABeh7g/FOsPbe4uOIHGAC3iC2f14odYIB4Du7e2zXQDL/BN7RSuJ27EeaTSUE+zKlKlNDYBseLHPTjCL6KoudZwRMacn1wYQEFaAVyawAKxJV0sh+aJwM7d2Ae+gashyeSHcOX3QGRXTAzuPwOWODxgq+KCuT4os4C3l7u5cldSU7pgll7dX/HCdiTJ2D+2j5ghcs7vYBEU3JdBJ/nhRblN36BLCGlrdhABv5Dvz6RLRQ5nzKj+5y2bzelfVPaz+nX2035+oq/Pqed203p3JTOc3p5uymXr/jyOe3q7E3p3oTuc9q93ZTuTek+p1e3m3L1iq+CI7UpfAl++3fl3fr9eb283ZzLm3H5vF7dbs5VF2pwfoStofzuXIAx97bvzy1vN4k3i88tbzeJn+EGRgX+PI7RX0ASBKO6yYUkwYyPRab19Z2+cTOKh1T9qP8vQTu7W3ae06C4gY1UgrhF4fcrra88VDZQwL0YLJbpdeO3KPDnGyuIPIl5tf90QsPPDRDnn4c5zPiLj4JJyFGNU/H06iwGknEdmIT/wt61UIUtpmVCanRQ0roOQRONctS0tAn3ILBZBX0lqojfDCVnRJytDUcqaQNiwow/aJpuuv2w8jWmgXtve/HawnrDCc2dzANZn8pwCTs6e3N9RZMtoRRvwFgo6dFdSBt07SfHT86/7/CB0Syv2IneMPz6X+Aj5D8zMmv0dm329TGSqaXTZxtOIIOAVNy1bmt54C5nqVVkpmr2zwyLubKAlQtEhcTNaujrltAVO2yShAm4QM2cRJWhDX9epqeuxNdRbq5yk7+Upfn+rEOfCmjMQEA1eKccVDu3mrbYv7xy0JcOdyXYMt3y1WldoUROqvExGQVnI+Kv2BrqTL+clSq5dzzAdHQFK6MeK+Lw5SzNT9WGqppx/ik32Jpg/j3TqPz48T7DIL9sACOYnBn3b4g/XBKbupQxRtfalAA4GXFJdawy1Kz2rBulaMUEk79hX8OKVcUmY0uETaFD+JC2nSNI36guMbRVadk5oZOQFNbsHjOmj+5p1v0juaxVIXWDpnYe5f9LOf2DVhRacj00p06WvAQ/wEqk/OyRPhmqA03RnVE0av8N6dvV2eEpWiG/39hkSHFV2haAgZZTFXPZZ2ffqvM5IQNNpYmsYsNU1GdHsJzhI4hlbLja6tEq+9CaDxkuSMQGStrHK21Lhl5rPsN5USJKqr6PqsrX/GDFA4ZRYdYeDFdTmf/crjGQm9W+xN3hKg8TiCC6Q11Xr6GKEQc3DNzB5SlCqZTuL9r7hlDV4YXJ+a8p4f7XaRthbTZPNjku9f597CxfDiMUgV96CER11IokE101xTk3FuALtv2SDO5XePRAyG/FC2yEQNhfKTYLbYVmUOKmYDL/xruJ0qOOdB63JQIdz/heRJUFFb/VtleTKQh+BGq298DA7+7SIqF9e+SESogXIwmFH788bMDTn+1jF9Qu9cwp7sxzKc+PvlbIH9kP+DndNehi3muuGOLJL5IaaOFxFiDqS54CDa1n25TZlGtlZmSbDDh+uuBFUwsUNcsFEOfG4LQquyuU7CinprmR0iKPwrvHeBmvg488FGD9KprNqzzcCCqWkfHr22jwaDPXPPA/IlYzvKOK/O20FDPZci/OfhK32w8SEDP+I7LnwftH/UXqnojZpVnBqYuKWOzjpMrmnlLCZgcMa3lDnZ5LDhLz7TqrT+psieWZwcvrUwPpwAhLyDVdCIrzg/cPmKtNh719jQ4omI4eLgrz5fTwj3IbJfqcYtE6sN1ELZBlbm5blDuvBX7Z3dAYgMpxjhKvV1Nk/8TDog9Qye6p5YN0Flk806W5LaXmeCRauSc65E9lN01k7r3HEZS0BF69Lnn5xOOIKau63FR/YG22opJgQhtgCmK8/9Mf8mW6iWkbyTVPAS++3vds1huOmXQgRb92b4NZ24slpDbYf0wAedwtN6MM1G/emz8eG1El3mtnKjj6AAKoxyLshGcWd7AQ4ILuQa9cNWbdapaN3+Bc5TnibJtMUHHoViaTJuoV0F2yNPm/QLf0GQ09wyFmwnO8meA3inTSk7ajQJpfU7p/zfNZvMtzVwLy573xmKG09Hv1h/e64D/qN0VbaUWCMkNKS2dCcsASQH7fHvaNx65ksg0syaSI6gR0UqIAzevWRgkdDOPdOiT9rFFFD0hPet6SZsOPukJOvDKpGvp18XWkz+XK4B4iDLUPF9hDB/ZwgP2wAHu+gZ07sOcI7g8f7g8d2EMD9rCAPSRg5wZsxhsbLpsh9UleVuLB8y6KcOhtWzxkzx6qZ03JhajSZXcWv5p5+v20B2+3/NB49px7NutZB4X9Glr7pnPT1qlv3dYzQCp3o/rzW3UzaNhZ68/i/Vw21lAozKMI8hfm5c3k7VdUpEcJ8n91auzczho7t2GDc9tjSxb/c+DZw/fs4Xn20Dx7WJ6dn2c/MoE9bGA/FMAkpa85BA8L2A8PsOcd2MMA1mbf/o6DzfceA9SmrMTnAdhzAOwhAnt4wB4asHMChkoz6mScZTUgwP5crfgABrJut+6i6mmzImBiyfbRKim0qdMPTdBaRsmvqakqP6BwGQp/jRo/d2vN4+nXPM9qW+F4AsA5B2Dk0pR/knNWSVge62VGe0AlnIyDG0Q+D5qWAHQgYym6CS+ILgu+lUJbo5o0XmOMEWPg9TfSyBpxDFqbFMydNUg7PuI4SWU8gUTLhugM14fN85rp75dkKXOxEM0ZDlmW3gG7KGJxElwzsThp0uNJLLVnfLWfIIlqzoz0xc9YKqP1H7IGjDJwAAWe5/ijWbGXHd4et9a6415zyD1B/iParJeSv+jnbqieGcnGJ9EQ+bWjTdvJVEA5uWSX/MkahKNNY2r2WZSRgpxgTVd11YG485J3+2seHgQPH4nQtYP+0iCmpUzrrgFjjZ2ZHvD8NcGi10H6j8oNAtoolRMSp1rQxEYVk3GHY2HMOT3M+FjrX8VHgmZFGt7je4jzOQAsVy39nrBf9ny6qQTkWMf5B0fDxcr7NAgpJ01MACxUm2TMhfO5SqHojeC8N+Sd8/IqnSLZFJ2zVUFqChTp1lOnXvbli9dpNFsnGf86zNxi05AFKZW2++k2WaYmr7cZHunE4fuoCpNHcdnyiJNveG1TEDgfV+FXXHnjwWzUerf5xRzQyk48pJU3n6a8U8anCllZphFbKCD2Hrw+eBaKljssSwNBw5wI64SF7OXyfclmKeKT+uBPu/QHm6fpavN5DTrD//bpUEJC/3XvqSRfK88t67mVEeKdWf+Ouz4YR7vtj2uxYOsmNJ2ewWZ3WSoy0WZM65NN5Wkk9iCs+FaX2yZm3ZUYy+PXQIdqc8cAVZ5LpOMrHT0f8boZwI1gO83J57jl+P9rzrXTDGBx2B/l4yGyT47I3FDXsR16o8bge8vtwsXOWIJwdSCPsw0oQqIO8wqeQrvkXMvmVxJ7q3+0TNqqr93LPXDb9KmAii9ctPxSQoYCkLzIi4UJ7Th5dzFBppmU33+S52kvRdLObcEvV7ZwS6sjREls4k5VRwLZKLGdb1w1+1XsZCSy4ua4dNiDAPT+BXh3/iuGSutRm8sn5deOff3CHjzs6SSHR2EIGNpetvsqEdaMr7A2/LMh1fR/oo6fPRdagxx1uuTlEIggJ1z6IsWDtrdgx4TMImLAFgZ0yRKNyufjgA6H8HyTzpsN6oWE0F7LE9rj0AqXmPEDa7MMx0AuU5eDBdM4yCoAzoUkgpc5FXSze5K0wqo71GJ289joejzWkZ6A5rlQaUCbuVbUamP+OHJOVGIz4l8ey0bsSWGsohaA75OGTigwCK8l4ElvKvFbarrKOyRXTRTgHkcLMkWwIlpt4gHNddg1a6CrrU0WjFUAkkSPOd2II54NKfebE+zno6527M6x+HQM+9hRNCSQCo1HL1Rr8v5eeP3OyApG6wKSq37PXL0e95ax+iHdrllikwSugA/AtM3Rs0Z1FB2SbIWoPR31EFw1d+jWLV+j0QPsGGD8QvJG7brLy182tOrrrS+gwTa5YHnwcCFk+NwWV24eVliEiRWgoBfe+1KtECw7Y2CGrCXVufi653DBbKGK0+EUfnsZh7TtegXVjqouUTRMEC2AI4OmrNF3Cr7RrBclpk4UKgyjWTvRyx6CH9m6hFAm1PiLat+3j7B5h9zF3lWL1dZwUBJTC5xRpR+yilKYN94/K8iyNgIY6cvyr1T6ANppyqTGGHt8HxbFO7xAJSz2obyHTIgtxFNp+trNTau/iD/XZMtTmP4rTBOtIBpgUMV30PHHx8zvGmK5qw/GPs/kksS/IqU7U8OBSH4kOjOQCccqnezH57YgI+i6pd63lTSv2njLErWKprmQ9bGTkrWe2K7eOz6A5Z64/zhGsWTnI79RaEoj77OrBlN8dBvBtyWIyiJ5naiZeawmXahAmYjjEM7AYN5dpP6WA5tzjQGoxEjMcL57s4b+EfPFR4eeeb27QNxFudUsmPcCHMWW24+aAnMfuQlFH2KUoyT5AMQpSafRMFMMqe7zXrPVEa35/8fGNWUVa7dZkEfUnvOdRDgMJsITh/VET3mGbFNuP+p8FoH8k0PCTsu/esfCYXWoucuLAT7gwRJ6wPOxTb1T66WshxHrwnsn+ii933Di1Vf/0oEOSlqd08DJz2Z+SqJX7oFLo631x7tgmvl67G2aWWTQPqrbW2U7afXAGlJb7n6tglHtU4ySVe9fwb+MixKQ02DZv+IJ7dNOkHCbJ3rJHCepOlWFk/SdyJZ9qZ/Fj+nPVgkpaoaQXFFPBvk4/DwDRkM1XwldCCL0F4guuoSJsR5kUgztKYbxdHAPo4Tz7ZFBae1IZ5W4x8Pe9Xh1pXF1qkvAkXOPLaQGtw8zoWPUrwqs3cvyRrxLFEX8pM1AxNaV7RQUJQBJV4ZNLNd8ptbJFkhDS5ccqrXjGMPjlSVWV6jh8xOOmJJrzpENzA6dOgkv8q9OE2uUbEFV3DQn45XRgCIIM2u/q3IaBsIWd9XpeCVkrmvAZYeBra7O37lZUWFBWcWwT+WHlqglp5R6JOBpy2iBqE9tuNaO3kKZr66zHF+nSe1hbQiKNMZUPKEaIT6kRGxo45eFnr1iF5sC/6BwlvHGeQEgsS9KE8YXNaPoP/II/FU6NpKDpxYLFDVbiqOto1gcfDqOvAUDTRKQQSMo1EEpGYErf+syKtfk0VgNlnI2hHT1PXfxQ2TEUkI+M9UuKhjMVxj82/wfHDo1QPv19k1gjturdwFtl17xBLXAouYw/+GSi1DoZVAPElgMB7wwlf6iFZeSfq4vVKyNY5za5IL+h6Crxn49EczjKJFhOKg6YvK9a/IDag0Q2voch0BwK2CWOc7rHquROeux4l1pk9MAIJ7mseiLdFKlRCnuy0s4cEm9RyKOAZJX1Qq0HRn7g+z7nO+msZB8ydIxCLrhbbSVdXXlTWTgLQBTj1lNV8gzxCSU1nvGKylw7AHtVMYeC+2nBKtz7tA/c3jNPzPB/X5+tEMhOohHTREtRIVBNeOPBPbFPSj8FPRwsN+iFVY9ILmXqWLgaZhGh4EMg8tHf2quxpBNttw/AunqNdGy8H4iwiGr69Ceheh0tH6TkrnbHUwTSHSfH+q7slLizExeujlqeE5L9hTahToGooZw0hYWmKgO09oWmx0ZUuAPApsZefPdBaTXUN6emzRYBuMJ80JsM4h1TqyLvPeBNI5RRKm96DtjmqPqRBuuHVv85m1azIjF2KmvUJTyjrrB4O7lX5/8deH1s6A3FYYXNHa/3qavOqkjrc548/jhScnJRuHNHN9TYfS2EI44TB2ZFCLGSGWMC/M9o/t5K5P80YvaLkyErAlY2I3LZwvpV/z2NQGv7Zwr0QO6cZ3WdZncxxy1fZE9ovfxR21/cXlROe0VE5F3lgXX4fEmGpR1odGOC43LqDvYOd/dY056RRXkA0BpxUFmQBdAxxOx8nTyfSz+wUhjaSRL5FROR6sG6YNmvYQhUBPLW0sHq3ATBfKztW9paJACXMG9x0I/s4xiZhmNZN3CjJQW7NfCAPW5+9lLywgpZ0+gHycx/OLwonKj1/L+UaNINsu6hwzMMqDZ+PSK0ui00OETn3uQw/76n/yXrjzXBXR20Ry7nQgDzt/7Ndn1g/gmcBWZdk8QX1vbabePmHHCsefk+r2X6WwXp1N7CwDJr/H4p9ZZh8rzgWX86iQHUgHuW+VX3cXHa/v+UvFbahbF8yT+Pluf009svX1zl+l8OSzjDrTeadzn2YCvSAbH9zEUqYNMCtwMwp06wPB8ynaAXlRfu0zny836xfxD95lXGc59W5bt00d/1jorCMyXDZal3p/F62caAK37kbVWdMCsAFabN3G+BSYjnXTAhDoNa8SIWyV9aYMRdBRSb85siVhRyg6hdMjtp57dd7IWD1aA2wJ16gJ02mAClNdR/1vDEhSklKKN/EHRt/MWTTAG8NNRQAP6BkP8+uPyXn2sRchRIDVVr/kxR5uOw3vFKabh1bHyL995Eaesbsl+kK9iHGiI5GwnUk5qFAFBPGY7IQnyIfcEZIte7r/nXp1pEqcyZTYS1qbaP00BwcVUO0rqKPCy9nWLS/4BVZHQwRYPHda4Uj/FJFa/rASxcq4rxO5HUjgMbvFzIR13Xhy/wLegkXB8YUCOYnXV0V9Lr1OXEP0/afpWJ1o8p6ljFDpX/YCDNSWnM8aYiyCtJIx+CFH2pip1ACmcqshZsCmr0cBuMFtKU6NhUYmDrhop5UYRjAQgGUfjQ4DqMAeuSGZo+OJghs9YVEOhOD/nACmQaUhjE8FDXRoOkEx1eHbQn/XAi/sSPoiu6GnnjqohF7ABrrkADamn+w1LnHbQ1VG/sox7fdDduh0ac8yPONTDnWaaRzgqJxfrghmKfdSKaFmf15ANslJ35cxtmkR9ObALNmioIFfmNEr3tu9ao/VpEcTKNfi9WuDEn7kManRxjtcgAiQGyCxJhWIjhjHzTOsAzLuyPJEgBRAfUzjXfMmdsEcva44PXhujLrIDoXFTJn0Yzy0pfvlAtg2hFJ8FG6Bq14c4M76pS36Uqf1mbrEhVIRfVcNU7abRXnIx+REUvYm9taiTd24h7m8P+bbmhxCWPvGagbKIHggdcoloSja2jZFpGomrybFVtZVhV5BnN/e8RSZlsXoLQ4fomk2SpyVEro+86YP6ySaFzXDSZku82KoD2x921jvBdTiPXn67qZ/8OYMfb803/nzBv7fkCX+e4AVc8Ho/X/ATLnm6O2EKnQJAIxxToeFUjwsNFpE+1cLUVBOViALa+Wi9tooiF+PYMx0i7OJQqcKqqdTiVOP4M0M0gkKX6FqtsV6ib2b3SZvYdssn+Jgl3nLhY5dYRLA4AnF5LErfthE7pE8uAkpdFjOvA9OyHnln+RwY6oiuzOUCs2R96tql1Cht8WEvX+vptYhe6nhpCg8kwLp7FKH1RvB1sGCVkdXlh4z9fMGL2Ry69r1+VSKK5tySBZ3s/evzN/xSUY35qu0//nR2Xp5XxnZIMGBzcFRWZ8nYf6uF/5GJK+vAvMzX0vvROhYDcefajAZYEmHWFpAnbNoTBo+s+RSngt/cWMzMaDcq0M1XHbCjueA1SyOlQoZGQxWDk0IgFClRiFWZCSwCDP4yvV+soIrBPAA62JgIhPTPDBzGmC6yVIwm3dFBdrHay+yOrsI8ejfkLZ4XWRJdSXSCXT7dxayqI73Un+HegQD6yg8cQOiIBv0o7HK9iO/W8wGg5vHQjSuerNoDMPMH2UzzWLZljwSY1o//E8e57HlQtnmsrxKckWxv8wsfQNqFMe/lseY0jZWEb+e6hV0klV154rf8iRYZU0gPgUbVuRswh3eWNANImKAKD//8UT9CAGiQPCDe2Dq0N0tunaV0egYz6s0zahU3i/TpGbA4gzJx3ui7snbycYWXQz4Dh7HhJ/r0ATaH5CPYQl0VJ1Iq5oIY0l+oxZgfAd8R2Ebs1TY1rGL/Ynp+xQ4Um5k3S4ahK9cxpvHdyXUvmx0N3+aS8iY9r/+73KnbpcYY4X67X0wqN015DNvLZaxoidBHWDYk2yjY2KOh3r5oTSnUcWllE58lHhebXFV9aP49J7IcuORICBIKkdnyhRKR9LkonWbARusR2cGKkiwiA+xDXHWNc1kWOxbsVynvV6ysYn8BNPzcgMJFCTRLKJjvT8LEhPKUyC2PNs1ihArvAodLz9xeg0dacPkAjnHipTb+2c0hv3A1JlcO5XE0FQWHKOZ5PjeUDofMqDQaRgNvZ/8MxrsnLghkrPbPsBfaP7DShEF60KF1Pp36jR73QBlXL00ybUjxwaX0bfUXlDPYOAz2JQwNIFgdteuYdyy2kfRv6Z0frLbsjnlPNi0RUom1R6zMa0lt4mCmFlk+1p28JpsdhY0Z37a4pJk5nfQ574MGDoVl2zJ5c7w3U7iNgpVs2AgxOs01mQs0jJFJtVCGwV71BwH3rJW3OgVrrM6t5d2ma055CJPrJxfSNIYQF5k0Re3BG9P3HJsxs5YZXKQ6k6GEWBFd9Zxwfolavty1DM6F3qifbaKVwfAmEfuqQ08Ia17PAVbI4TlXdrVcxbvgj3IUB+sfFy67I6kiIOJLOCtZgSyKM691bksgyXbu4ToByGcca0ab41stXAtgV47rrpUgMK2+M2IkxJlr21StIzAZHsFgyGp4xMWynZ/qNbPVOhOdqnrPbLJq/4Z4BsOprA+1ubOPYlOtFBMtBYkLom52yLYDgCXFe0gyw7XPhX+7AuKHG7jyEgPdBgFw2Xl7kTz3y1wZK1HQjWVEL+Txb2yWVhzduP6GIJ9fV/Gntxgdx3SXnoJHBs/3ybbKZ3r+TbVBxfEkvYitIVbTaSuKENMT4zfcczyW0cgnaFShcm0z9Duo23hs528bhtuQlLZVZadAzpurblcOe7s67kPlE50elxrRqJDScX7lCc/L7gIzZRZu2FePnZiJvEilBMhMti3KzW4NHy+7JzM+qXYvqeuh1lb6bSZRnvjAJ2Mv2ArqSRY8bJQSocdQSb+wENdndy5fR+YSp6AJEX6iGLVQBEOJWnw6ffPeUJbP6ps/9mtmV2cCfzZD2AUhP5PNMU235I5E6LHdJDpdAdXsOr4+KZffDjlQymDCJpxohwsprgPxAKoBCNXUHg6uDFC3Uu/L3np3FeX8wdTtdJz4K1qgk9h7SjcMkciyFSVHAb13JMuZKGxrfxoveQRNdhseSpYdoVoDnIZutcgLhJQ2pH8Na5JLsmWymONesGNUfI1KjS5oqH8j3qN+pcwt2sETe33ULbIQE2SHdUQau3phY6c5UCtmaByKnCtctfUKnRPAUcTheoVOCuApFQthxpBZSqH+dRDDByoIhALLjcZc1I8hqZpYwbb8S2RRp6bg6SRQeZN1QJnR2Bw8iSAKIyX17HspxbxdfEEeYpwryNxXGmjVWeeLaW81p8aBzjPzfaRWuDVuxyCAMpfYyf3ZZl8NANfLflKcOnc03UlX8n65IrZNeFord9GGwvMYjsJzN4SvNpXX9yyufi8XccVmfdwWLXo3yubG11pOCNp+hAUYWsfXsCo5wrmFO9DkNSfUtS7Yv8FFxidSA6lHQ6/RAGZEbVmpd04lIBn8LWhGkaBJFXbjlaxBRmi+tYwoYfdsxUwatH4SCIlhasoQzDSoenAhlk50w4wUTirNkMQ5OGVhrrhg0NZEql15uHm/cS1uH9SjHMYaRFEmQ+I95HOTfpQWZPrf2IO72FWbDpVWG8YajueJiDPhq6A3Fzuq6p1dbHEdaYlk4ZakncUfM+LxcUS8NA0yrmt/jrN69O+y51tgrLz7uHsX9jym6sPai89XdMETXVW5MoGWK02PWOWbnufv+qQyVL4ny8uX1hr/cR0F9Ev6qPyu4Y1ovT94y0xgaCsa7bJnhTLq+waXjnh09LJPneDsZ2MfHcf1T80mu9KaUHHwXvXzVuIzYaa+X5xUuyEFsvPdcjIlILI2BZEJW4XkxaGW2g+Y6WLP+3f+lXuyxHowQLcVUz00swKhVkFXvfewBA/m2eqHb1svTkiyO5jOg8DQ7wRaPVCQKRESwB55cwAI6b1ft417HilTlMw8O9Dfo2ZsKWizGdSvHSnY2VatUy2jZzrT2ZI9KGidmzToJnoH0GBVwUqp01E1XTQdT7umPx0NIhk5GgilD9zN+RhL7/c48hnmGbFqZiTONUIbXTUNN9XKu9YYVcbt9zK6IcmutpBHkEJXyCfokXCQzCykV9r2ykjDVm7P1nMrmVh43yfPZcVF8w6dKaktJF2INvAeEhFF560i/483yQBiloAzmy1bz/jwk/fzr8zJffD3bdc0vZHLtre7scQJPiJywyhAkTuUMKW8guHLH3mXLwAGRFg0E/hkPeN4BgaY3VzmXHXCT7MM0xE5cVSZU4bssEz6H6zFmd9Weo2jaX4WlVamtnHL+rjt3MWTFDq+WSohQtyk+eJ9UwkjUuZ/s0dfpe8D98fAUn1VGh0BHjfXGrhAX31IgaxhHT7wPPsj6pv7hxxIO+cTV9icMEvbheMXlc6Q5ZVvWLIVIFs4WcaLMFRysXSAC3+En8jNNZkVoX8UqQzhNM6foSxg1Mlu7x/m09S8BAKilIzNNRlKX6GQB7BnK3UXYtZ+fuxxVIc5x5x9nLHFC8mpTM/wwIFN9AutwNBGqgsmKUeB3Sk75looRGWT44mU5QH4WAv9nyBewLimImhBDPs+eEfr9CVqkVemv4zyMjy0nP1+UdJ6MVgq3Xwq/XnP8x7eSLFjdxGYwaYbhL+SJjQvgdNkpWY/GVDWy4pJW7/ar+dbtmbjhVRc55vePoBzbUttB6VQgr6LG/VeePKoYYZqVi5B+jxcybG7CuWDx2+CNmHp3Aw9a2KBC0+I6DPMDXMH1Ym4x8cGEwkzAUdLUwa1I0LUacQQOtqQoxBUAtPliDfgBhzHYd14RaKedt6vOIJDxzz1bjIIc3dLy5qa9HFaQuwqAAO0VXSlDtlkyq5Ts60wWO5qGrwRALXxGpfA1C1ngRLG+RyIG3TBZXiTJ7x4naNzKoI3LGKCCsc44XleCyuvBr2x9GCr/UgP0MbEPPW76X+KbLebQcKRaKHGtSSCRo0Pleb/E+9+ZhzBa0nm2MAyOeDxhaNhZU5CeiHdSHqpPxQ+qONI+xYWYvBoHlkdN6n9DqvVns4mYYpOZHEcYMxbpjyJ/WggWVsQaeBkU0JY5q2c5IPcWD4pSZRUs3GZR22d56+AnBM18wZSTprLPINH2SuKWiuQ2RNziCdfHXSQm6RzcHI9eUVMU2WV+TXx4rI92MHD8YTPXS7+pZpSXgj0GRS4BEc1OSNgrGTgWVWcXIkPh9jtPkYjjA2Lp787kJpPd2uDnQNLqu9BarxCqXuwfjgX8jhkNUBSC5YY9B9NoZELODs1HmyRnwqX3yFO6NKGw0ftoum8YDIYY3YvADhZ8F17B1rM4IR7cFqi2Tl/8grL59OKRxoILziJGkxuQfSV8TjhTS2JL7yRQqlog8XwWrvLk2lKlQSwddg4OF2k2NhR4KATDZmREUgBVNgzIO5M+71BuJFD2I0airnTOFRZdXnadEgKzG0++FYSCTvJb0bJJKQmL/VRoV3SVYzfzGGsljBovpCL6AkGDMkhAMcOqsYxJ4UKBQytWlsMt1H0Z7VmzJ+E0CDdiq+1U3IHAzRWKk7RVT4NJSNf71GhRZ5BAdHySwokcmUtEwcpMtW5FKI89bXjkMDfLcTrnh/Kx8gvJFZsvCh+B9ogZ4JSQAIiev4P6vQuTFpedtXaryqvDgQgn1CgwV23N6pYg4lgvQcmYs5O1jS9IPA0Kq+g28EhZkYRVt+QCxquvCNEYqLkMlegY+YtO3NwjImTNbVgxBaPSXFJrwRMwnf/X4qQ78bdDWqGflZH5cTAytZd+5E4sl5Y7qg6Pi2yII9ci46hFDyU7EHUcbA4q8CEEVOSUTVEujW1CmFDxNXg9rXZIZA4XtC2NDnjZEIQDCvOLwGA1cxz9MqlE9ZXr6hJpdh9ME77TJBEUdE6cUAm+zqYRrF3Eay6Du1UGJzIDbykqAruJKO5vccbapM0rkJ74PZzbBKhMcZNAxXFzfFEQFgMnNKcBvVFx6zhbYnEV+ngd7TEX2mVSGpKDqaeX5KtIlYRo1rEobuPsyRULPMxDjclryG285rwOP6WjLLyM7mkQ9tT0LoG/y3GQQA0eRXRLXV2z+q+nIbXHtCeNthDjo12sNs93Y/CCx8kMQpMnFce4KbW0UCOpwPsFcc1Iw9cxi3OpPcOjrhntzFK3ozt4ZqYSrRCyR6cdqzP+JnapNOi3m/gbW2M7tQF7aR499hfPwCR9NfURe4YTiSU710jhqiXW0WEjcwW87BGw6osaGECSSTyJmGYk2cO940B6+pwCk6U9ehI7EOIikWrC4RIsGEe81+jdO66OUOkaaJAj6fANvlUDEM9CKLzMv6FOEYwm74xBzWXqA8+3koT8Zbi3pDGMgCMPiGG/ytBjuewSp/RRjfYjzj1pOZ3MtFnNGdH5kJbC1ZT4mVuwEoepDdoTfIHN88KZm4q/f2/tSmJb8IzN3sufbHr/2k2KsgIAfUYwNtAk/PJNmIctdy6CKvw8ETqlaFDxmMkx2MqX+76a+KsVD/32zGTmidWGzbW4ytVMGYVp+h0HHwukCqcfbRJABWU4Fg65R0nskZCKMq8ZysRzIMwKGCZaAxv33JADvhn3RYi7h87g/Wznd1DXrZU/pGx6Esg+EcQQLhm1jwSQZPAdIpj1tKGrNAl26mcnR5nIXE+UDXMdR4AVN55SVSCXat0RTwf67XCCEVLCFQTMDAtpPpSjEHG8fXsk/TpjNRMYd1ccgmc7DqekOBjV0Etz01zDCS8EenuPvrSjLQPqNRifQAuxTVgl3UYHvXIgw8rIC2ysD+qBUkKXY/5z1QiMMo7kuDXolhz9AWUOP4ruD4sJ/4/dwwaX1o+3OwN6EU06ZyDSEZDp8lnQfWXjRJ+M0wcOIbN7v+wiwGhbhLYSeU0GK1jfIXj/goP8iRQOf4ZdSXW8FdV0/vbvtvvVjEjwDXHVjWQG1HPoqbHDTZEbrgWRJ2oW5fGvXXNZFUVFFqlD3aolKGnZoHu+UupoUd6DvVHHGF4SiBci6qejnSTg0sdN64p7VcPsmW6v96uUG0c+J2airyYZtkUsS71/pJ/QV9UKS6lRfzBaG2nIPEYdaj/EX0ZkK526Vmz2OAMKYPPqLhxMJfsZ3BD/ga7qLdh4ICbv6sS1Ysc5LHGORltvxY6j594t49vfCSFcyXJt62wzj+mQpLy/nO0xOXuqQgEoRYyLT4ujLf5um3O27S+uMO6IASV6hovR2jMwqLjXSevi0TE+80/0p4pAaJGku+Cus1E8j5kDF2Rwlvyi0U8eY9UH+cXwrd6G0fvipWCI6LF2laalQ0fllxEcdNmoS8jeFq2RSCcrsCakVtb4npNkcXlkHK1LYq4GBs9kyQHhR292UHUSmlZE7+xsu/zxJI0HfKflpCjVvM04hi6dW4g2oblHlcY7zfZiWA/d6eaUcPa00D8+nUTvH5onzLJwfYR6H2y7Vow7xS+xNNNLaNO06ZXGWMbICql5NqnJJbjd7QMN0hanXshvyoO2hryVwH79tH4bhDp3pen6XJTOQ3Ox/l8+IVXQrjSVUc9Bg3VFZS67DU15KvlUDtPzXuhDHhu2Ss7lakeuWEHnK/IHmGzwrIfjYqKK6MPqyDHX51hf4R8kXq9huyXBn/V7AgGfQsZBfd6N06jDv5Ovethf4CsEXq9Ts3IvV475kP4DsTHT43AfsO90XRasBr2bY6vBpA0wa/ktoEdtdyiARgC9q5W/qSzXZ+Z7qG6MknsQ966Cg7I5XD5j3J1LKwH9vosXPPfqexEegWmjBWOsvzdDVLjJat+bKkw3DpoC9xWx4FumaZswf4nwd6xIKV3kWwcOADFjyK651Ydkcz0im60rTaoVwASy+sLZj7HKXWy83BwH+ArSVdoFFqSeVlezbtAbmS5g3h7MkqE4UubfFR+aZ99qV4uxKNHCz8LOvCF1ycyYT57tjMZaXdgJynVd/xokOjj7dcnajOXbj6VH8c4cKLsaA/5oUzvaVW4NK0tyUIrpyrPNkfnf/aKcsHdwHKJRlaYFdt0Yu/XL0W8lxIG2QUHKxQF82KnhrpMfxl7a5N3FWfpGcBDDroKde8xH+ENL/TD1p3zfGS40nlY0hIyH3e9Sr97PXmVI4v4XK3d6OXz9fHbL73psbtFt0kfh2Ef0VcPWGueLFczcJ4hoD6ZR3Broyyprvlqlp+oASqhh+gUz5u/VVFKV6BpolXO+LEvpKPkHmRzFrqggXCIV+ADLsL/Cfl5ZykaJysIbqej73kGW4NthiMXE8G8gXHoj+zZp/mORX/mu6MDjuUuLEAnI+IL/q4GR7Gvl12er4RrJb1DLyOC7ch14WCMnRBQoIBnnNVsRYYwsMi1wbMHhgDun8kvmMkrFTteJFWCpjCN0hHs3FEqXb76GKZt6Ok9baNHKe5nVvSkoyRqj42caNL2dZWKTrPi1l94Em06HLtPJaaHVxZUtUdM+vJaA54H/k2RnYCdn0XQ0XK3UkB2prYgFLIVRdjlY0IJ+oklyJHSHoipEjrq0vb9c4YxlbCsoQpJ2oT6T5hdmNjpeDKTjifIeYFMbYHtIOBA020G4Cma4HACknvOFWfw66efSaiLfoKAnPQT+qD1d/CQp78Tf6p+Aq9v+FwQD5hilMowTeiaPTMOsyoV9+bzbKzhrAEPV8tn9RzOzo9Z+gv2TZiGue0HT24hR7AyMO8cdsN1YC82hPzYisIEO2/jRsGpVF0/dPEXSWlITBsY4Z3TBSKrXgVEXL01jHLV8/bai5MieA13G3w4wa63ldqmd6FuX7Sn0+V3jAQ3iUFlyfoSupKqrMxylPOn+AC6gKZwlvKFPYW6XuzBa2Q2s8Dk4CtLVya2A3nZZSdd59yqqrlPplEy6iQlu9c57kDSJgQRrmaKsQXPWUFuDM41Cu6sECdmj957bO3d66tXmMnXSZv/A+NC09qVOPCbztp5VPjlL5pv4OzcVDdKwm2fCbzZ3L+ga7t64SOtrF10DGuNF0e7eaErNHkBTvoQYydWErcxeJ1HGP9vduYnnEtteIf5KZo9LZXCWnsZeRJvujZjaLN4ce4JJDVx534IbZlBGAprGwAvNUEgI0TTAOyeh4XQmfhcSf2g2oHlLC7w+IMg8CosUfvEtVytKlg8asWdOg8eEVgAQSdPmpelvzIqSgA5sBIATIqO9ZM64HwdEd98x/4JiZkRP0sLgK5C3BMqNQUHrXRapZFvU8JQNLMDTRLQaoCN0z7I91jsSF5A0asBzbBEEO7UvY9yn5NLLTF/J/lydQUj0P5+JZ73t+RbGfVv5FohPb9yDxfJInIJFACiawfnHB2eWLhACUaAmIHt6QOfBFVwXOtaspbzjmZlQnG5c8sL8agMHjccoWlk8k7wBBOMt9/WMu0WVWKOpJ4NNzU6iTo1uSl5g3r4nsOBX2Cn/lPs1YZ8/WlHXOb9LabKy0+MiHXad8jUZaIpiQl4L6RehYXpHZ/O2Ty7pqQBZKOSnwG4YLyrnOyRyokrhoVzJlmEXAY6Z0C0EsH+wVOyhpLQmD9BgTALjYc5KnL2YtUMC8zv4W7Yrpvibwci5lNmPwoHB0DxWJWJ2ND/7bITmv40GaqkhiBAjFI0k8tTjbNRS8JoJR0kJgTnL7ru3yCHkGbPgHGeRHN7AoLqSSwY0RktOB+bkqofk9rJAfEiLkRhOPTRjXTNqHq9pNcVlRbZYHMqf2ilk8kiylQkOhxr+f6Kt6MVjDbUs/GcLrtsldN9aEVfIBmym7N8ITCtonp4L5ZPMB+Uq1cQI0DwtBZfaZ4AXwhgx45DWNa6CUcQ8WMAzkCkm8pBlfkUkY5p+0XAcD0InxslOkoFqe/YnTJpCLOWw0oZ19S9fa6NHHRNh1F4t96U5Aq9C9P6vONL4T+xqsFzUmobB35hhqSl37rBiszdwyzZ/uUljOkS9g4kkWAiycDH01zwLUU/kUWkQ5Vm19dZLJKt5LDLH1Kfk+gz2RnKkm4wsbHw7y6FR4nO0AFbOiUovZqQL7kTkB2WkE9rVlxNxkf+fM3F05tDwZDQtBSeCm2PR6nB73vNcAUYMBGxE9Mnyu26A1cD94IxxW8VRuBMZhL1JIpj8iAUC7pymAn93IDwHDnREq2zlXxF7b0MCa6GG9/5yt+Fga9+od4pVRDnAiR6Lm6D2OSgKA8h2s0lqEyypPkMgYfy3QFeUg5ySYPbwfVuZyf5+N1TG2IRG+7EPG0zc00+2PZIKlP8appVVdQdVBsKG4h5tn19bei6vdqapdO/y945fpS89eOQU5NC/kW8HMozux2InAaRNlZTNfHcsJxTcgg2xdd6dpaCvNpQjYAtDQU3NVFqLZPemio7KxQ9CVvyJYXLWER6Qmhq8Z04BzVLFazMsCMC4g88TCmouIIrb2UaigVLHMTJ2NU9HGUw53DuKi68XUHu9nAf1xRoyVc/vCO5YX96HH/BBBoBH+cacCrxw5eO67+7/f3Yv8bHzNrMuQjKU0PYj+1mJ7Qn+1qDhN9dE7xAWPaDgME+CmgNcTM+OcHVtxjtVmDGVAina6C3/Jnt8TA49Mii0ketUWj+l74u2hZ3Z9GZjXJRYqYxQH/l+Qzxtbes2XrCbO2sJ7nVzEVcnp98lVo+QliIfoc5tyArLG85Ca2QAcgqJCc9J76j7jsrwkFUK3Ocb8JQz9+mmm9PwvDehMqOGgLjUFFxfE29lZVL4bxBrQKuc6GCSoz/3ZRzipMSVTD4Dh5zEp+Kv1hLGu4m5ZCIaaUu2liBItdNXqrqioOVtbV+WXMl+Y1EbSFmQh4Ye2X6p5NBrAkjhFXWFYfnkcj6JQJqEYNZUsLuqqqAxUKOKDqAp8S2QDNmcJdAS3Bvv4Dq5aVNfbDNbA5bkBEldPTOHgLwzulUqNe6+Ktr+Bp4L4VGb+89dHxF6D20+9FG3q/DhgWKGVRP41g8ZnGcB+pVX64TuP9wgecBSuRgP21CmGyp182D7Jjrqzn4qFEKlxa4c8+26gwJvbgEvOfNRdxgphpz4NzHyLpeE//0iR7ZS5pg+LvtJTkNK3wVqLdSaVDOLK3g98OTfSkJRwtamTffWF9SNR+t5bxhiXqS7O4Er1x/w4/OGC/6d9GRiGfD1CS1xvJHU9evA1iWZZbLQWihnBh5PMtRTK5W6xbBWkpDtYp0u4PkRE55aoVIA+sxZkHWIo8G1+Ik/jEf4vV1b3ji5MGdPV4UOkn18jgC9xYggm4ILmeepYvtJhJ30FFf/R2NJHWu5e90xouORz4dqAaZ4nSbM3UnjjJmNCau+WzXK/mHSc/jIuOxRt4DktVrAdBdVEqEp1Lu9bFuVRaEq1pUdWYZoc+K3mC8mZtd3tZIvmC/l4JDq4OUFbF6SvJgw14SvxxVzSNz6SkLVqo55pHHJ3t/00/Z/JiUig5L3RQTPPmmikGJnRJBdfsZiq2J4cRkklpWj0zkmRf7F5twmTs7UlVNNIXisqIDZoOdihAKavf6EPi0o85A/u3KyetgcWoJ/BGKap29b+A/c+WN5iPVnHQWww8lFeM0XKUQuNDOEiux+ApOOniyJSyFFaorY3EiOohNj9NpUYUvMtVeM0Tb5IEqUZM0UPqCD6JskurktRUVPXh1pj8Df2ZSXtTPPTXVBOTtT+xF1Rv5Dq6aBuEDUeSr7bfiCALOBYqNwed0odk1U4/oGPMTE8KjC4OXEfRcBddpp8xB9BDJPLCgaFHQzSH+iJKOrrrQ5DsEbXddGdG6svvxL+3Zy4mrySbcP9Q1FkMHQEKLq78BWcRCxZoCwpk4KzcEFWgRfyn4oFB4YufeKRZAtlh5D12ipglI85R2vSE+1HzWg0OTWTdJeOaAHrNz3EKhV3lwvaZngxA5AFzdISS+hIr5YwzhnvXXin+WcZ69N6kaR0Brpnw/El645J6FVFh1LXHU2QtYlnOFNC3hagXUOUKT7OjkmHPuVRJOG9hcXJ9soSSxvvONe5Nkyecdd4BtlTYKUI5KY8dQUUAQvI0AQLULO+vzVcgGJvX9JuHW2Bx2z89OUvIyjB7UPTRXxcOWQFSaQsVRrcuaAlHGMg6W+jDP7B6ls1LImi6mcmLAjCdrf5qsfinoGoJVgPkbpboWihTOx9GL20TBu6KimU9l1ZOrDR+QYLxYm9eknP5ETD1GD+HYMUFVnh6ttkDkr/icUhHmFGs/7T+HfCQedw8I5zUxWmpPWdKX/T/jzIayGt/e2ZqozOmycc+UOvoc013fOhQ7xApZHUTQ4WIJpCNTbtF/g4ErMThWGalbHI1xRbMKr6jc0ZbGpU+DbfO3gQ0lQShJ3TTKodN92qFl9AMvjlLxsUzJsdfp3VRKogVJqRU7iyMlJoo+aIt+mgwV1/o47k2ixd0vmVkomgbw9BRNX/Uts8EyrwCFZR4JyjvpYoyZAHJFXfa0u7OArAANqyErrmVa5S0JiYitqwilnvneI5BgQFChwli/0VLFdq74EkCkZwAlc5v1GhR6ZozuH6tDtruEN7alUcntcq4SR28A30JwvGEPI3ZEdPVolhSKJh0Hs7iXWVO0ohD+f7nBzmxNytlPEDuTV+zVpmJ31GNpbIzJWa2Y7pEDdNHZUjiQ2Pe1HeFpLI1Zet4ZRRTwTJgxhvTiLj+JSz2feV7e2pJ1oPPEMWrCsSrTXlIszXjf7YJhcnwho2N2q0aQlSTHr+KrMHZ6Vlt9qMYQJYPRBohPFdzvrnSLlBn0dAHz4rIJm16/RVXfB1FgGACSc8FEpNzn0stuxwvDhPBphjB1Qqj2CnJG9/MDpUWwFGcsxW7e/dyWMF3ZEJFU0RDw516YVpCfzDENxfuh24J3ZbEDgVwUtmbxI/ilKgDhNLP+S/MiTuXWWY6JZDR/7GbfFzkPNNFKAV9kWvdS1ZHDTYrN+XEXi05FbZ6cFKxpJP6iR+n4QHhktoMAEe1LKVgR4VVEJEsbCw6j4FkBuhHXkBZipi/VYg+dU3eVY7EtEdxqBQ8MsnvROtW4C3/TuVMguheY/Mg4ekAXn5qFLrDMtT2cuBNq7Q4Hu9hIFAXNm5U9gpRKKe48sjUyJud84UhzZsnh8VY3IDBYk6H4Wr5BjseAHRaB1fvd9Ks0hAdSauBqN08ZG9o8bh5704Q46Zfx/We4zavJ3pxeGn3J6E6vTsDDQrOGaVs6IC7qdLyk8zt4dFtLgjjHpNCs/pCACaEqNLN/4jXvEfrm7jViRqHHylbRj5nYQVmDvzOcWD3mIY18a6QsoZ+AgDO6DYh2Y9hn/2DYd6fSINg+jIHktoW6NoTcOhER5LmIxd/YegOyrKVehRK8tFylaCH0TEfyzc/7OBTBGCDuBPWHZ+Wg5FFH2RIYKMweSmzOdswETb80FTpQQR63X3wduHTjbyw3eg9CrtrdbSqtBQ8k0Vm6peedVmDUlaYXLo34lMEZ3oF5LNSacAgWemqOeRLySMo/l7skMTUuUWqbJg7vqg7Naqwu+c2yIFman2aiZzaKB8asasnzAJEAIsuZ+JqnVMyHqfG0AoVr1p51Dw1KghZn1wJsRUCcI9bwo1WZjYyUn899KPijiPmPghU9pfZHuumGEinvvUj1B9R7aue1rcJ5eFPGA91BQsSdVV5guNQubthYl7+MhyDkl0VP1UzZR+Ow3n+lBJe569m4hgbg6sVLsY/rdi3sRhOW3zKqV7j0wRkJ0qmwc82goB+rGwvA8zVQdDvLkb8Tqq9432jNYJeDWCkehzF1Elr5DN19chhCKaObUTClpXWr+rxWhew+RCyizZmGi2W5QAIc/J+xGmQqqLOuuL3cjqaMs5KsK5LulXfAjp19gNU5vjVLF0WAt1d7vUWAMFxIyIopQz7qYDq+sIw8XmHnvZcjVZF1HS91KY2k+IpYxPBFFOijIE4ZEAdEpSusg7DCngDwDNWrAmATh8zkr9kCBrqcm2otnkcT/5tfu1DlFsmaM9uTEy+yAoTHMz0Cp5aouKf9+TbBB/evVQCCgWYdA708BcD/rMMKp17G0lg9eCmYn9cr5Oyec8IGkCcpj2Buv7sOenq0bDa4JNMfB6Oe/PLj7COMohbEW4/ysl8XSDQk5t3Xdsl6gfRH0JLSqWfPALNXclLwNSH7mLXebFGfK0KG0jcvRyFnDs/7jhSuZBNjO4MRuyQY2c4pfCqHnojS3hUjrrj7pbgfLPDuw3j10X/bIYY+Xtedip3auSW2ItVwGC9dDriEThJ6AE0t2VSxI7KcopHeHOu8dUhTSsRYZmsyTgZnr2iUyNa07Ctlwvv8WuSm6NxyXn21U3IM+nJQA9yyX1Jmzy7UBegXuAt5CK2gOLO3Tvts4d8l6eJ6oqAtEwIr/ZmP/nsmPtUkIInOmedEtIlPgaAWM2lwKWe15vqyACOikkGV+JFiZ2dhJFS758d+hoMYENKYaD4O/UzfXd5fW+q1qKgkyI+X7n3gC7oDqWV36h6W7JngvwJPgPYdzX32FET9qe+vsLCnk3Co/7MFl6Qdpw/5Gn7EN5RBAEDwSKW1TTJ/6PbsARrgDRAZztK5txDFnG1AJwTOwKNF/l1UfDakuLWzMrdpwa2ygvgeCjnDsPrczp7yooN3MKMuENPXsIVGY+WcMbYoFsU9mO6OeVMSyHNJQJtWG0w67rHsF/Nne4DSqIIhjFitG88rCMxF6+CosW0m9j6kyARAkxsMMhoRh/E15FSE88Vnxuq4XAiRR0/29xL2WE1d17UeeZ5Ut5ElcasSUPp8dqlpvUSs2r6XHtdFvV8lPGvL+TXTMM+gVaWUiG/nGwl1LOC/OeGUacNRVoCVMBwlJbUcEktoWTGoJCQMvb4cTfdJz+W++T03R64JIKVsdNVtFwQucyfuiRvHbcTbDdgtEaibsyj2AnhL6IfKxDFc9WhVvFiwNw1voiJuG4Iv0awdUzkcpAY4JmcLuTKpbFqKDZfKaIXHZ4hlDqAIeSR2qnERTDQ5/25pK9HXihOMOM3LI7S8vDtiv432u62B8nEqmZwCwrCwdwSp5OQA8kqsfHeEHcOxHIWDvtI4GQMdv5G/VL97B+fLcDA6ugF1AGFyNPsgQdDqxYJt2LoyqnPoXd9yjGdoqPwwSzj/DGweEeI76TATPLRnX7eZKuXO9tIZOk6LqkYTJGjpVoN1LSRRGOBCifxOb1UqcxTHsoZ3Y9q1hZhBN7S/onEij1PgOyZCALPRb9XIXAkF+h5SkCDmec1fxBfZ43//fFtt31PUT2Kccm9iuO8WILuiGTEYAyOgsSKB6aaocJY2VoyGLH6KAshvVsbPu9uoyUke9weVQlPIQEV00Odw3AzuL1apPCOFDNyFK1J2ShYdn9d4kBKlaE9b1uQ3SLVY0YMot+zNOtj1d1BZd0uhfrDGjPH+PQkiLjh2RykrrXI23yIQ9QD1aUTgsUZWgCP+qiKQqBzTLYJ6ERc7Ck1q0PMhiMUiIzvWhoVT7hAfpWArp+NDXcH835Z+eJ0sHQDRM1plD8P0xoxhUhc2J2fR+8y3YIrL1aRvzgXCOCZ0YZNbe/0cRo5etUud5/eZSQwlNTmpd6U/zvoEucC3ywc6auDeY4fjz6btjwuiyyJa2V+wc/XtyrtjUv2VLlW+2DxTs4SCwnwRFfeabjiEF8OWN0vAVyjKf4vAEahozHAh7LRzZ/Xz1RsgtLSDS7pd+JrijgpWOTHKZ5VNqZ0W+kFpcYvAvUSksPl9iExk7DEpaMHlkh11VvDk55gdlLigZRGIKtw64NbdzXY+Kwt1Ey/jiWUfgvHNhjQT4d/1Xaq/+UHKaCw/3oEH4Fe31MatrCVUtjiXd/aKCqHI6AwSBgYZV80Ejhl+b+NjRLMsBHwRCxBaN2C2/t7xhKjSBHuFPlVGvFYhUrUkURixZxrJaY1QguiAE0ybMlygSHI+BvGaJRAGxefZbAcmWRBs4BGimay3M9+cpszi5BKzmFqoebHG/C4frEVN2UkLFhNUExKUirqg+d6nNgvXHxMyp+GzmwIzXdfV3qRFETWXP8tcagsAOXgHKlinPvjpWs7LGXwHdxDk0ixhgf/mknRb+AnVi+wC7aNhM7Wbl/0AeQ2gjp4+bgLu4/wVHlVBxOQAgqHu7uVY5BQBx6NpVEyxzsI0Gm7q9JFoTYe+JrahiiPhHv+MFDZ1AXODDCZyDOMo5I5PP/BBsDPIeokGgDb4skTyffK8xHqADAQgl46IVl3V4ROqZAgW6p4K4haKRTNJzAidGtydgUw9OCGnKoXFnYzOXxXpAsgpQW/+cDnNI4FEsTjcbWoMfgDzGdElJFIXT/kl+MVaRptX/4wf/xNlq4IOCH3YjlSPeyKkr/cMKQwNrKEgDCbmmFOPrCLSAu0nRQvDXhf7zYPDDuGeBLpLqFFuoQwYiAQCdXCelCiIKxrnfTxZ0OlVyu0IBwzhKMTYP6uEFgNEss4pNlAB3FebMYPZlKjbeXXWZt6ITumuR+NKV/JiZbH812LaARDUs1MK1VCETD4L1EcSegUUKcoxI1yEoDksFmae2jcX2usGTGgRKx5LSKXRoK1WS/ApDLn0B3KgYPTw8k6pyZXM2wgghJzXLtpDBBfmK44TPgK0pwayDnq5jov4CKJRzBTxR9pjeC9ifvrBpFjee++HgDhTAHtH76NnoHPPLpQKQ2pZLSgwmOPLn2KbkPku6HNWSzCMbY8w9O5caRWuYBImGklqvG49iF3x4Uxehe+uE2bPjMhcoAzFz2oNPfgGNG0Y+qfphZLnSHlj6P61ZBE+D/LW3Vpj5TxLWgXpSUMDXDMxl5OSNtSMgxv1H2BJmx40LCHzE8JO+pAvvxHb6B+O6aKbHGnY3xpuXPU9Uj06AyrEFRu9KTSxkcIKbQaeatUGU+khTYWoQg6LVSpMV4P/mMXFnG8iTqjEoGyumQNHW+qMw66Rk1vjSihfICpLqG103JmrpzcMDMu3Zx2YF87gL5j2dq4q/kYznw3GWCBVVtXeyVk34gH2g16lisDdI5EHm25n7Llz/PA1vf6ECNdgSEwCXUq37JSvFx9/mLLwpl/FBESArXOM7Q412GW5jZTWWWJoJHpouDVFBKCLkHQEgtuaeBrsTOU4eQ0KrqaXB4Cl4mcU3VsTUNKzzjXS9l39Qqmfbwh1yJg8IDY+iVcLcxIDpejPYlwowOIpRlfytog05E/x1Fp0+X9FJd039wnDbf2DS8JK3mZQhqeMDZu2wJDz0gCC1yHLDBJhcmL8MnZ3BdZMMwqB7ywAOzMiqdEMzmuE0bCTRflrL1vvNwQFSQBKEHetiI059hQSTPeGTDzHsjp04mV4LipxP0QLABMPcjJPVyuXYkUARrwC7uyN5dVY4JmzyxJmq4BWBJOXpVQov5JImjtecHK9oSM1FarbpaCAmVxTZI6d2fqRWRIdqGhfC0AugNg+IWC/atFlJkxTxfpiw26WJs7hf4+OcxLkpdtx9MejBPTMIL3nyjmP7OWaJdsAOsvOWn0eKqwsK/q22J7Ph1ga0VLDHqFoV61Qu8PjB3bIyWolRaQd7YVBfnX8NfyvYrFSOtkGW/nlcpaQS4z+Pnl/RTLyGUtw0Qlv2Wx2Iydtf6EXTebk/fLB2Y1vCfQ/0goclm5oqLQZu/be3t1d1fiGS0atVb4S66PF8QnG9oG/ktnRsbOJkjNJdPi1BYut9RdCT6d+nbzPCTDdd5ikF+P3lt9EWGjEfXdTIBEuc3YRBsz/Y7WMMqE34fNuMTCKmMh8ksGzjyUHkEFaYsGWo/rWxt8ibKUoe09oZXFCZA2NgmkvyVbuFmh3BcRfu5A5+xz3ieAOuLq/tvrLdIuuplCM/E0zfqs0H0E/Otnu4pmV/2wHKd8W0NlFVunQsL27ylt6YtcEJNjiQwQAOxf/9nkmN/JQ5Z1PFtqArmRkI3Er61yor2Kw0cUsw3eOUQ83cY7m31HFeSz5wpqX1d6ovipUv97tPURHoDow2KYc87KIVYGRWjzwYFUkOMpvFalgBQm7xAifvLxvUVL1DvdOHEgrVEJ/qduEjDXKVJMkTfQGeWD6SY604yYkdGiJbKnFwht5JLVW+PqV0KFqefAgXz1qdXD+ncpFmfa5sONwFcTeXv+D+zzr1GtGaAP1whUtMnLF6oFroWjdX/umhjMoMVfVH5mRji9hfEC2huzhSr/5LC2W+5mJWLnXJ68+zJmxiN//DKKYa8TzZPHAzWpa+KLOpZRAXw8/bJcBJcOUxP1vMsmS6RPCilk68ed8SPNKVDIZP2DJbMmCBhgGeDfXcKVs+0CGcBIB1B9e2wqDWNXfjxRg4f9UXBUZz7gwdW3lQDZVy12HNB2rqbFsqmKXVd0a37sRDvi6nr+gkVGwpCKq67Ju52BD9cPWq7oYi8myR02k6W8dJ5p+0jsyjdUZwMkoblxslcYop4yN+XjUAwBVnzB1Jac4mm0WqrZGfZz45S9PxdeVWUhYXkm9pvzQEUUFSOY3ND3n2itThRH+o6xrRtAgmeY/uimF+E1C9TcxExx1fDJ6QNyovMa3Ql3lRIPyZCEBkqBznj9hxcdXPUmZXXmO9xlyaQDmVS7UF2Fh8kj0ST4YJoGS/nuLEhn57j+0Ou3BSGXHThRQnm+LbG2iyT4TE+rmyDLHeQZvQjFUkMuhetdy2Z6HscQUoYS8knK8R/r9XjVlt4AU6jTQ6SmYAollXQSBhPzX95DiVouzUz5NTW/nCyePmiPxPt3dgvXVRs4ScZyPk7KH5qTrVCdVSPnfQbXfgufAOZ3ZkMmZrJP9jLWZ84XT7slCGC93Ikst3KV0ExMFDd5pCJJ5Ca5NUHDP/Pw3YNYdFUpyGm32RhltIYc5mKy3PkvKhS/xDyH1pD0703+HLR2ZlnwGVIGBPAA0PgPZKONd2Um3p4o0EKa9WJOm2PaWyGP/UJx1y9P0dM6TesYSIEvSCkDGlvJAc8IcRySsoKJvy4hUXHM1zPRFv4U5jalE+zYM/oNunoPO9M7j4Y70BzycSbs6Lk+IYsTMVx6zzH70r6fZApeAi4L8gI9fa/9eLjaWyEl/Hm1oN9+Dm/GaOXfZxNxX/pRHE9LCsabSl8nHGp2Bmrnk5VmbmVpZ2aNnxphWNUkmxvKwAXXrZTv3FmDFDuZuPqooGFmcT9a7ztqmGZb24SPLh8qwLx+NeTWrwZW2Pc8ozZoGwh3mxQlhjJnUqk4i5Lrk4aizx0lzpWbgZHb2zjICbHmYCQWLbK/hsD9Jj0CoQVUHa6BBMnA0APHaHsOK306n9ci/zBafBow3ad2Llasn0U2+jJIwrkVfx9X0OQRNtYUuD3Nyc+Pa7gZu4RMZl2G7Fhe+ogtbChZA/+3pYergdFY8PD2UveTTAQpeFHr9mqA8fHULBQ7kZ6hoiR5huESS+vqRBuVtXfNuyVtTj49MoyAKE6MzVF6AQRIaOm+OtwHNyk6byRwkImcw0+v9jkO4aZv0fRojVasF6C4kWZl/gTV1NhFqaEBs7ss4vOFSFJ8SRpBzTEsU274Q+TPiFNYxWQwqNyV30OUq3jRlkHvZA5FEfjp/8Xv0tRX96AQ9I68M7fp5olxcTxpz19O3sonPSk6bDuJrCc5YB7/JFj7bc4C9tx+l5PpsJLazoxu0KurOGdXI/2w/EL2FoTL0CrrOcwUaMPi45aFNhw0VMIuwnhmCQM8mQZ0r5Ynt4WOFaXsqPMGpo5JtxZz+Sdll3EHvEXxN+G/YiLj4bBtT6u5WCrQ5sbGP70AAF3yrSXHIZFzrkj7pqQ2Y3lQpA5l6LGVv67S9qGVIxSuxW6WkoKZIq4I0DpmaBiUhWOMdM344GM0k34w972XLh35swG0ecKmP21SS2HMliliyMc69yrph/bfkxCdZ801j35V7DgCaxHLh3fiopphTyNnMRE0IDmTU2tMCOg6YJZAkUHSD6tEPiOtry1L54W7boeSideEZbvEgl4SduiIrTDxLCjyfa7KHjvaQ87ibFHycBcHzhl0uUGtxvHaIYVmj2sO5yzijALM5ZkfHEyn+B926ei70ZcR/4sGu3WHLIbJ2s2ldLOzij2dQaMt/oo8xU4Wt6wiP4xy2FbzGnGLdNarGwTQd0EeeJ33WzVnF8SxxWPfvt7h7uJbPBzBr6S1K85O/nd+n33Pr6saWrgk11DX/a4fPQVps62Kc0Jy86zxWxZxcNsTPzBIPlcHIqUKlSh+BZcG3oT0iOm+rt8d6Uo9VpiYrHgD11wjoKlYevMoBRh9ZpIEoiWQADxyvXia8VT3kCZyXrgfDRQy1pPkScrBIe8i02UxElQYjw59vPB0wGd0Kp2rBGycXGYPl7JdHptsq+DWShUN7nZbHErZbIjdG55gar5xjeu1EE0Fr/hZm76wTFcWFmO3+4ciGMo4s86Wudyo4/lKotIcua5DaN+f5Q++qSBTnL6NTeHisxtMgKqMHPy7d+HrZCxDT7c9tgtzAd+yKNNkayLcv9nZEn6/q7mRZ1wCPL00jJf75Xi4RrpzN4cKuTrxRsgBOiSm0Y5IYB+O5CGFGHz1S5sloR6m6u6+jF57G0f+Qo2ac+yPegVEYY+Ppc7PbArJHMbw5qMaaWJNSTfhadugrYjAHv4J8czAFg+O3/ZwIus3Y9U/VtNQZjjeJEMxwzxsLHLCShjcGxVS6UU8F0aY6olHXYBvJH7XLbgXOS/p+okfZz9sZUfrem0i0BQIbN+4A/b/5+lDqqyuvffb8HGZ94c+BXgezi0mOTfGIChqtZZzJr+kx26XsPmXMLuMWPpQ7C/2BwuXwwCUAtnrR8FQmlwiifgOsGhYK3BSzn5ALiPy2//mmLk6p0NwI6PPAxhRWlyNhaIOmlE1aCZX6NGBZX+T2mjJkkVrD6mavsMEUlPS+x33p0Dlkh3X7XsLr2pfdPIKrkovbVOyD6xTxbQVZne5VMaKOzwJsmnILFeJImBySBV3q4fz9/bztzubR1wUdbQjCQyKsxI1qCdfIztVP8OgWbzCyMrPZACsjaN8FyH7GOMysYHM5R6D4e04S45WsqXTA9SBCz/x6lajmlqXkx2JiA4k5mq7bcdnhU4A29mYKb8LAa3lgrIM9HgIWVBSyaIlaliIUaE4tFV9oF9Uyw8glPCaSIAVX/k1LgpHJTmWEEdKlh0mqwNBg2lqfdLgFzjvv2hVMBuYFvdpkT+qolH/bQBRalDCRo0fBWrTBai1oCZlT9nF3NJ/K+44JJSQxom183E9K1w1j/TCCxl1Cdt/MLT0551pLxtioLF/yK5Wlo9SJY6659vGUSngYW8uTi8awYNUubmQrv5tcpT95brhcp7PM7fLsDjjvQktSd4ccw6+kd8ddiCkwCmbE6VDePMZimDeHV8/VMitfHDGY3JW1HukEU1GolqKOkJq6is4PUVsavTDPx/Bq2XOehe2o7Kla9BmlZ+70V/aENcj4UHvTB8D90pNWp1+UM2wDU7VLCwiEgMYi/2BmYtGANBJsKeaN+py3ldLlAMIUtlOxnzOh1MjlmolucQy0PIG89bsZm/QaMMK9ZzuqcWMblnMvo5woKvuVW3KGpjbL84XWzIVyovKodJqTpRiS8wHTJk9xnJGz2Qe/8QCAAO/uK2uW9gRCqassqlVONsOB5sJumJ5cr0bK55iPAHT78nc8NgkScnvIQndMsldImwXdWAqRjIOaYz/wvHU9jK7lKe0qcvKukBiTtTV2Crqaji3PIV8wws0rBW4LZA+cZQ59Pq0SGJdSPIa4j0nhbMIw62JYuB0TyFoPRLYtnAo3rGLtJ386VLuIHrcutBmdMckj+PmLtHV4H/4MJSyxDILsqylU+O7sm10AkE82E5Dkcczx6wuo76uI/gzrnHHtjY8ZOVcvPMDA3o7xezv7qRHzAELunCfTpfkK+PpKefW6679cBa+7fr+plsZCe4+jGILkLrVyTVCIYdzNHN4ki+EemNOtL/60OUp1DzfHnOCvjxRackuRx27I+n6+27ODNR830RdOzYXiTxeZoprDQS/MsA4fJ82v3eqEGnMPmvsPkHJN2BgaLgOrBxxCeJxNl9q6cWRvINTxJK2dG/wWYV9GSUkIzUCKKy/HmwzIfkBNPsH9HWp+Ax4fjqZ3GiU3rNZnlfL8GflHR5yv1RFfh8ad5s0IJeU9u/2XmLCmR2HgQQH5S2aH868HRX36Q2MWTpRfD7+b+1v5LPt0SYvi3+Wq3V1fNypSO++UQKC0MMt5DQLCRaAgDbyBNjkGjAm4THhMTmDizYtP3prRneGujfWWWgT1Jal0/aFFM/gyA3ZXr1W0z6JJnx1e9I+vZCqfgt1V827w+vrDMOEz/5hNfRmiHR93G4dz8cL7NxofJr3o3/68kTv4yO7C59bt4MIaOD9t+xH007Y/Qj9p+yP0j7ZXjBsbP9GF7u/Z+J/lwlnUrhn79t3f4zX/rDn/Hq/5Z83593zNP3tOJ3vTth9BP237MfRzbX/Rw3DJXgxKYFdegzd6Qrus5is5kpMBzA3/Kcce3QtJv+BhuLgXw1PXQDz7Z81b7whI5wjFziveTNuPoV9oe7l/+ZIr4/cB//Jse5DQX2L/fz+/Lv9/9AGf++FjLnA34cBH7csDwHzvhd1foGeOSLMGjnjgdNG07UfQT9t+DP1c21OtPHBqALTTRQ/DJXsxUPUicPIwL7RVkH+CDnRFUAOCj54fzNnI/xjk4JK9GC56GC7Zi4GqO4HndJCN21LtBqjj91k5yj+I0K9qkr591+O5sg7+kaSHmSuid7JqkqfjeHVWjoLaIE5zUGL4BQGEPMAAHAtPjw+u9IpFtvfwIuH8yUyTf+dcwcvt7htz5ThT3I0rfMxdal98yunOHR/NKZLrOD8eFf8DSKXXh4f94sHY/zHqi92u3YvM6Hti+oxEvQXwPteHHuRLTV6if1mTGwwvlb/E8LLyC5v9708U4Pd7N3sm8KLqqe1+of6JrzNt/zHb/cJE4vOndpe0eOvnj5XXl7+yXbvPLDEhiD/ix8xeSl7qf19i/9L/y1qJahpSZz1JGeq+veetm16TmEt0exX+BtcokhOhArKuMwqXf34A+7i7i0HIwqeQT57zwN0l0S9Dnzih33PjAj38/cWiznYXPCUyNxhu8G9uATLf9nNLP2l7VtFvuezEsuJKZLQ5zfHTp52EzVqI7i/OsPu2rLUWJiAQkw4QdR2Rn9uCfIn2S21fXvtGX1/i60t9femvN9q9ROeldi+vc6PLl7h8qcuXvpwbefgS3ZcaX073RuNLdF9qfHndG129xNVLXb301WUPEPFjEvRGMEB1hJPHDS9f6vIlL1/W5Q2vXurqJa9qgr/0Ao/HXaAXouihxOHjxnxJvjRfNm/Ml+RL84aEwXt5I5I5RDSMZhUBRi96WH8qU5fdvdRm5Pr+Quepkbudcs+5MsDY5N161D4jhZnmumQQvg6rhr5BMhKLAEu9NaD4JvopgUZ7EmVbdydwf7JX8Y4Bv5R3oHYPMSf28fuWpWaA8zUJrS3gTPJnZr+07TEjpNWlkSnush1bx9SVVIQRyO/mYjmUEYe+toBHmkZIK2MjU0xZ9qQijPAL1ujwiRSIvT5R1KgsqVkye3Yy7r9MWA57Qo5VI15QH7sJP6rfdM3p+AAyybBWpdvPdXv3o2q1WO/GSc/R2JmMLF0wOvIb/mc06s/k82PEmU/HUBEcd4tmgDDGrPEq2Ohy4/GD2LqIMaTADxbRn+L/ieoY8K9Ssb5YB56t2105NjNHc0eYkK9mESmvm/m9IEieIFfJmGQQx+y9sbtdZhhFe8wJyT+3UsU3UZUjuPd98uJDyLkv6hJVpYQMmU9esIuwGFTG1+vM/aTi52vDG0fELi9xDhnhPHLWXd4RgXfxbkkFv/CgZnir7pbgAQYDS24q0dbH4tO7eLcPdqg4P2eCvHNvtWOBKy4unUoJdZyaIHd1DXkPihG1KFVmL7Cj4SUfTLr9pCx9QeyNIjL240OQisLwGmjn0bfKYGBdLMbZ08jHem1mhXPWJkno10/fPLn5J+CSDJautDN+zeafgN4Ehy+HJFtg3X5yBwgwjZdz0xdL5626d3++Rf1eMXtnul0yGxxbw/yvDjJjEj5b3bznqjoXiJeXHrELpg82RLyA55i916KUuGa4+Bh+Umfm1aQ3caqytB12qC/e9iq7n/hV2cLK7OfgVXA0qyKwqpg/LVsZ3WW5F3b+UWSNbjDSv1L4eifXh5JVSKn9CePvdK1kXqjfyipSHyrLJkA/v5s+9IXakFD9+U07CUXpUOhaOE7+JfiMuyBaKkIG3WVauryDjBqGp+2ymUWPmf4ZRVfbn7tFQszOdXez+8SYerTuWZZKyheWOS4K+8eL2bDhO9jFs4v28CtXUk2ap3LuDJSInixw9H5YPi9a1V8Pj6TI40IhsKjX16owpSI16PX9WqDshcn2bTO5ZpDqRfd5AdnNv80UZVjKxPu9m6z8hbSKl/7XKFOt/AWmlzNZN9HboSeZSFVQuYkjVDcVW2CirBngRAKg1C1x1LpJIyFda7x42O4UZI3vgDhcrx2hz80u8RoaOxuO8/EHGhU810gPw/Guke0pOEIslmfrrg4ER5oJx/YH6n6965NM6oelVf01Y3vu8xScIX9L03UxIKN5LaPial7D5k/ZLGEfRHyPpxttn/JU43tzqTYyc4SxVH6jvD96lpohMQcWs5xMotZ3qMYlyZwJU4u2H6J6RyuViwi5n+rPZZUB0oaY+Lxo7ZMCVRZPkj1PPtlSfW6qYIqCJoyYtNc0RanGo2aVIM7GC13EEh9X3QIt2nJLvaZSFFT8KGhqsKXfN8DDwiPrXBiSzlMJCdV18Yhc0MfDh18n27niWyKEG/jW3AeRiAmdUuq6U5CaSAiyMOcd5Tj7dG0VXENulrJBk5YONpOngQydes+J1MDSnGnPyW0fjhSvs2IYc3f/cC6/0kP1naVq/zEVN9GPB8OQ4cGE73cTqoLh05OitsQPrm3aTXxDhKfqHTBwoO+jCcgK7jx7dr6Th6PaQvGNwNXuLOncR5kcFb3cNlEWdbVcFYLxWnWJTWUoJ4b0rT6KUJVrg+SXUMgfNWGc+gdyCBySSf3N/Bxe7aqZLrju3QH4ViImOcx99J6UUI43Ppz0KHsivC4ZXAy5+fC3qCTHtD44WZViq1QZhvOG4daUaeneblESMuMHjC+9SENrJz0BhaSVLYSCSILgHYvJleX2Jrhb46Z+CX23URnXNXxNi4v2KuhtYW0XcF0KGrlN6yaaQLFUbFOVTpB1/jnE4uCuixh8YYaOhj511equFpSbZjPiFDISTmkviBQHO3LpujKh8r3WKgp94W/vjIZF+QshAvRa2964caTNzc8+/xhsLyV5A/nIzKuukla3LXIsKMOb1WpcsQtciSSsKBPI1eq7tC4MRa46FqTok95+Vb/weMWKYLbQ+dW5g6DMrEtAESxStwymTTwiSpk5huQsDrn+1OQ1dHEOdUU0TYpTYaySsYjkWHJMmBkWYjfCieqy5tCNc4SMA/lxDtFhwy4nmHXJI4kzBpddVRVcFWdOYkIqu0qcKDh8KLrHFlw8wqZqJX91dMUomfDIc4kliYlffHTegrsYCxVq4dxmbMkD1vgalIcfQlQ8M/Ia9i+ooSLD21TPWvzZWa3EBMkQH888pMY4BZdK3QLplamR0HLSVyUjL+kKt3/WlYdcYc4LsvtvvaKmSMKuqlxLT9RxJX6TrvdAYrKK69CCMx12Q65NOvgjO1T3IijGgRM8A7euuADLE0EBlip2pA3d544hqoqWQcJ+JJ04MXe4rnqEXiijdrbe/kTDiQlp4vAjTRUulvkp6qTDqewntllP37/hrL4bWKFpzxveWwmiZOrFD2JvwyMkaG/zW9jkSlLcMFK9HigH+bh9YeqMAORVvpQZGJdXxtBNBrvqNpVLz/jX4Eych9fqWAsMNnIuSVGYMmRQLYm2Q1UlGlry45i+SB3tXo/YOYzMn4wkVbWZE5ZZkvjvAgqaJE/xCLFyMre3o9BGb9sVMJSDcHUQkUjv5kzl3BMBCIty0mA+w28nI9zgngzSVw419VrDenkl69PeJd9Xa4mWhbAAriUkcLfEbLNhJ6LGg3k/JVSC2Td3dLAz87GJXXCRmHyJTwlX3rCK7aqxEofFB8bRaR1KsTQTmOUKeFoqQqAerFwJ12wJirbKRf7Qvusd9o7YFVFFF6jJvemI1xmMQSa6BofKuUTzzfSc9g1mHPEfnyi86tRtuGpKFrThHcVoZjbyLslS23HEyC/86rK0b7QCDwJtunuFdYx0xmac36dOhrv2ZOcT3yJq7SSxUjEi2gJIb3nls5pCoW2uimJrvRePGNOv3aEmKDxEVhxzbWkX6DHAcxYYz8G7x8gXK74bPaPiRFVGpMtO47G/8KFN+5SiI8Fschz21unZucvTIurCRhK2AyUzYDX5ScmzPTkonpKk5fRFJaAeo8wEa0SLrE87dMFt72q6B1AGPUjCMZoTLXwNu+Uqe+N4mCC3G2IoXGb4nvvEmbj662ROjhLtwzsA1ZTlKPfxTNWLaDJwfrciqtcuuMJERKfKWFkHSKgx6R3JMjV1FFLqXeWXh2Rar0zQ3Z0IBzZPqT2TfU3D2Zs+FSJvGbxtNlWQ4X3a9aFvqNZl8BDfT1QH67DQoeMAGa6JKOK3lcbaDlHwsKL+kJ32zX8I+Rb+uCgGG9MVoZ+LEgMOr4TyQFp9VQIDdZ83PhXRUms9LGnhm8Npmsvlr53MBq53ZIheZkGFj1hLwSIQVgdQcuWu8C6eEV1ilDXW9a6qcabnwfQ6I78JVtB76dghNFeQuVwguzF/AC0nlvIfjhutndR6ANsGtKDa4mctwcPrxasiqQPKA3s9uf976yYZGd4NyIh4rqW3ftuMvCSU+0ByVwtt/iFmBrFm3iwyzBbdEGxrS+ydbp2e1ljC35tcEXAPUYge4x5DcIOZP/MJWj6QxQsWEDRT4OeAjHdtFKQ7J93cvX8AFtesi5c8CyEdpbFfJalKOC+sYzNJPNqMRSHHOQezuaM2JmbyvslPoa7yedUssoqe6aRIq+qiTWZ6iUwf85mN1juPvAJldpzKq9QZFkmfNBYTEWgLx7qABGceszg5ws4HDfiMJT0Ndw0f4+G8UqtbjE153yfWlvxhPjl1L3OUKGwxP1lbU8LXnmxj46bdcCQamg1FJFRz4h7tXA9QpUE5Okq+srqFc8eWVgFt4YHQaVFsGjnBderGsfJdzt9n9VL7BTfog2QP2zSnsGth3kRmwZWNf7g7K7VcK6SKr12lNcXzRqjfq1W024nM53bWzYQ76Njkw5/NwcC/jzG3X/ixXXMb6cZGHdKuChvVQ4Oc9BCYPsY28JaEXumc7l8Mzo+audtxR7cqnkFcz7VIMM3UixO5HKoFLa6G4aaXVNVS5hjqxbB2Myg4KrzN2KzruXJkCJIpNCFgUh2ivo/7qjP4TXGGmj3DOyqF2aD+Y04tTXVwJYi4KiMeTb2wioLpsCwZL3wDvRJao4fyqTkdxTRpFExXcbKxKhBoKG44Hym8Nf/E7Tf4V8qJpzpxrm+kIyLfWHgsdtAJqbwL5UoEzEKWoIDYjjqmj2LtoWJ6yctgwSZ2jojh5XjVFJE/k+8OVoHg3ZB1EM6VcWNWglIRdrzY8/43NUjTr2kdZH1wzsEviDNUkeBdMOf7mRCqw54YOXI0n7O6qySjla1Ydb4rfxSi21gjEY5+/WyBqI57M7i0ioZrLmlGXrHRI5IUVUTjkdx8aFqVDZhUSoUFEM7Pf9O/7lW79Y3YrR1sAOcJahGrp+9Qj8MDWg4jGuOeVD538Qz3u5BS5eTO/pc+7/BdZI96lf+KwmfQhY65ATVn9Dcl+tt0AE1t9x3WFUXF26xIMllE63cVTWJTsAtkyGo5huoMXFub04EFrcY9VFw9jflqMOoWAkrXSxCp60nmkp7jRVkV3IptgSoZ0+wmVWH5oGX5oMsOYT2sNN6aI3Ntg2onBhH69lCFIYURxdsTEPk2gHLfQmgqAsHvwuUnUda3rDJLN2cqLXp2UcI6c1HtzOYB3FWcMxwPR6Pt7qWAaQO/5XRAONrSFt1K9+5gwjAR57YSw9KfTFMkwrsfPTFuQRfxmzxJXxzEAHNbI9R8PjrgkLkZCmR9QuvYGE/Fn6f3ZX9mX/3V23vgBa3Ezu1EMJFA2HYVLug3oLhDotONKesQx6SJNnkd/9rq3Y+PaOueI5v+7KGrjOD6tdfthUv8VuIx2CrqxogRu4KT+IRc3MTOYrXz5QAZq1FjB0pfibAFq7a41oCBxTqL1d4k5hvbNzpR06xPM8HOFKvgrbduF1Xn2PYdT5Ip+T9H7Q7R1dgHdox3UP0t/pNzcnu3K5GdOkhVC1M0fk7ju8ZsIHOeL0FxzQxQVfOHH8Sw8KNzmmwX6o/MWp12vlbDZ2lHgJPDCJ3xmlmX12cL6ZUU8xBHciikS7bWMG/mq7NTpBgbS6qJBwWihRu1Q+Rio82JKQeR5mZMVdOXNV2Dpqir1UzlVMINe8tYHytZbFb52D1fauad/qCBwnRgS/kI18H5xmzm3CxspnQjBiK2KjbCkrVWWXQ1mRuYtKGIqFTZG5xTOxKXsVm7jTlvaXd+cGbMT6HRvRz9V1EyVKoLd5pA/i7HS5J6bE3E/hCNQHXX/zdVHGfDMbNFjUGTs9Xedf2dlRQS+WoIo7MBenx6lm7486hZ1+nTzVG0pKcNOkKfIULuhqXQJ1YA5t7FvKqG6JjuMZNxIv3T2d1En4/Wgd/My6KWNe2oD7/zrt2Ln8qbPaK7os6NBuu7ZrTmZlzRmClr8nZt1qDEeyixZhkYx0wWF5bJnS78nuwOyFJDjdPB5GpumSaE5jaEih1tD3/WrCK1X/i9RUYp4nsga/UuTnbgy2pCnJCKIqYgogCKjAQcVp4wJ0EHxAhEQswYgeIpBhKWq5DCwI5CWGIgZ1bDYwIROLAm4AbDrZrdT6aXnJWyg+k8xjl6alt4CSh6MgYhnBuaqT2DT8H15iMj4rRsizvNqyya8/4spdkUfiZ9KkDM9VOweYOuYaydTsmu7CAUlqVmt9C8R5KMPpZiruEiGxTW7mjL5p+EW/bDPf0wPe7pvybpAHZuaEfoLup4lnTKc9rcw79SwPIsbvfWfgO9Ch+/V/UEzY/xe39sesSCr1dpZUQg2UB8+wj7a+Pzpxg+5xGfdv9Ke8j0HP7wiNOEXZcmQrr7bzkE3/oBaOgh/IyXd969WUljK5PgznNXPHTHSUbFbRpja6zpu+Uzyg6ASQ8QY5/6pP/kWSUNFFqeqJ7ypkegQDLiP3Z+sK4eZ0CBRSLq7g1U5s2WP0fS43vXbHmosqNLupM77cfplq9cRhAqGJRcz/lHdYekcGcyz0F9eWbDOzXPyw8S+Au7BOZAiHc7qiAeoO2Ch5b0XEiTeQ5izYj9mbiR6iIggPY3rSdC5Oy1/RoFXzgqadUJ25Fbmqkaomfo/Qtgju3BUDRjtyf0HO8L+RuTCU4G/f4uneZEN4xd9+GZAnxrl04p46L8s3qBmukNVRd3/XSeb0Ibs/gsZcSY8OEiQfxPAkEAnCHxjTziLjqjhV+McHu8I1aVuGhfI7Xhec5G3y0t+lVHr7N2MF+a/bet+eM1wvT/3+6mX9APenbPv/M063dPluMknyD7Q8YNyWe6L+j+s7Wq/UyihCmB6bd/XbcOs7+f7s51+fBNWdU+kgcSgxf/W9f9toY8Pvc4j5EGo7NEcoryu1/1e6Ug/9a26vdKMX7MrO7VP6Ks1mz/nFWvrTK+ns/pffxK0JlkuDmluu7NVcB29hguodyy00AYWAoieHAUHSpzIujebN4kCy4POjRBxMO+ymtePLFVTSCL2SYY+p0IG+pdDkNLH/eJaRZ4c/bWRIJ3p2vTa4ztgkb9Y8wi5RddAf7giDUZZyR/DvSeebB0959COEKKTH2sRjZ1Bjdamr5L3bqRrkmzC+OuGGsmhqC0gEggecty6g10cpwyEt4qRsS8QegonUp+gTVktWdVGXgJl1WjgSAXfxNj3f8BQf/TpHeLtyA8oce98+N8h8XTA0B2JEMwqo2fSi4Mirv6RpQ3lIdFh3z/xi+S7rh3XY2Rrh/ANEXe5XgX2lBm2GddbiMzh/IEjC0qxmeHmCrIY41VUclf7oKtmEoNTipikngcwTMGlJNs9WRgU9BvX5uGGXiVVWNSS/fJ5TIHRIrL9kpYVH/pF25dwhWSCRlHAPwq14ZXKym5VEI6oBYLHnTN10zUq8tNCRCQ43zyn89qOyFPPZ1g57CSr1mtYcXscl1Lo7WLEvPASql49ykH1hOmhiDUKtL1SNDFtT6Wr6hgdjgbNKCrMF+IJx3ZR06ezSPS0uV9HKyF8/LsOpzFbN6qYXWmJz+lzz3jQQMtFhl43xosT8TQu6dHQn08X1/TEC3kjwhqSlFN2Z7eZ2iHcANn/YQU7H43iGbkQB+lOJakSkkzP8JIiRBy1yF1qOrgpFU2JDY1p54wqU6WrnbW2sQq9YDZkONemTkrpnU+sJKqbF792UGIsOG5u+08CLj9CGPDLnKmnsF8qfQqBLVgSXiYBlfC3fndyu3GHCVCj9ngsPuwcnDAVX8eBYggO4FbsLujMu/y67+nUYxH8guMv2Hxcsd6bs5YsONlXrKpdHmzZ8SAxQkUxlSFgT2AP66mCqXh7yDKDP33SRyHRaPhL8p4jHeRIzly77DVoHf64HLlhfMLmF/Radnvvs8wlJgKoFiziD3N6rhGOp5obtm+It1BcQKIIdTFTC2dvxbSyCFb/yab9cPfosbfmYKS+AOhje5oNNFp/IhcDLvRcoFFOVLVd89BKux+ZNFVtE0ETkJ9yumtOFsBMJT++YBce8YpULdZTP1Mk1mhN3Tj1RnPKCi8hvK/l9ncjnnlx8NyZT/3OJS+AJ14b1bcXRx5e67UkZTSUF6B62CeM72f/44Z4WN4Zbvhol2HKU9suMKuYWcrIyk4hePVnAulpytwbgVxGYMhx1PLP3c5IzhHET4+GpJemjuRO4WQ8dKJXhp9e6Yq15SBEwqqOal3/hhpj/KBFHSuS+2xKt1j3yp7mTTXVbiPAdlHuVbQY9s11/JVQpJYD4nu8OSKFkkMm03pE7SjXjP/QMxZx/FYZh1W2rgJHV8EMlwuAw9XmLkd/e/MLduQJ5LoR0hNpkI/4gWiLXJCkfdHJRLFacBGkHwx438iCZ5x3EHnq9WZVTwZQu/11UWmerCAiSsE8tCDBEdosVVwPn20kskuqGnXiVXq+gAF4gR6JzoEiCnTCpQImeTUlZHgrtMETgdY82r2RtMsUQfas9l6rJ5WPY/7YjBRU/UZeo/J9rw18yLC0VsR6eqr/JrfR9lnJqkphS8gnGbZVsMhSYQSluQtr67j1ecT5kmrjYeDaJ2fg4rOKTEgrTckZrbuZyHM0uvX5gRLY6AAF6UwHYTBQJR5hvlVlyYMFBVkzkbanMO2jWHQBDIffRDMN9bV/gPIOxA5mQEBW1iJ1DC6Dkv1Hiku6T1u0gsrozkMgpUEze4anU8UOefQN/H3/wXHbLUJhPoTopi3KXd8o3z98BWH5ZdBdTY4VJgm0IzntwJYFrRqo7ao6M+GwNMmv2IxHPVk//A0tQf9IvNi4x4qZC7ltU2E4s5ewvtnJ5c8jW5QhBiWwhfQgiLXwucjTUliI4wHUtjktq5KzWpODhKg+FRYhSPs6rfM37wwdHI2d3gbyHvZ+a3yht9i5AaHFvgTKz7NlrcqSS8ncT7ORg9lUgtAG/NU9nrr99ZTpXdW5XWaolEAnqzjb/CPFt+2q62cCfsQZnHHlbCMmCafbQFjm/e28Nnuxw189t7Mxnj6gfZG33EqpEiCWlIgwLyaF1K7GpWrP/V0l3ZyRapPTJND9GR/qta6ebp7v9XfFvNImgFvwRYIXKVdkoEac0qDOQQVUCMl83Y+egd0517d4R1ntlO5WzRxCkMPKvIFOVdxelwHTxz6Jk6yc8ydx80eHehOlgSM6inRIpMn7kBfvnLGlxarPO6AYFKXtx0pYcfQK4p2snL8HcN3WCZNMAh/VG/FdxA05JXKEPkISYzG9iyEGL3n5GC5B7XE0hwwObfePLvFtTUHofY9iYM7eYzeCRdBEQdTZgobfrV3JtXTDnENwvQfltv/PSfP//thufvfzfrrziBubdso9QeXasxdmQcdGeqKm5gwMdxFnBjiIst+CPQPD0SszQ+K0LhZJv1CdkfRCdWDY39Mf08zbaMvBqFNiCorwRpLOyYb/6rWybtsC09Y/+NWCSY3xLAnuuDPvwefn6zHn/B64ZnABZnfwTBuOs6A0NItI2rNZsKD+skM9I1K/cSpkF0Ugt6T6rGQN52UvNFa6GQSdzL82syhNjOhWZdW9UpZwbaooHaJ7JaZzVPKzM44/QtLbdxBYvhZXcM1IOKyIxUUTledXxu5SipFNmRzzbnPayKFnhnbYZb3dHppgf/9frBUc/ljnmQ6rKVxmZtyaxY7xMzG4TJjG6RThPO9CfShWWRblkMaNrFn3C4Mc40+vjzhV3Z7QqWbIDZ/8mPlr4mv4/WQ5c12DclE4ZRkEojmk9C87eWUE2veGwOZJazX/bscKLPbezQv+JOfDoZML5PCsWuA95ZDjC6IdE6NxoQITdPxff3KhJPXnEKAuwH84kwTqTZuRs388WpgMyQ7iUfITD2fRbeOcxFe1rm/M7o8A4mBPKXuamFbi6NKT0vym1/byWHlcivy7GC23fPqRaIYBtOyfmZwC1v/REzuhI/il/9ntrhdowQrroJ8tgprNx4NYljwTin5pCsQWXZcab8tSoR+49G5GAK1PU2DimaUTJi1oVV/wKwjrwGN94XwYzdUsAKRyskcvU0tirmo82aFmI72cZUVPXUjqREeIN+Y4UW7eRyC69Du89svOecgPqGOaPZzZXxk9LmKPPI75vNVJ5ggIt1i8dfc5C18zt97ucNrLnB/lFjmxMhcGpB+9R5Ah+8oBpK1GlN7H+eA44lflePEYskaxNldY/1A+vejc8Hwy+9IJvJCt7s+nBb2ZZWseLvbNLoy13IDG8D/3pRLl79jQwplKFrBdWVkVzSyLMkYVHNhL3odqSToqXKdyDWSwUjbdFbK0F3X7qrpLVvuNr7n16zb5rWZ9tOP+OA6FQHsyOg+uotOpOiNkJderh1A6PvdIwkVbgiyOhej1fy1rcyXatAHuR79w2dvMUe5HDInovWqXOlpl8ZK4SY9+jwEp2rMhDynH1Qt9yAfL3kG8jnPP/jYY3PpfRbyvRHWb+th9rZbYF4xn/VNW2l5tvCFHOEbGTqJuZ3CvFEeZiu/0puYzVc74I0IvG8+4F1Uiyjkq4lR5CT4wKXk3SGovlsM4cInIwGiV3RM+48awjp5ZmcXfyuy1Wnepj1+seZ/JC9ZoaqJJla+UzMMUsiSIadkzJCA3ns4myYMUO/+xJoSGHkP59hUgLR3fLpNKWi8gzNvqsHZe3qqTRmUwOtZNwmaDMTe/Rk9gcJ333+4N7dF9f5FetZP9G4lehPlIZF7bPdnsNZI57uS6bg4drhGa6RKI3lXxpFA7hSpFffUL1rbieClNXLlerWUZxTK8NT8yjVqUmxnrfTBAhYffB7v5Z8LOHwguePkLGdS0J7Okx9BgzUKx1pOBAgN8M+UMaLwgfCUMthpL7MAgsDPcoRmIUQJERKTHMXUYcOWRy6gEgWQFQidkvcswRE4pggcu0I2qQUpC67IknAFZJ7PfhtwNDbllzwIIPKDTJVVnTsr/xm1GLQZKyiuIsM/WIkFK0S+1L0CbO6qLcZyoM3QcL0viFzuFBGe9xWFVwspn0EGbsL1ZiDe79GduOXcW5Avk5hcAT76qoKp8xP0VXtFvgqf3Iui+uv15KBlJAIIeykoQePwN6grzkLQ+oUfEyhx8xaVfgDCnPJxFNX1XO2AP79C+P5khWeflaKN/U/7acmeThrAgK+u/RFpSUE+Iz6+iDC/zEp4+VponWPBaF062nYo0HFnUt+1kz4Q01LzNrV842Ip+G3L5uq8XsnHsbidERhMxAlH5lw8QpK6gVKUUnJqhDqWOJ/xCNulvXtE6Fx0p71YdYixrRZTSh5MnoMFWYYMbIiV4fpat0zDt906heaedWw4KbkeoqASYkoA0GczFxBIssMO4ZA61kW6E7VCF0m0riRqWpxrpysvbayQw6jxOMW5lw6UlDmSUc16D3uDYbw6pl7ge3k03Q5kJVxPcg3k3pznm7O5OX+9gnWI1Rf/uLyIl1LF3UBugCzstodhaHnEuIPx7/hue50CeUteNTbRYjvgaK7UbmkKC/cOQqf2adTF4+jASEp+S9l8i3rrM+hAUhc/fCp9mOi3QXbzvKVP4ER4iez/iHJW+zsniZlIhrx5ltxG1um1weBjkscBS4BP5AjOgmcbBOkJklX4N7HxITxdMQeBKf1tll4QnTWxBwrC26JAhzf25QZlu9BH9zZVil0j911EXxjOVQWd7YKIRPvd+atW0LzaE/ZEcyVjbpKCEVhzh5Z+3ndCJ/8CX43tzqzCG8Qpsfxwwq6QIvtc9MJzz2W6WTXuUYGia87MUWnbotyNxisnPxOeIjYphrP0ZZ6r/QicD6S8KFw9lqLHHznalj9XoRCSoWNxaT6ipchpczzNDnr+xRnoRj8zTqaLwjwl9MsgHAUZL8wUVGDCTUB3nidr3h01uDi9YPSwVzTqyB3Ssd/UETVkbveC3M1j6cEd9pqvJvYZiR/xzplzeKkJghLN7VnhZxBffKrjY1MsWPZEkgK8Zx1mI5wwrKB1bZP8PHo1m9qgTll0I8PCfWgO5lEQ8AYN4ES9rn0rzuJoGLa4wSsax7nfCio/WWj+KaDNa/Y1Y49Wpmf5b2LqF3QyjFJWNR2SeHtropJ9ZmxBEwDA2sh/bTn/2px20siZIaaFHo0OWFXOSVOrDitHvynu48N9DbTHSYWdFjZ5yzQpCGqZzAbyouFYKUC9frWAAoeV2jNNm6IUhvzsToFBqZvHRDtXTLPzdTUW6L75yOKr7vpCDm4Q1aTuo2gIwpCRlFC7shCGpdRoEgaKTz+CY7e6GAqmXEMZZcNeFbfu6KJM9Bwv9MPDSM9BQLdN9V2oc4Y28PEuyQKGgwQkCoCRonzQeskLsgELCGMh8yKBmoFEhShFYtFDIgEcDlx6n7dABcuquLAQ9aJgsr0IsO/d8xQDIkM584i0EP13K0Xn8p8PRhjvW6qUgb5ARVDA2ek59KCzVwGGkY8o5ohmhrhuQMZEYl2ZMIOm+ve3lLVH92xqtdVstaALsizxowphx0m1eP4YZMFbTl/BaOE1y8lCLvsoB/3stx7Pq7T5QxUUFC04A7+KIcisW1h/yqrrD87c3+Tf3YGnSKGa7l6U7GZCXdf9Hs7B3HZ91qYD3GAW4G5rmp1sHyF8x6HijnqS/ouK1cY8Dz+9PEzZ34K68gd3b30z1y0FMxcWaVxbA+yiWzJrh19PlfYG9BMq/d9jXZhqplAMC69OUT8259R3Rk9lL4wZ8Cb047X+VPEP55yQ62NgpHmU0VhVomruNjsx1pTyFO3ydFBhurvxdegHJ7kCPttpL/MpUiQC2UGh4aNMlAJdVDtbmBA7GetRvYIxEPBafgAqKGHeCpviqR2so/i78Ch+5nEfx+WAkJNjhxnlSSZLZQonorQcwLwr4w3G/FyZxWWOqXAIkhy5Pui/x9yIUb2rTsSo8pWSJjiuSvqkAiFZCAEdKDpoVINRqE78evsENTqlvP5r8VsB8ZENlexo0UHYdeue9ryBiDEQooaETuCuzINuIc+BVgwevApgzlSkY3cLCE3ep6xskTaIVJrGbBkeJaF+S9Illn4WrvPLGFCIjWHEHrjYM5W0iBfXIdbqjRnVgujXEYDuLxQiE84aJqD1p07PV7EmGXG+l5heW8/GE4iFn2y7fnzE/dlM8wKF56luU3w14U9DBDw9Vc9PpgegzKabiuEHz6yQaFwMZ/AIUdScJL5OPdQhknBK0tCsgOgcDIwv0WpEatKudMCq+KE+8CxEzGjZMKU/6X60D2j6UPUyLGGxzmMILeeh5bEKfbGf8JFoZ0DhCBmOghiSVBMRmnKF2H7CPIjZDI+SRJaM/MHDtxQZrkTsoQEcOaMLH34jY1YwW6FeqKDNiK4LbffjfgBTbDiT7+oe4qP/Py1KS0NOCrQGReBYUJMMAOSLvj2ycTdLNJaLxMpJgVMtFaevDRUggf44tyaJj1PzWGNpwspBWprLTu/QaDRKN6jYir9fOn5th2wmDXdKj+DopafYmPlhx94i0l05wSZhOBhGVKOJH7av4hhTAyoSjIwEiL9oVAXU6s+1V23BIRr12brXasEhKhMli96A6rFDlE2JkXlQky4/i+nT1n2Ms2TptS1aJtKJjOCbwDOZEZikLHPZCHrcdnzvBEuvQ3x2/IVc9DhkMp7SSItiNZsyo8q88d2CiwvEDXaUURzUCaS/we+QsGc9yDM+tP6UaFalHmCYGEhwhrQgtwWtE/xn+DvyrnuxMCxBi8XNdZtiotqYHD8IxVAP7F1ZLmHQhxnSdjkCOqKk7KbOs2PEydbqZ8n2CqKBHv6U6vYpWnathcG8o68X6Lq13BDODmbRnDNzvK+vYjmG7fFhZpMKfZ9Bb5vJmGEA1Dfe1s+db1xeBwPjovGyOwwkwGiX0FT2iOeJL3dFeXaE/hu2y2WjmEuT5IXXcP4mimOU2N+22Yf3u71t/O2YJmZYxJPMNw39eivK6p9Ubtfi/UQulquUXQibtiomad8uTfEVoNR7gosLSm7N7kpnp2ytfML9Mm6UwBvG2PWPCxd6jB4TLAepz7yJGOfJL34EhvZ+zIozTYaJnyPd/YecQS3qH4djz8mIrzVSd0VRZi7sXuO9dxZI3M9hxeQsDKXN3CVDX+DTovyQnR4RiweX35IYQGYzK4AR1XSWM6GobFESISEMPGEkfzI8p0GHPyC7o0I08GQclJ8OEO3PmLWPohmDWppN0BX2pxgS6ICYzloW0weOG+sY8xKxOZCmdaJ1o5iBbX5QExefyuseIF+SwjpbjgEh0i3HSNiAFs02zQUz9NjUcW02BqnaqshFtAXRi8hmEkCkQSRF116GtPCIn/nDjSUZuhMhCSAzY2jGIFCp8G/iwMmRIt8Ry1t0HD7rAOJ9v0Aaw6lBWyHhIoAVdHWPYb+F1RCQozwPw2uFtXtA8Dag7t66hSB3l1IcI/kjltZAwvdHjgaWdE3jlrVQNONcQYLbUmEPh1zmPPbo2QL8L8ufNGZWlV0a/2MasizpzrzIMBeFKz/xzYcxvaIHqlAmHGmJ4BDvg8HbQTs+7I3oDm3x2xPxkBMNUhWenanaUJYJxvMVuITPj7EVecN+qJL+2Z6YzBuhqELFc4uKHGSxQ6akAmvYuqpGJ3qOUCyO+uEOrNxJJAuK+jlgtbNlHnQzc4qRYRw7ea7ckjBvAGYV2FID4ngNpmhbMI4BwTgQuYUxgD5HwuNV2AI3duWQcM/1CBQzRKJc1Dw8xZtgnQTT4AEX/94VyYVPafH+Pz5vXdcgnzu/rPzU8EB/xpimlI3TQFr+3SeBj9/chfOP1GIZPaowcfX16sjATRgsEKspHj8LM8qJCsWQScZ3JsYJs6bKPi47KQDglfgIzTIVGmDjhkkRluc7UBArvFZftCDdM/X2aDQowhMc5UPyhB0xB+0i03aIsMoQec//1kJN83+H7aP4RJMZ1gJk9L2FR6w64GEH4tcgChUOVOyisestMp2OOB5eDmx6Hr4xcK9JZzmSnw2861N4Cgo9mGMdsnYj70VGVyRav/AN7ONZkFwsIYXO4eBNwvf1dvsxLMrCcNnQFs2VDCAZbdE1yTfyd9VXvmOOPhwyMOKWT4y+8y4j64L6pHUFO+YO4NqIpAINql7knz81yDn6mkRMcs0ciN1kM58hXXyvNMKpqcXsmezlNLQvltyoyd8hZkVhSTpGL45kL4TyjWmbpe0d17qDyBwiuRtE+Krbz/+0bB7j4ROvsLoZ0zHGlGGaHNbOYzJtgvjqo3tlGHBsTCeQMsOz86S46YXCLRVqhZBT6cyXe7nnQCx1Imk3F6wgx2BFcHaJPnalyH1F/I7ztLISoXM5mLex0o2zdf1tKO0oAvrOT3YK0/IO26uz4jTDpkZg2CZWfmasGB1CTbAOjyXd06JTG77dEoQTpnJKiTZopoF/fFvkl5kzvKvBZQcpvXm3ekPDaml1WFOLKa5+ycSVtZgp0waMEfItyJek7CsmiQe3bqDhQ41XzMyXkGRw7COgpZr0+LwDK4QNGGYq6u/Q9QtTM6rjQhdjf8q64jWDdNSPaFg9mukh6g0LcTRA5Nt0duti0AzSxDbZ8LxKy0EzRS7Hh/rEP+4PoB6KU4LDOx5p0T0ze6lA/PZSg+6S8Wx4R1tModbXGDkv2DHMYAfn6NTLfmvJ8TPzQAQdQQv/ETWkH/JU8mEVd6osmc/sVIyP3NtkE4/hbt7y43fuhMj3DUrYex2oPjnQXaHuSWtOGbQh9JPMkZ5fgrVRM3GkpiYUXk0JpLxjN8RLbgsUGaSvE68fc3lN4LhsF8PTOOVC2AgWIW4mqiq9tUlXL/0KMf6iQ1e2uZdVh+lK7YwuXqrx4x1f5XYq7fliTHqFIngVcHYlx03ur8xFHrU072aGDXXntCYfVuktPuBSoZ2x9oqmsoJFJeD/1NOPHURNvM45/FWVk/2FAVdOaXnMR6Jz3i/HgQtZKBYg50trOyH5zu3hWyEB5DAP5OCBwMfisgvNUIlOOGjgQLKdM4E1o+8+l2IjkXnfSiqPo3xPtJBwK+fqCIcQ4Mz2z9ghyBzrxa1664NxLxhgdIxgFojXI44OXlaEdq0uiJDtyX/fBcHbA8yu6h4wWgQ7VggOl+f+kR0+MWSpv2PP3IF4oaEQGSGUDbIefGB+rCjPJSRmk8f/nolPLsRAE/DlJDp2+YwEzUMu17jErtxye62bFy5bG9lhAkhS5eXZeA5HwG5mhiGj2CiXWrGLdFsD56DiTTaFTnyUMtZ6YHTdEEHkt8knMVqsaCH8l0T2upc4dNV6j0hcdeZlDXZm8zsFv5UvRE5qtAYl/hHaj3NbbpkeR1LPgnxWHJLxzdrcDEr6q7P+o11A3VvUOpvmX9gUwqVvyLyQyLPsmgZi6gs/1UewhTSM6DJ0CXrkKg/C8cNV33f40zAQCrnGYZ+H/GpHjmaT3P0tzuHuljfHHg9ieixvfHP93pWwXDLHWJ7MdT+w4DGtOzDEMgp5/rXmhrdM6v3VS6idkqM/Pmw6g/G5x0hn9SQyDJwh5Jy6cA41DuJRFo/TQdjH1Izek2cppIC+ZANSPtm0w9ZSUW94pE8SocIb1db48kckOYegPI3pdol1t3yDO3ZP+EgwlbPPhlIUyRZFRm3vOwFWBSAO/bg6SergEaSkA/XhjRIpEo0YfWhteCZVg3yT459u8wvHMwKpL3nvqh9chA78YRRX7n4hHjAqgc0s1gmc+wuD+J9pTeaU4EArUTbu36FKOAg1KcMxnCk8ItS7AgMuPxbbnUbtvRwgAJ38E6yyewkiCNbUdYh5ygFbX85eodmF09FZjnQH1UowDjUn10GViJdByr15fOybeAGRn7byCDkVY7qHiZ+bqXMiRvWhJRJZUXFnrv8zhvUjmnWi1R6MCbOrWOnc4uU0CEl/wJfX6AUKG8LKDA1lFUSmkfLYRJt56nVSFPngjef/JFGMnFPfRmLjVm0e50F5891/rZ/dd4s6VTyrSbDeTlPPLeJolFBwLEvFbfyL40+Vfm+tp7W0Kls2B5589JJVDZuockLgnAtyBkHNIi44NNWSGVcjqBTTIwmvfzMBgDk3RePxyENgdqfYwbIFZBxS2s8F876rhYGnRdBVb2MTBDXj/FIDpBTzEd/MFxlwC4UAtQ/iCxv3W7A5fesESX69P4Bg+v8+sPsemwQrO1ynFrl8IQwLCDTxyz/iOYM9DET1GskCP8Y8Nwvzan+vjPxlJsLwvVL4c5BFX9h6/8E1kG1GiuyLoxIy/WpMt6pKPWNB+Jaxr1AYXpdTQaTqfGaFaZxkVHc8ImLLUQ5/LJ98QPxj5RV//Fg1UyBjOQsw4bJUO046VnMs5cMkzsmBAwtpvtODHaK4Y2tVYUTU6VI+1mGBAw4BOLZhiK7UeHzDiuAgZJUjHHqDsg/sSGMDDFZXwcpVwSm1k3xX6R3RFWHQL92hrh9XGNiIwlJ0MF8TxcHX4c5bXadkDxU8T19xaPfok4wExCLN4R4apKDCzqKhJkAnwd+mulTOnQylWbRRySVP/RjtuyeQKvv4E2QZbyutvJSZXZncfTYxPu1e01EBaqV9j+QUE3vP70qhSkLakv24E2huFRuUAdYCHuPwsMWPyrUOH03HFHDGbbo+T+mihZ/W3Uwz2fMw6bb9aKbCBITmFDP/6ClfYj0RMwheXV12cM7ocR2V2Du2SuJWRKaUi8wUpnZr1Z6xtK29rvRoaXb7uT5FpSZt4XMqW/QrzBVovfSsp3796FuyGT8sY/DK5crpXiTJg+jgwFSbLtapNaOu5ZG8T1f54CbXvR4N2DO9Vp6WrDmPu/C9lqYBTfuCEhaSUMiz0egwxUiYxgZwhvsbSSpnccIMMHpc40Sh+aRa1QPQnjo1CHNYCAbhG9roE8hwyreE9cPNsEI/gp2YeRSjdJ8hc028NKzb99XpxISFOO5zTYJHHdr1SRT7jX89LP9dR7sOCtoz67AEHVP8AbqACa6gmqGTOm7+i0ZpH/3cargbAoW4Qrp66lpB8nFWVPuUaOVuw7bFocEJgcbBob8Bh4koHQLXgmWMfRq595w/H5TZ4He5iwtp2ryd2Hbws64tlrpl9n9bAqh+JTxgA1+YR25YExFfMwH3f7J5YXth4Fjkc9LAAvkWMFHZOaSjo7hFZHCLLegC8zSjduWk5YgCOEjxsAENk1cmNlmX//pBlqZk9MThfFjVh2QUMvvJq4lYsfrd4h21TKqHRnOqsSykyk/kJVMO64YLoY9Nq9lEy4Li890a3f3iSEO4UfdcK9IshTii/vCeaF5HNNLNQ0QD9Y8T79D+ZGhUhmR6v0LoZ5yVX1an1YSoCjz8QF81MAkgbx9Az8YDS22l3nXHgtF/BX2GRshP/WcTf3RnFNPEbMlZtuuf8SJjbK0rZUNhJ29mMh+OWtecWTDtS2qOiVc9/Upd0dDb7QLSIMHvOimsaCfau1wJXgNZpcyoLgUa7Hs2CnNpn3IIbwO7Uexd0DvsCechYUcMSIhm557WB7xEBIupDfpkAD2REEUs3UOFwj53r3RxWVzZQNk/jF1jntFej5wes1f0wt3W4r5sTmVanX2dnHzjtBPi3DbZjdWruCgvRXdKYXMZghSvP+PvBrDDX/zuh3qqxKuL3hXPVxOVjHFMxHP57OvWNw05U2pFesvCtRYyU4EElTkEyCmX8k263wt95oM8dd60K5nLPQvqvSzPciFslBDJ1ZQpXY0QecIL0cE8jqeMIsJrpRh7okrB9LP7a9+19vylt4LkOuSdHUSmy0gTee8BmkYYsMoDhUO/S2mNct/CAPSqE79p8Dhy3X+TUmmogoDoYt8UEqwewSZYzxkfYGC6x6TcuxBQNDaLIyabQ7Y+WN2HHKjla1tD2bMwBDU4u0e64NmtZDyqbD4jPTuQjd5j2ZRWQ3UtUQDH1TELa4nX3O4a6k5ORBlc/gjSSVFjuhtSSythzPMEh2yacxecY1dVgQMqmZt48K7EADb6oZlVQ7SttUU3mxpmMMIC9dLkh/DARRCni9LMSurgTA0Ls5CbKcmilUslH0iUzHenMMSoaV47wOONCTbM60oezi71T3VeINm9ieVSsbQ/tTEO44TIo/mBWaADTN722DkXCTTKNgqsln+CqZQn80PnQCsYP2Uo4DufhX9rXgeCT/6ajrObyFK1EnZdPmQIgpzPBASH+2U6uRqhf5V5bmYM4eFOPcJ09vyawJxlQ3vemVaz1PQCPuLG55Dfs/zY4d9PZ4R1jclAU+e8vgL5LevjWzjqop/S4sOetaSwTC4BFBBG4CVSY7rduVAb5abpWoi6PkIfIg59FH23KRn/Zk5/Ti3+IOUQgdFSDQlqJz8tV3f4RxZSRMIch7PeGfVHcbnflziq9C/MdJQH/G5W/8r7d8K9uTnDzRqeg+Fyc7YSmdONf0nETvdXT8rhnn3Q0liibnxqhFEPIFeUWz37YOZjn7rxbxcL1QO8/owx+h2+0w5Yntder73ae/6bIn72fEZ2SYx16iZccRRV99WBPTGTaKvu8vKxXL3l1/mBpH2jYFTeLsi5OLbepvhdNF1vlptTFqEML12nLK86Rx2+QsRdN2kWGMnX2xR/iP7rZsRu4vbVOr9ZX1+Nrzfx/GbtbuLu1RrcrMtX4/ImDs5rlK2bOL46w5s13sTx1RrerKtX4+omDi2at5sxi3HC3fr8xnCoosUVdw/cXpXLmzu4aVevylVntZTI5e6BnI+K7p7Pd5OjuXm8qXwVefP46nAlo+oyZv9nd+wSe/SRv/OOMst/3mLnRxEX1cZItPt76FBpniGKjLKBcxu0jVgwJWROG0pLRoQgGhyRBJUQx3SwnMpo9DETDqIq/ALdxlE4cJgxc38RAszxoNG/rc4+Uzamw8d/OB4q+R60E08hvsRr42t5VjpqHgmXH9Y3sN0GdQxctpyyZknIx0cIUGL+OGpCOISDHJS/kh7BtQtZ3DrNs6FFsiiD5Hm9UEUTBsIQagJHfeAHB0NUhv4gQqpUevbFKFEC3ZZGxhMvRrKYBm9nv3RXPe2y4DkKozTSjQvgm+4YuSVaaGas+/VICPKF5T5jtm1XuwLpK+naJCCUWO8WdGOmlaKOq+bGPf8QAdRNA9RpZX1ZHxv31CZHKqzaxXfrEF7zLf7xqgs65yQF4Q0M0YbJR9DstDjunYhmC9Rp3Lzh7LCeOfZgnld0YZEgH7r0LCkd2oXHCX3QBTjlDzrRUG/d9uc5glSfIzTnB827CKNg+2EviZzg1PJF5zh1GdqB0z+hC3CCKHSi0fHaUV+dIzTnCo334+1IifoKelE834GnbkKXJJHoAS5mJ/tS6nftqfsP0Dg61WSVaJYZFwKtPpJpyTDRCXMXi+c96IVI8+324Ojx7fzQuAPujaJRNyvpRa9xclO0wzlCc47QnCs0mqAV/Sjd1KE3Xj2tDdz1eNG/ksSx6KBemnoWvZDpqWaWyhbe/XLm6DcGXaZX3sS4Su7TzeRQDOG/27gj4n40aQWZEaMDKy8ezvn6z8xBU/bHAzDT+fmQQTIeLjQPG5qHDY06LobDRV0cgz6b/WJLA9YHwb0qi31Ml3QyIj5ZNtNoYQdLY9fHQwdcHjo01hUd8GXd1sFe2rUdVGYGZ8l1+0uvTrousUJ6P7EJ6P4cGJuHD41zJw2t9coonZ/0accfn5E8erCDDUHnB4K5zznao587fTgRPWxongdozC074MtcuMO9Mmfv4ud8B9LGXOyd70F66I6p61MeUkZdX/qAjIUe8dKzw1fsQR9O34X+9sFEmPbND8tmrR8/04v13jAKgouGKrJHeN7yfi1BdJBY9jY8YP9UGshYMGQxBlbTyaPyHo6iBnQJ1cayDIElDBsqHPTcjZEP9+Tmg3mgoJbiQkf1ixRxW3SUHsoFJq/UuDA6SydxGf/dH83vok0F6Eif06/aw84LKSltfegy628YgblpWHCFX1MznoJJTY2JhN8ccVIaChz5sdDg8E+sBJgqOfAjpOMiZCm4Fh4stC2dMxi8i+xsFx5JHypslhhOVwe0hFordR7ALTDAcrqnqiYfnQoSy+Jx9JX2Mhg5wEEhhdva177dcv7AlTdBOkKovqtFFzC2G5IAiCzh5wP3y3m+Vl5wiJ6MYqdiXCPNp+mzjNRKaNRLTtd6MqfR0iyhmrgl4HRGJxTkRpzvuX3hsZEg/HwNl1favwr987W9vNLhJg0oY0xXcM34kemTmv6S3H4yGF/nwoJJ8sh0J6502qZeYhqSy/NhDeUz9PVG9NIr3d+kff7oJCduuqw+vFlOUun/Oh9tliZqHAhsygJNpq06pSDYrpFQZknfEsanQ3Me0MQshN5Fcf10T10M8lB89TDBw/BynotwhuOtk3U0M8j05pvQSXiqpxMKcp3wff7al1e6vdnb5+/r5ZV+vZlfn7/O5ZXubvbu+bu8vNLLm3n5/HXl9pWON3d8/rqXVzre7PH5u7q80qubeeUzv/lmEAZTa51SEOyH8nN4eXnFlzf58jm8urziq94awrKT+ZZwDtNVnVSQ7IX188jLK+erwOeRl1fOm8MNYdrofG93usunDnnMdNV5T/zgSuEKvEdXn/mVeK8h/+92XzAvXYZaPCE0kcxyiahrFKhPWNsWx7QbeWSG04SyZTxZISBQfaZenhWF1EHU6woi068TULeOHy6oBDaB5ys43OyDhIy1q4LxwN4uMAxH3WW+VQ9eCbNex3nyY3S+d5UeH6nvzfwNw5dPd4wWSOyxrnbZkY/Oszgb6PF0VbweTuIbJft+CDssK1oJesqcDXXvbSBdGUzQkWM/hP6KKdigf7SIHrgC3Zsr+l3+vuLf5e8r8r3x721+Z9rtKnQVtv99Fb/VghjqcWVsmB9WcMvW71f9Xr9flfvW71fFovnhALfnFbhfyfuQVo0P5/gw12YTh2VtvEsjwa0bIZSMMM089tcMc7t0j2nhbDP6wSvngH/HsLAP1jciQVofQq1Cyta0SGWiZpHaSAerlNOQvXHctMr4sNU8xVBWvIXpMo5m0n0BhDhWwA7b645rOBiLZRKP/duvvVs+vlquB/pp5zyVca6Gxo4yNVhPWyyMFPrq7ividN9ub8iW3LpRXody8l4x9fkhgeaHEJrnAgCV4wtX4DWIf+qmfYg35XE4rEJte2gM/6eMZm+SOTdHqQCuqLtRrevtj/7Xyv4dWVujevvr29rqj68v9sTaPr0vhKL3I41f9vFpv3la/3qjigUtqtG9ndpjdNze7s8tZlCpL67y/vdSz/1+UuU5xMcnuN3txLxtqWt3O/E/I99QrTkxnDl6vy+VVoYSKbYwD8HOwscXokag5afNvGk+7W/s38ydM81ncuNsMnMk709PfMhzujAvsHzP8CQXwSinMs0fsWV4RIiPF+WPy2deFk88LC6sL98zPCnQf0Hx4iK2DBAhwouKxxUzL4snHhYX1hfuGeaq6qwJwJ0TfJe207mNfL0nX3NaL8g/95rON7n+sxRr8YLic9fpfP38VUL18kYy4ldtLTbN07tkSuc4TOLGabhzdJAWScUUmmXmQtl0CTkkJ7SQDYtHgFhWog6DNgoZMm30KHn7uhCu4hMoTTIplaPMJoGM8gtlCpByHJZlVGJRXhR56pYbETGsxG9a6jBpp7SYsQ+TvJHWDrOHBBVrOFBhhROypR/rpkqvaL/KKtyL+ozAOlMxhXvFiaXVdb1KCqdEYMd1yMoonO5baxAXFEef7OKxd+eSytuLuX38tg3atsQJoR59SPl8xR1E29Ez6slI5trySEtWj4UrfoYerq7z4roUnHeoScmWSomYsLTJRC+iRwIxUBzkffA2+FVWPjKbnjvOrSUBVpE/tWsTPvHXDhmQtbHteHWm4D7QWxuRji6tmR+yVFla7FN1bRAusKUl12aqtUb1R0eKqRIT3aW6B2fsuFSmgUZTtucN8RCNjtKUhREZqLYGN6dToiI59ZgRIlla2OjUsyrFfapjcCfryhec43QAuOQPqOm6pRUwuL/w7uDh3f2BSA4CJJBBwEep+hZghal0wXk9ZdDt7snRZ7Q+7FDPgoumD2vyPlfTSdcsZLYQlB1/dJbnewSg29/FZvME85Pp9pfe22oQHFI9XxeCO+IULjlrk0PJUVS8fO4DY2kK+lYQ4NQgbgGcdhgenB0FAVog2wDIeBtwHHrqCjqmURSSHitV2FfJp8mO8EjQH2pUOFRJs5kRdWlzZwye7cgDp/rHzV50WCUlTEBMf9Ja4RUwhiTxLJdXAZJkpQiZpw9dn47Kvly915TCnBul3LKeqaMkchbr+vilKbDML1lY3eRxtk2b5eX3S+R7RUfDLRKK4ntQRN8TcMuQjHhMCck+1Vn/H3lYhXvr/zc9lh+PZvdHkrzLQ+kcmmTs1P1tIk5M34N566xvPuN5xxvC+ZLpPvyD92yS3k/e4QbwXmjMRWj1WfOi/rP+RXv2xHE8zOc8E1grAAYADCB8qrhRYQDAAIBBBN6HX4geRfuo3ady8w5S+FRhnj6J2o5nlW+v2ZaAlmzyqvKiRWDJuv82VBZa30pJUQVFZVnmPv+NI1OYwIrN/9spKQ+GeB0DuR+EpNqAOc0Lyv/Xj04WVPoFPo3QLq37JX9NlVP5+IX5/TzYY/cUoOlRtov0tNuGbxrwyZqn+PgcnE0eHGXR9KJfrU7o+Dnoy+8ryT+bvyB/QX6z81dSfLZ4QfGC4qYVXHDqnO2sggLXnlgXnfRHRICVc1VWykCOORPlfImcDaHj1L3/pZBkNKPwtTQTMlp+L4i8rMqBnKH0gjyodAavIP8yIaP191UVvVdafLZ4QRE0RAY3qfj87VPLdEF16rvjDVZCSl6Kwv6SvSCRrUzkgDkhx0IHvlPyJeDw3LMbxe2wFTIOcsKu9afjBEl436PJBzUeaKcQhn81JgLO5Ajq8MiIt8gU0RIYk/2fao+D7Ao6nO26U7jg9ad9TWgCQSTQeW1D1ttnI+20MRrhTXEIc7AFKTgK5F9D//E9OMF6e8Cp5Nht/2LuvK5EZJ4PCfWHZ2rGNGeG0CRjcWIQoTiB4XouH1Uq/o7l0rf7ourfC/w8c4n6bK7deQN708M/G/qC20yrF9oL+As5TqJ3+w5dsHUDfvHCrg4uXhAifHpyg+TixMDBxQlBQL52jTC+lE0oGSq7nOefypBNjMddg+KxS/IfCBEvACHwIJvYzy5n0yBquS9lXyAZ/zshkbuMn52Qvdcy5LeovVRlBN7cfzGtjncIXqMvLPwE8P7piKK5+yKwwxdBakR9f+5KBIWNEeD2BGDSceELOLhwAVaOEzzGRnRi7Ig0CBcqPecKWvxmFp8t1LlqYG9uBaTVC+Pz0tP5Ai+Mz0vPQAi8MEYuPp3foHcxN3BwMSc4uJhHmOsZwyOMhYDqlrneqAq5MXV1gBfyk/HG8ZNx3th6oY0TBAjwyXxjvpA/7rsJThdyMyyTHMb1bURDHeGFDd0un0QYtRZQZCtYZWPcwhVad0o/3OZpkX0qJ1Xb5/Bj+FR+60v7+yfjriG3S7Cr4ZN53i07NaMCbImMc9uZOfr7ZRdRLCSxPUcXlo3Qm/tCdpe3/zO/5F1+ffNLEiHgz98+Ew+Tmyqd0Jub3O5dUJAXsgtJURw/W/v2hy/0juJqZAWvHa9j4MvwWfPt5foGRVyNrODluknCt+D4WR+ZWMZf6M19ISt4fTKNjG/B8bMYmVjGE3pzE7KC16iWwwtfWvX2OPr2hy/0rKvpb3wbmxyOo3rPAcZRjXcGQra9M3Vvryu2MjIGQXPHKqHJxUIfjsf1vXHNW2/lLbMO2A2NfaWn7B98fL9Ln7QzrLzYB8TAp9Iwd1d//B9a+xp0nU1gIubfyWGnNjq7V4zvYLN8u8NC9det8SiNR9qif4WP1+GF/kblc+A5pG0JgWGCDjVEsyCobdxDRYyCu3xsD45X2Pa94NaSMqtbMAIXjfAklltmvXf94daDH2HzpkxuAj+/HMdXxm9eUQVvH7+mz+l//IN37YV+YiJ7GDTS5sfzzxZ2DKOkwZSsmW5MC1qNisb0OzQwPprgm4wNhX6J4AcWZoijg3dbRqnE9JAmqgRDGiwMWzFfiec72k4Y8H5rxdtQdjJlmvn1tdBF2cnhbe8pPgU/cEdEBn4EL+tI4Ab5i/MbFO9fVJH37Rz41gJiHoPwNib4EvmN85coBCvg4TQLfkr49hL5jfOXKACXvC+nAM5lmPqNmgHvECd9tBYYBsOsbDZgBo9M1qSsL8OjkzV56evw6GRNXvsTmmeKRxFSXMrg8LHxdQ6U6rl/LZUWrXUYdH2uy36qc+2fy9cj2YoxsI9PkO//IXyOxbr8PJIfcAoKOHxE5MF7PwG7zT51jZ1VPNBjJa5SQRPUwhIWjeJQ2UwU/Pm8g/+F8A4uOm7DgODGNnk2ADAWTAc4ibexA89o24qCilRxzifgSc3UOFnI60weY8LympDYfI/bXH7Bp8vvHUsjtctl6o+/AwRVOQ+UoN3/LLo49XsnRm7VaqLcHK7tiU1sqgl0G1PQcxNN0GXjUhC20wQbwHl/c0ChnovK8fP5kHtfiQMtoOtYLb9wxbWed5iBqb9oEVcCd5cxL4wlTRELvJ/427rBo00FmyhILshR3EkALuQcJyDaQtkrtxfH0Hrd+HdHn4IpNSSL0chXrPMtJLs/xtU8iVPDV9a6X1D3IN0iyslemHE5KZtBcf/NTgkPMrmSzZ1gM5/JhUaKFCIbLsa/oyCHh2JZ+13nb1PQILhnpBLbPMgF+kw3ZkY0qKa0g4hhlmjBLgVD/DZLaPOjjnWOvGz8FkJAM2RT40xCcViuAq+VDN+4jS6OxXmQEsUc4HRusUAz3y6CjYX2/BWX0qsvf30KZ3rBgvR8br7Oa1UIwFNpig80vGFyzrHrhZJARl48T4XGkjj+Ga+7pnzNNgVzfFuXZnvwDNmV7dblG9ZwPpymdI7nyYAmmDD94aNUK/PK2miXT0S+0c7f+TWFe3wHW/0qIb7VLuWGRHPNyXddclGXNZgZDmSRfTQoWNTilk5wiX6DMUdO2qKpD93lbt1DJwH1V/PtzKm7taOodnMi8Os1fagqjEEwhzoTCgG8DfZ/LDf8JXyYQWXVTe7gFvC+nCxxn6L4yF9FdIn2nPJAaT2VGIE2AeojysmmeWrXcOnmkp5P4UmPBaIf34znLT3R5RDXuxIcB84IRA/MkaWKGj8dp43JiWa2HEHnjNNpjlFrF7UUguklmU16uGKZyz3yAqWYOOOHtBY+cMDaGQEGQ07gIfdKZt5V4ecxfoARJPYzm4NAdxI8YhL9JcrdOT76WeappMGNYnaIiQKOxQvHYpIsmyI+oOfl5YuPY6WVAZpLwpd+c5o5931Jyr+KeNnmYxeqnuyOmMRbH6Qbm4reNqpuvbs1I4eQmmyRJtTwpoIyVKYHq29Zq1XSs2oCj9fod47tieXhzWnjQZqOtB83skQUze/FtBc935vLJtf0N0230vxRTe/S9rF+oQGRA37ZZbfXnO0dCdaRX/Gtpqc+9xiRlslTb1q5L9L2G4GOmUmNX3e/tuK0GXPuMoh61t8yWRc3P9yBrfBj2CIJpMx5hTpav67BrY+lyEM2eGiv+MdVViJ/65aCTsna2BwEscvgHWE2dDDI/KqtOIBBaPIuT2uHQHS2FkI2TmzpFxwXFlQArmUU7DG1swP4kVNMyaB3Nccf841MIjo5y0/tUNIZHrvf4ddSNCXTFjdPWAX7WFWj4qbzSosPccDSAKTuLSrAT7qrHFU5G+VDOky+XBxhKINyveCnOhDQorORjQqC1gJtJ8oXy54Gd2T2DX3is0NRevqFQV3psKEmB7VTHMvTRJM3elGSkl5kHhXgAYFubsGCmlsB3YeNGvkrDoN8AerpgGd1xRsZx9zLX3LV6jfKcIQ9yr5QDyYGAJAPCiTNbEXjvComQ+xl93vKxCnCcDijM4oQtFpq3kWoJc0NhGIspg2uVpoYWpZVupmnQmAQXHeTziKNAL5xhraNI49lMCGQjCVagm6o1lt4/FGcnPt6pmXW82HmhFWi8yUm7ku7F6Mb+bZ5Zn06gg43WQ02vexYXbaYsGoekSBHwULVj4Y+KurHqXDV3S1iVyTr/XTD/d97Y8Mp8r/iJbTB59wECVFuSqUpm2BFd1L3tsfzF185CV0b3iH4+XnN5hCmhXnyS0TQ0ugjOAIMddTUubFS1AcPY1onJN31T+DNvG6TgSC6Dh8vmeoigQ5I7lYq6vfv65HQx2CqNibNhq4hIKTdeehZcU+ORk+sG+mu2dtl9Xaa8orsvs1A0bm9FglUKnc/OEHWoLCLCKVQkBdESaE9rffvNwkMlnvH5yfM+WLYLXaTaxBipoZyZ3rXuCw2bl7wHgnn2+vuKHsmicVyTcYsCYbP/1v9vtiSvO6i3H4soO57/ra1T4C6GPr5ZXc9dBKP55Bq91EPxZ4wGdAkgOQM5HgHTtbqviyn78i6j7EkOBUQJ1pOdj6dNYehBMyPdLCNb9njSRz3XUJ8JW0dCGqx/Vv1FHW1PV1R/i6PYQyRFYaDzrUD494U3UHt3WB8wo2FJKymwuHNcrXIoc7Wk+oEXnJ9QVFVVvRLcbijRd+OvCfMHyFYZ5SGISdhBEsLH8+Mdt4zaKzNa8B+HSqmy+b2ObPlF4xo7UH3hQ6qLCZ0J3xIN4jGcvsxPOqqp1VQ+JgH3kZQsWjuIlMaI2S0WWUvL19XekbQ8ep0cAm0BW4CVge6Po1naGlqPjBvlmSaCQveRxrquukOlD4KyS80tfCwsG4oMofsC+1jk12PbiGmzM9Zl00Wuu2lth/rKz68dse+Ys7RPHSZiTlQEimUaRS+FtNgNPiNuAeyH8Y9xz2yO0tyi7sOoS04E6pqgvbg7u5EYUkTuWfYER66/VY5H82Exe42Ef3IQCg0jgoWasm1T3ISaHU3vVs4EShxZpmwjONTl/JZc9VjGD10sXBjsd6ClWTVGSfWYXFQcFDoqAC23DXmCJtdjv4NW3WJ/QhoIhr/Fl/8BOHyV2QAcKib/1E5qyUQN4hNgMz9zCTvPWo4jJDyiQpNzYHSPrY2ePbhHRfD0ppbYqIkgAhsx6hUtdp77IT4zT8G6KZNzqY1tQMyzIC/1x23LfMP/5bt0hjuEbCJDei/bmOCFa2AaZIlrbjSv5bTJIf34WP4uK3D0gfHnPpA9MVdZ5cAQYm42Lzlnn9f4Zicww7ZvoM/Hq7iCo3zcJfZ6Th4Mqn+HM7sC/6O4bUzTNaaZAzyUizxTzx+WhADHPN7hAf1tBxDzAnnnYSTm8LDz3gXZz9/Lv/8+cjPOIo7GoHxmEIhCsXD8MwIB1BEm2Ll1rntK2gsDk8x58yD3s/GifUdq0vQY1HZ5mHNaYHHNPGvWrxPQn3s6swHx5RIGEoe7L0JSPm034oWcSxtDCkJicRwZJzYrrX7u54AWwWu4VLwVt7A8Suius95SDeNXI5B6GPMi0clz5aOlq07EpQ6ivfa40FiJSucXv7lSpJhrXRqi6JuH8AWj9zwSn54q1DYN+CHzouvh/Gh9GdJldqOurwA01Vqck6nwX0I1LJoOPtaqsLq51EOmgmqAqvwB7MR1uYX0o9WteH0ODwFZLHBK2b1ElYTbb3esqPtMMgdrmMP+QbiRdUj5VItb1Ty6lTnMlFKTY3fJQpynBmkVqZC1dNgeseqGtUgxbl17ifppfWQAvVdOUWmQPIEhwabTEnldRi62E3iy+XZgLTe/ZE4lgwVBXhdHMe3625/tSmfFcbAih+s7Kq/FjJXQo6ewfe+d8pV6RTzEvt6S0YUFUfSVhOyU2Y3kUPtDqYp1uBpNKg2JejPGwQAP1MNwRAbMxAHewQW/TYgmrRbG+4hrtvM7FoGXwi8pWsGOdsYPSJOrzKhuR64tNWsgLbDz2tbjgaRkKKLQUr1UxmKHMTwoEQ1ajtseAhyqLkCfisII5PExlwK5UebLTvM4wdWjoVr9oFiQrcFJvYYtw+GsbVMFDdqOSMtuwUr66oxvvKnWQa2z43Ogu0/rh4rG/aoNFj7QfvAMPerZP0/u4w+Ehu1y5NHmxW4gcczm7VefLQxdpBHF5//Bh9JIl5rA3RFdRrayvri7GAybEWci95L3deiUDG5tOMY/ZxGe28IK+NKsTffL94kyleL3VUBhSHQ22fYMAl5n8mGRYmr4o/9gcSPa83O2+YDe1O3ZT1dP/7HJnvqqjF8G/bVlfY0Nlhcm2ZhfxtswOOlPKtZUj+ASn5zHYbN/s9UOQ5LP6fHM8GQYrJR9wd8tNnAm3z8wMqdsbXhy+S6jSjGIrHRR9COW7AJrw6zt+rm2A5frjWxLgts0druFHHLbis9t7Pv1rW2fJsBa9fW7W+17aCIE6Y+0lMc+XCy0Y6hmOEfNlLtKpAohEhqP09/oL3gO8nfadExvGCHc5wCy3if4ZLtBt89Eil87MS+wPkceGaJx1zJcnvM7sKTbiR+iMlfeCqCqEN/j54FI4lLx1Gge/F6+6K5eLoK/bOLeEnkdR2J/bWT4NdiFRdMYcTwglFIcvaaANRbZ9MBJBRj7BTlj5QcXc9TydWVfiHfq8NyTmPazcpYDgTMnlSrB0MrEoK0ffOYDsRknJyw9yHZqT1yxZC8gjNw5KXN5b91S9tWF/YlAf3YLdl0x4QhlG2NpWEZPhOWLD/r707bhzWYvQu0ho3suvvHDJyw7Il2Lr2tkgFyOeNZeXABptDUxmIUdasvGOyJPVe3xObtYgKISqUrUGtqgZWd6tWuNj/YD3z/jl2Lc1k52VbB6fsfduuhPt2B+C43ttwZyeVqSgLSAM5n6f6utNCyRfV1dGthH03KDPlQzf1ksdpFMUQJu85UWG9eqRIUtrf/Fep9GXMdZGI1KeVVr3KDK5rIbIHfDNVT3pt5PTvvQuYbDr3mUHuzlFlJiu/66x7vr64XAqz8LMSE6U4MDGjdeph2kZh2rkMeYF+Ds9E5xlS+5geCcdrBouTQUEN2cffZWIbF/y4c9LeRBcP/Ck8V/H5DdW34VX4M8F7hS4HxRbV7BWQ3iq9QasPtl7gvE8fQPAC5uiPA11waIPNTpaE5Fm1EGekHoxUjLs01HtMjIpfwoSD0chwaqTYP2+Fc3GzNcAAIWd2m422/eGVCjmj4R8gFHUclSqmZpWYMhS0xTSMvuGgFHSI0yREyDvIjUDzRWkcscPhLefmINNtmnqrgzbglagh3qgiJmRd5hZVvyIRy3ivi2z7NeRSpXxesew5vfXC+NZnYkCoHd+n3Frqs2hDiCd9DE3F7Bh7Fv1usnP/6Rd4e0lVkcXdcncQxsevr5fSjLyEbYs+jdbslNJAFcZbRl05pF3UO6UVV7E7mvn6QdxdpNXzufvLRJzaQwOURiH+O5/pJ+YgG8S6+aDy16R+qA2qVjo54G3FO7vtzk6t184ipvaayvaex/+3yoSX7fWuFmD2dyhxJ7LBpzw/yhMrrurqJH2gpacSGG01PzQTjGnbZsLDzwEFE0ekU97m7av8aORXrF0SCZJI3cJ43W27apPnkUWnFPKbkhxVO//pzkXgeojyR63/jdPMJuQDEtaj46CpCjEGs8AeZZ239UsJW1oFU3dvFIVymbV8M0CebYI5UNN+PLozbjBqQvVAxmvAMlaMTXYRSb0dIhxMNw4pmVNKKJraPDc7qC5N4zGVmW1QrzbodOP3q+siBtmvydo5mKfMsPaklM5qdlz9MNkpQheHC9yVWvr12MK1pU7gpYXp2d7tTmXBtSNUxCRgvVHJH5ZpglR6zoU37OWisnByCyLdPf9W6aLtpvtDX/hHRdiJSUWEL/SWnXKgqj0iO7VnL0qwTLp7YCdcjAlfkiCvYmeHy9qMp1YpzgGK9zmuYQA9ScejrcFjeRStOcQF4y2CJtYmrifxLJnaVIQvqfwG8g/88HkGdCWTc8M86WmY4BpXZPDpAIWM7gjqCEhQ3JgioACId1EwLT1AKWlDpJ1RZAPyFyh65xt8IzTSO853NGEIbZEDDY/ohIZT0B7dnIlDb2Ib/BTqzy4UkuncAPwyIEG3sVyOVjqPI+vrtOKhRNXnKAdcLavVwwZn+ZL0bkhcDKxgadjdqJkzCxsswaWfSybsGAz/vaIy9cXKjbGAmUHbK8S7h6gYZDO+bpdfKuk868I0MrRmfyvfjOzqfCHhJNmZa3QkOwfBGD1Izg2z4VYOxjWENNZDkjPwSJb5KBt0D5jfaaTQ13q0a6kS+625+YWS7So1Des9vqT1rUuEK0MTn9VPW8WmgAprUH6/tjYl4rAaw9HMA4BCeXYr0XUmk3EishYT91I6oe4eizCevWCe36OlKoelGohvtdjfaoGPizREMXSEh3vuw1xX2bnAZ3QkNGgfr2otXg0fuJVH/B5WVUFLuPezsCmNBhpXtnz29TvBRs5vWDeG6CYi5oNQf+E1ast16q++l7eTsLWyNJkYl8Pon2DgBt/TDBm4dU5YJ7aZNZneYM+mcoiJZpIG1SVhN9iuy4x/yhgkEEkVmY+WLpoB3QxxL7ztpwPxD0ahGNDfSiaHpZEG1vYq3pjOTjlvSqr2HB2qVG03SfoM+FACkeCmbbA1Ekj3najn7kOzgaC82XxrmiKh6Jd1hr6n33kvH7XdeZLDdCA7cwjcXMFFu4l58CCoPYXn3FETtqQzqe6MtSmHW37JF479Y8A4qQp59+VuYhjSbDhS+g6LaDIbot8PPXmbxdtkL83yw5XHXK0jLakn6FmmrogoytanyegI/DUQWY2YUw0mIHD4gAsIZh6jdd7TKZBDcNB73tUQDjfr0wXIXr5vvsDKFgnKzyYMY60DfTbqG3DswYTCOPb4vL7mMjNEeKl7tuyZmyy5zBkNVaJA6+MYXH/n1LrIxS4Ywyiae655BvpbZ0R/SrTkBPRqbcNGCiZXHKPakIcVKjB1FpijWOAPJxwyeY0f6TdtOiwYo0i359wondvH60F3bex9D0yImnEuK2jqog/Z/cqFa7YXl9cMsftl8RiKonwkFNjrQryGCD8o3hqhZKeXQNL7OgC8xZlMZbTL2xZjKP5lZ8GGFY2PrGbHqLVUnucR5b7lQji+kojuhTsbc84Rje146K8/d59UJHxjNXj+FachkP9Wea+It4VI2V9pV9t3hEAAu8emZSab4Oaq7wugATtJqJdMnXvqbZ1MHT0fFKUS3eTNZx4mtVaYyq2RC0ELN7kWUh0iNhSlEnP0f5BN69xw8XOiLf+Eg/LeL5fpD3++W47Dm8M3PvmuAuBtS5+/KiK8wBcOkZqmrVzvA0krMUNmIFo/TB7uGU1ZKrRQFKk3CmjznWpI5wI8dlmmPuuKUSbp2RWxLvxcx7b6BfiW5aiTcqLkivO8u8hdtf9wpkALb+EIavy60fF7Q7SmIR4xliPjCMFxbXDmbEW2V6mmoDLRCcKuh1pOuADsDQ9QxlF43YON9GcCbco/OspTer3A/nKObm3ZH+rjC0bp9gEe6M2u8O7b1jiam/lLQv7LJgJGaYFekH9nO2BT4yCywxrxQbDf34h2/rDwy9Oh9/0jS0/4kryguKYID5yzVRn8Er74XsNC0efIbubX49dfFjdkf/CnSXr5KzStPlwA7fx8gpwAwsxOlizDet/iL2wZAbAElZrDLFQZybZsOPT8W8gxY7L4ph+7/hFhFWjgKyZcxk0a0OyMgaqZEu8iDWxfAd6YgYeZ7Wc7Vu4Zj4Of5igCa5qqHefrfIqCY4KNRTA1As174rnoyjCqltDPEyo4OfBYHWwJwGrgM9L3mY88vWylBHtqicMAjHhLWaFavPHflkXvbFyo19CLiz0RgoFmXUHcDspbASu6IoEBJkVlqKXCFdv1cBT/c0xD0Gsm2mI66g728cJQa29WJraUhO7q2EZrjkdpp3tJhcjB0ep58v/cg/O+OyzSGA1DxUJancuhoYvrmxKwTpi2bdiRWWxT/rJgGoYlPxdtt9mbdyuFcbz8gHo54Yug0BL6G4y3YOWhSkvCOlhv57K/W1GPph9/QlLZ+/Y+ur4Pz6jXQvhbDXVMmhtzCaJg3wgGO3Fvmn6da5BURgmlHY5urT9r9b00EJ2/+kaGfp9KDOGPVn5FnxsbtOSQVFj6dRsSPSYKA79eglosyKbgne3q5Ft1uSc0JRcckVnycvQ7tFkuQ5SiBMlScv1KhtLnIGhDenLP6COxxiUCPWepBMJlF4l1E3UisQ4xduPix7XkyatgK3lQflK2ZmM5f65sHDuLiQKsM+DuggmW1sYxWaZc9vxHfhCPW+n8/mw9Vf+4/VD1C/h5VtcwpQiP6/TUeY2ukmTJrBa6F5AIcylrcQ+tyGlnr8xayPXiwIeSjw8MdsJYsODMhcYXSYFDFWWx3iBYL6tS56vhDQiL/ntjhx4g83c3BvEVm5vptaw2AyDq6nHVlFWO7DjV8FEXNrMf7Zg5OfxxVN9kYNaG7pBPjos3pDm3C3IhZjSNm2qg4eM0SjtXTaZ/8vJWHdgU8ZPm80IvHKXsUKQVj7ud4MpPPNb4W+sAYNLuZeFcWbkmkybDWRhJBYpgZ6WD+uPoM3ixq0wJ2m+F0LqEeZn0DzPgRxrrxAQooh6Bak5FxYvlDkO8sncya15ae6SIcJTMLY9Oo14FzVdIHCN3wnfeSCLmFSHq+kqeFs7+l9VFrHeE4XBRSnfPuxOvN1qNyEQTZIWQ4I3ORSOGSWKl5bCZn9kLTKTpbqVePJdyLX3wlwWcYtdSDwupi5VA0U69vrd5TaSoa6RzJMJq7M28cL6zbKkxi9hFC1uzqCKsmskuGKoqGqalgyjRlj5ptjgNozRrX3E1Vi25CjU03tJMFYRujIsWbuYCqf2qBT5GFa5BZV0Y22JaLbJici7dYz85QOLPETN/xwYO+0bwqgUKdLt9MdJ5fnz94H/zcNUgtwVocC6xwFaEJ06yq0Jp6rgwkLml1Wbivj/IwDNV18gGanPLiJnkkKk5bQBJVaBvFXgpruvmqZzFzydZ7VEb/mofuyXz0R6A/tQfKxlHmrjlow5wgEf3NUqMYGAvJFg1yT3T8cS3tD3QTKtziMmtxBSlnqXG11avrTbG9et0lpVuu5acCRCNASaBixhl5rAJ7IWla5O47vHco/Cp3X3djiSXeSetJ5LGhVkVXuKDabLP+uLtUQiKjke6QZ67CRnH+6h1IjPSAfnyWMKYHid3H74UHWEvSWh9dp1sb7ZbWjpElqFgX7fWhjVTtlQBurKMYH+Rd2h/bfAsnY36sUvPisnodfVeoA5Bk0sS2OelfDSvDSzuM0JKgRiZvdz5ZcmlAp+qGeBdbJVlUkzd0P8guC1t5phYdalFRF+msI6s1f+2hs4ne5Yv15lvWutBD5kMATXxaX/K7o85DXGvy8yhdbUFotoZUArcYsFpDYx6UgC1QQwUXkgSCVdG4w1S+odbjxJTYGcr2GDof3Gv9SAxcCzlabkNQZblCchcTor2bHzWHKxdf5HyB+RZEWZye8KuCUGHTl5JybNXDdto6kVRttlehGAnwPb1rOFkvxv87/RilXIkBz0UCLTi4An1t+0R1WPUv4iqWN/sZWe3x1aaj91XTE2aTanIHYu3DMCiEUkpvbRpjeNEgsJ0ycu9JFtIfVDIffazoR1zVYIUk2Xkd4mzFzAPLa3yuRZJWX7PrVgN0TDOqR5d8afNCQfQ19/jTkoX4Q2n4Mpw7lTi6VPAgmh6TKTVTPRO1bk1PeaL5n4J87IXLlkT3KkU9nJpM8EEJbT8TkmKLJgZOILgEHn4xZFvUv5CE6hZxIXbCJU3CBukYG3UDP/YNmxjNZ5A7DU9JGfYcVX0ixxkSwPR+QVoPSujSiEjGT/R5wDwdHniikmVzMt3V9qJxxM80kanpCWID5J/mEbLK7SdLXrYDoyKY5gxR5DyxgZXEclcY6yyfTD8ya+mWNGUc+2IRlKGpSEiZRA5D5M4rtG6oy/sHmAAF5SZBBbiCWoCIhYLCQD3BM/Ny4wy4LP+v5gjUAZBjWLbrPH5HnYfTd7ru58FVzIxkMvyxEv8jp0B5vE6JWoIsitdEhaPqGUp2OiETjP1wa89pe5S/XiASBzB78oyCTxQT6We2ZSNHfO1a5u9l3B4vmWcIWc9HcymvxDmsUD8ov5FEyt5ZonX5FKThwv9Yx5dqyfSJJKH/I3P4SME90sSSDN/gVZyfhTSoEwHKoEBWTOukdGQXyrCGFuQfmXCYsmaz1TtWy83bJNnNtLdjUSxkR+6PE3/6CjTy4aGKMRKqqRrVZzzErJkPFasG7rOc3vyqnqsu07U/x3zPJ+DdjJDrdQ9WM2gejm6IM9OIczPBMlJRhsCsmaRWqHJtU9k7tJr1pvvivOeeXsJZouRZAMptWACHezX3AEUb0UeQfAAba6iYiRcFV0rfgKo1LtXLKy1ObJ4NEeammkj2I6T5uZ3/XXsNYDlqN4YxRxBqszGEuEF0n4eYnVfD03iJyng3UBm/tFdEUl/6TIym5W3uDhFV6rnPZU5fPB+O5srsOxlhMQNJZloKgR9JakE9vFA2TFNtwHYN8ewmUYZL1ynPtQ5rLA2/45KHUem6Pq9S5k/Zng+37gkQyjkk1ruMBRGjYHORWrsQztlLd2xMfgUZGmiFufHmtGYn91wifOHA1S1m6YdHNZz1CFo3xjgLcVZaz52EnZOgK9/AgQOpdr0JdLb5C/cvuDOmN5qzSN7MQMN23rFvc8D0fsAzyk6xM7ItiZ5E3MLXs8CgBgY3pZeFSs0u62UvryxSHuIFvDOV5z/44XrRTZHiTJbh9PKj0poAZA+rY8B0OnOn2QY0AZQvHAsk727QQoH8Nt4IFxGz2DdJGMQsJlT1kTmTSszUC3NtQv5hI0V0R+7DpADvd6FEEOmxYB5fUdO+lvmpbx3H/Z3H/jyRv1XRAH7I1vLbQvprsJLfJPFVO/ElgOnDqUHxw8sMIH94OvU/iabHfKp/8C14OV91DTiQc63OPLKCkXMrma746a5s8zQ7kUTIIs9LGgbH1PAzEHAnDfJpqo/duMLqicR4vMm+EqvLW6iOdIBaMSgnP3xREqSv1OhwCADSun9CY3uYwJykDcUZ6jvBolsUYph4QClo7CzFQt66GvsbBNtGF+T2t5JNPKsBzCOwDecX/NApvjzqasf/6FRYaFRIaCndioBQ5F6knZj/gEsi9yAZ4A/VGOaJYvqePiY9GdTNxPyN5g0eFMGeG/pEKYFOI9fXQp0y1NBbAc1IbCVUS3IOgmZGHLK/Vm+lRLz+1Moz5jEUGfnNMLRFCh2KotSqSj+M0Pl7fNwlup9PpK1eVASWIHC1vqIJE0juZK52f1j45xDklyW4Mp4nAMnxBGvmKw8xmnvhNPfrsd1rwKyQLtwbDi+TN+fWhQ61RxiZoYWpQkeqshoLHosLZJPRAnlbhLcl8jYLb6z9fE/lXFZiOpNZc+DUNkvemNnhX476+NYKqC5qKg4OLFLm0gG2mT7xiddHQaffZeIhBALdqGYt2nYVt1Y+aMXZoe6PUTy1Z3S62nUHEBA+WE60BOLofx7dlb0Ylun9cpxciUCYBxFuWs3Zv9Qvh+Y9EZLNkbdzhTd2lj+SgWww/QV6ByELtYleMFmVsENQB6ZoylIa8wqbI+Imc9rxR1wXNWNudauKJtxxlUxqD0i5kht2v+cPi/sdxHKRqWaWjIqB3tqLvjeAdHokOjjimSlI4fipsoRa2kfJ42HMvQ+MXfGGPNPeiahuL/PkomjvjVbm4VG1ozl2WBeHiFqttztOrzxfTijG7vWE3PHqiLpdr2Z2M1IVzR2Q5Tu4eXmpl+fTtXkSV9rTZZiztCVe7K4WpPBX0Dp/vKrI65kwzFm6G3GJ8Q19OM2GEVHvXfjbZRf+Fth/1t/YuwNXYkLzsh3o9ueUM7D/rL+xDwcQhH5JgtztKmKn6xP0buiGOEfY1NBiHbwwpSCcUwJpXiSfjRasYYAef5ZvSXNElA9p1/0q6uDthfllbU4rLhyWmataV+E8LMYxIJRbPYjrct2/rOtWHvxkKiaImJyGxaS/xl/egLXbPZyCxZz7GHG1JNIRF31zGmCwllnLs+4HDjdUGiAlWCizPGF2UzA2NJH0JqQV8Uh5HyN5PCjsoinNgxWRZSRMxXtZS058l2YQ1dHVnlb6tka52hPIA+MWPFqk80KzzxKRQH1jaTVj4gVmWNQAleEkiKvG5QSBt3PQq8XFarFjc1k5LJcsM1eCl5Cbh6k+BC8Ue0CwicL4DqyYGp37CMfXpN7wgGezQ25EfByYNCqNlrJLCfPEoaL1DwKaHThOTpwVYAZTQTIPZYsqKtEJCvgr4kJr+kvMhp3z7Ob4k+eXNEVr+003VEhYbPNAwTaO2+EHGg9663TEBC3P+dPUywOn/enaJ5057Jg+GwfdazajXpRxJj4Y7/7u77kbVQSfRe76zJMG5oQ+SpI28bRYp7MTNfbNVVl6rnUiR6kv+oPjNUyWHjCWqBUp5ZlfZmyDf/gSErairMrZdDLMYAIDk4jdNBdw9DX12kV7vRqrfebCsq4cLI63SqCOiedZiRIGlRFTSeAlwHU3VIz5QVoqmNWorPWzAU1DrEqYhBLuhKXt4h0+vDv5r1Njeeai00Oy1ufPZj07d1P7vcB5slzjKcTB8CqxVG10rEihADDlECYXrQRqvPo49M8/HZodjA1do5ySRy6A+PsMnq4reD56NJue8pF+PW5VhXV2NU73gnvC1otNklXGTPC4qQpGIHoRvmTQr8ZTgai3l6IhNnF4Y0y4bLHLh5dVCOO/aMVOhYJeh+ACMWxC3LuL60gkTna8mxBF5SCLDxiNlaYXjHYDazyp17wHr2UadCiPAq1hJrUXIdWMQqnjasmBC/5ihaVnvw9Tt+PGlKV716cl3f6p3Suwc0b/26s6S2sxPLaBFh7rfU05i4omzjhTthihi2ofGUrOo8owLdDb41jZn5YZQYJIWnUYiA8BPI+GOtNgmXB6v7M99rNC3sWZnvh/ljizdvJOATkez7RYYw30J1ZoYCGRePBRAS/rMm4zLmQntp9ebxy1MA9U8pMn+TusiYjGNMU8nEWzHHYaGzYonlXx+LnVwdHwGnSTHPw8PYdeGveINTiphnLBtIAAQsm6+z4M05p1nCCm4SlelAdVmY6Eyy4m9ii0etyWKOSp8XYtEh2MH4Y+Nt0Ui+0f50Z82/jBeDg5VHaM6jh2Q6sA0JpZJh9kltnXnSNy6pol+UvAuOGKX6AbfT9TX460h4WV3FVi6xoI7Q1cCol2L3HS5GFDd2/LL07PioglgPBkGDyKFeMXQMJLJhaGgrdXipybERNHOiRTVMLNINQQHQEOIARelcBCz+C865b73MRAcbk3ZuXDeXecdTRrSFjIvT6fNcZV3Hy+RDB/0PtIBO5z+9paYB/GtKDc2w3o22W+xDyQn12Z6KgE63MDy9I3ZqKH4p1zaKuz2dQSmIDvWXuifRhAXTSSQ3IswTmoWV+j0GVf1AWF5/V/Z0eBG5zc2aW52bqQ+pC/0dSyaTKooTR3LrSa2RgH65vKoCqQmYilvcyGZJDQnGsDEeRcxQJpBZ3+cPiuDBuFLaWkOJEfQVc5UZ7YqmqDmjFwaGlstRLPCSruIYy9FyopGKT993Zu970L0Q3xdHWVmn9t6/SevevD4pTDkxAABIOdJebmHqt4B9jfaqbXG2kAqSWqVgnqtP7GcYHYD/H6TD0pYsmy6RI5OBjhyOzTuMK3YDkE2jqQiLiizQ5dS+eskZi9qttMzVzncCzUUDOAJCuQ5/PnLHp2im1XgkZj1ViBeb03SXjMpC5jwII96Oj8hrPQSVjfZv3/5S3Xjp4t7L92pdPrIablRDnbcl+qPDdAJk2wUOXur+AhaDQvUW6YMt0r7kPM2dASMBpoUx6p+HKpYGWfzmIQnJpP2Fw039E3BBI+Ma3+relOoJllVSRCg8Z1h6wNeBGu/etUhKq3bbr5siHtHCgPPmAe4Hmehlp9vCZNzgT/tP9ndUlxA2utaOuqesn8Crqt5WhZBjnl1qGo3oj+GvkXOC4s0gfFAsYLMqXC8+gX0mEpR7VHRn7NiPIsfcojLIGahlz9LjwBBSZpmehW9sNMOSha0woHQigGfeNAiDE4ZS/vKn5bxvjxIgFtlcANQ2hbAIkB5lA+rfbpRTm4IONgwG9J8BfB8eQfu/DF1Dp8sX7dIf3OIDk9WBtS3I75gN4mZl1QkQ1Jb+V1Knp33pUrj7A42PZMQnJBugUA5jNnxuKcnyoCxZec/0fG5TmxpemTaDCQVw6PVxYTu1TuP7GUnP47R2USUWosEXJ/MTc+CSzXLw2d2Ll9BcIk9J/2/tpdR3Yv2/SuN+d6SjnVZ0EdLR2JN9JK4gxpR8/mMlgckdclB5OKpflafNQESK88yDCsnowScD1eq+D4vX++hEZY0AbmkitajC6dXRAGrmZjkHWSzl+P4fzAowPD0bkoviGF6zJwkGAiQIOqoEo20o3iaHzy2YtIhXTICkuhbjvszrPGeNGaM2U51EJZB9GhkpMQE4PLFdSISAjXSqIYduEW2snzGbz7wg/CXliJClpSmhryBCja95xIMSAhCDFDFsSB2xY5CIEqhcUqklepHF/boY7W3whcEKZ+yCQQX4LHR+vcL5XBOKOcUutqiHmXC6cc9cst7LpITZUpLFynWkpFBEgvFXQ2lVsZCL87ONAYDWGAgitd3icVbvCFS7x7M+rWW8mCSNt7+d0zjGaQFYaBkR+BTKz6K6fMJCzE/4RBmAwqPAcVORTpOuyCycmQCDTbtQG/qHrRaJHmAYdGRU3kPJmSroFJpAQwElgvXNNEppR/QfAUTOb1z1oZIt7PMNbH2XAdSyNPhML9RmeoEE0AzYVXan+q96yE4Cah2PD35rvZB+wBFPmZ6qyeznDVIG/v7lgu7Ux7WgZLLsojjhlH/Z3PDCtG3NBPr6xQqGcnezxi/vzflilmQSNLqkApF9GO7BmXSe6cizw8NwoEPjh0vXb/LfjdqyyMYxsWFONCAt8YLefxoytA84DDh5bWDpExhHCKrfTn0KFEzXh1eDZmJcRXWDKxo68jIyruEmLR7jJxrbzo2yGRNxVEcn1c+B0CJVgUNFWwF81qlvPCBtwROUW9o1VaURxaxGtzmBd0E43BsijIRwCcjUR5Qd92ANMQLbCAJ3AAHMCAq1ZN7D3Y6MYsrUTYSmQdLstfiR1xvrBPaCD8Pzg5JK6lEecJSJpuv7TDvP4HVqlPT0hSg1tm5QRVKsDVtwRq5iFsg84+/LFrXtEqFdqSpmGMeLwUDPNZDosxoyh6AeN3yGfSmrcA+CvbmhiIN72XmiVOnNvrBtwl3veZt5xjrk6La2GKYy3xetEdHzAwqXGmeZEnGoXRhr7yHPyp+rBqjKYWibImW9tVvIrEK5Uh/PNiU5q/cPY0shlcK3sUvsVsLhFRIeXNzVyLl8wIZgyyjLdMTDy/SZRk9VbBMaCdlup63Ctbt9MxVeL9xiynZJs9B2/qYcxAM+d71DQF9RN/gCIrmzcaJ5Hl4BjRgYMiBBU+/aHoHqd30gLN4mSalMDX2gw4efqHW1+STO0A2mEycRGWI+7X9BXKVJ7OUuRW9hXM9ER//JuQIqxOTjDrvo4i94+bLBdeh7SiCLiGKC0VZiDkflY7qdaVEctNIaeVtSAX4cOo0LCTeSl07s5F7B4DvxTCCqXOHX/vWkKNGQHDMRMF1KrLqLZ0l1BnhlPCCJWzav7b7XZSZxGyz7CNKiyd0KsN4ehpUE/txCHr4cTKVWwrpsyZyNjoxO3UHaXGJpybRhtV+YVOme50aefvpVT3bRYGPf4HpEb3M2OGyq3+Eu8X5Zt8zckd6vNg0QikOrcHOVgxrJP77zAN3pvGesAacqxtb/I2XJlTIaRtHZJIetrXMqntPOHJErbXLCP1eD1r40Uo7xPTIw8eZf0IsXWEW05zgDYdE0ZGsxiupfst3Pt1QRMBxzzRZF9BNs+ZJcsS0nlizx+gfWe2pj6gAp0S0eZRT1pV0t764NV/TmGwHhvZ1HdARV/FhZ8h93Eq+0YzGhUQ5XPcASRx9jxd059hi2M9sNrPbp7HQkRpvTElZ4A1c3++3kWiMuJtpQoaEB8DhERLLEnex0Y5dgx2M64L1CcSp8vZDy7IwvR1JTPDkJ4qF+tr+exAxI+6rEQ3upkvQs5TjlK1EAo3qEWkUw+yTK/VkDYoW7AWU9b9VSZIM8koczHTMFFkPG+tUScO0qwhi2Nr9eBCNV9Vvy8jzhP1Z1aCmBWw1jJ1cyWMuS65vEyRMNwM1QO/1+Cfyvts3WDy8zZXX+rAQxX+5r3XQFeYehi/QNp0ln5BR8dhJEpYV6t9OtpNdgwmhytvk072CysONY8QtWaqnssyQ5cA5Y4a0+PYcgqdkE908hL+rpcryDm5NSnDzMRi8PpIrI6KUY6TrSa4Miy16Expagx+YBHiVQrEa0wjAaK2Qs7cCulZ7H6dnc9CklvZ1nIbgOaEXUTZZrAAAIe9CkaUKCSTuVzNwvXQ7uW3ESTTmYVVU6xQmqEP9aaglueDwQypMvD218b9+QPOYCwSDjtj8TPTTMMbaJvyXe9bu1+luZ+ZdZ8WKa9xjk39HvD+0PW9/S4ApOdv5nn+4XcAMc96MyKfQ9V45R+CvkOPsVPShqZpSD8bstIMlUd9/aTx1CaAuK0BTByppyU3pWsUUkXS9JBCWH10u6FfpMYVJDV6S/7vYC13wPlFslAR6yFDTKIsx8JhKaF4w6NZP8Ix3Z1ETVTEkKn8jGN0ElHew10/P7PIrYDAkyhc0aM42QvTVD5YuKo2jYvsVLKILX3KhFd34f4A4yMGDR+5MJPhBHnBBXVzHgjPzTXkRMihlaCP+aMQD60aSv3CxuxC+an8TeYBzRqmJTwUDmBwEoIO2EHKOVH9kxGuPtiJcawN3zTK/ba27WPnUnWuzNGTS7dHQrWQvZz8rKw+iUYjWttK6pPrptMiSAt7MIdSCkmyLrkdJ8dgqumUOWGKWKZTK6pw0TQI2lJb4VaOlrlZYpNa6Lan2X4aBOu5mf86YwZV6zMseQHWqAdj7mN7X6sueyCqRYT4w3ZvUv1cQcmSQNu/VgcTYkALJ5S6TXbArTdLM7Rp8YzH9ciTuF7j3q88/nyzy3kqpDAUKi1oPTRFGdCqhy4cbYePB7V/mW52VrczbRq+9B0N59IevxNNLbnIfwf8Ir6u+zVFlBbtN+JL91FOZGYv7bqPmifgnjRSv2mE4+wThokzVKUUo1wY0HVZ2fJJip/hup3EB/wkgXYTOBAzipOtLuKJ82s0k9jYgfwSFOGduDBzIxqNtgYEuJQlhUjwXtZDOe5gK9XUSafVzH88l5Zbh1qcfKI2bLru6KaPVSqwL3sa8EdSgypS8LRWJ5pDiGbiVsCyKgqRBsqCiySDJ6BKWrbt2iX1NNGk35XI0+bIlG0oVGdFfTDZyNrRxbHCNQif2TpzXvK5qEBTgdMEcUrBAHy1KXfTZy6j62OGoBPcAhN6H0eHZk1ZBCc5rASpofyRHhe1o7mqLf9T/53OpF24Njc9kt+JqPR4fyciu/DsUMGBcBuSHMMRnDkknSW+YFlph0DuTZoas9XDuxdFe1foA6Q6Aee8Io9Ybg6wb9TuWAEv1nqPmeVqftKzbsjM3X9iGEkUtk+RrDfWHhQa0CuRj0wwpPrhRcrXy2x8EK8XCYdP6GBBUEFa3vBIvR+rKwDnDsVQP1WhpenSx0ufMAqIMC8UYiXYNGh+WDBxuo0wmjDxgQ0kEILdO6UMrpHUnNfa2jFtpWHI20JGUihvfbgINZNuS7nhf9qpQbiwUps4X5+V2woro/fD14BMIBO0za/pjQY//FjvbQJUp6XrqTetQAYxMJDutGSmPm8qEjEEZqMJz7MkXoGhJ6VpBFgsQ9f++ly6xT1ugOw1q2NaIm3pdsNTnLXN2AVSlyNPiiC3Gy4EyXB2492e5UepIJnFuT5MDj4bddBSFxMvgU5f70DvPgXx/iUUzOfi7GDa6iBTQnzcUyfpNftuVRjR72PTHLyk37DX4kzJgX66o5TWOYvhm95n4yPBloMuKBh8BOEpPw1BWVQCalRa12wBn6VfzdHtA30aRAAMQVyC7GqDWgyJyOymW4VeI4+U5CJr2IPOvqGrOYxYJFsRA93tmChfC7Iqiacma57H3HRbC2mHhlRclb+jDmKSJAFR8q/YK4EHu/rfMq44+tOKB0WyhykJO/a3ifcrlIgdSe6LfTYMlf+d5OPkN5dMYKJ8GH6DNqzDcROeR6egCyZayj6fFXBtMB5LlCmUF1gdqtq74OqDW70yh+yky9yOFdcmDlpFesocGLoHOnhHDj+opZYu90msfUz/YQ7hBycBGjb3r6ydJkoPjVfcUqlacJDUi8VWchtm4Y0FUCrE9DrZLgWxcnRvI4CcxPs6eOgOuEs1a5e5DkZeXiOIy65W6jQrb0b/9TVBp1m4m73KVz1m2pX0oOJQYoyaH++QIuw4u2iZI8Rm1JzMmzfe5JrH1TFpcekm4CjkYUfbSk50mN00trroSmOLFiP1NmcgnM7uKTsHH/JVAm7RquDoa4VtdVEHJcXP8muBog8kP3344I0G0ITOmH7RrNYAyxSxq2CtRB0Rg9Ft0slnuYxjO11649LWUaeeVUV8FTY9mhdkVJ/P0/VGlJmgetNWbpBTbeLKM0tsYt7UleH2dA0XtKVe7ZJspVe7tG25FIWkWy1h8Bvu3Pli7nCnyNkp1o6tFvwhHbip5e9C1PDnb3WwGBixIqVduWYlxNmHFhqzMRZcKTqBd81aTfbxOXZct+/ifsCKnWeUoEQD76MMyNacB1Ke95hs3SKD+OjQkuqjPL/oSmwWvEbFR6SE65YwwY/0lxTiJ/UKrQSocP0T95a61/MU7qr23xQhYssqGnIAHwCmX/sNSNvADQHmFm9C2VBJ3iupOVwBSIswxstsF8Sf3agEDkU4frgRAFsNNRfbhXshQAzlUSoICxmIrdrClilVgr5zSSmnyCn3LtLnH1ULPil9TvdEQ0QRAyV5NUYJVf9n7mIBxVoEjwVRVs0qbtoe9wRaITjCfH2Q3/FzoT/C9TtiIDwFIl1pe7WNZsdqlhz12ZqLuzScWe81F2zhTkWcXanQd3hyVs20yq9dBtzwysQpq4eLUIW0U1Ok+BRAaxkk8XFjml8jCyTzq0rdsE10Nwbf0xcN5GWOl63dZmO2J/y0x7VAywhqzimzAIgTxwaKJO0F3AfuiFfNPtUZx54P3T1wmDYKrpdB09fShO7hcJtjfE1qZY6SnKUsHMkSQXR3KL4wFwNZRsImgkexGnJ7QQLsFKouLrUPEzKRirnLweoPupaHmfYbzeNYaxvSkBDB3dE8CxwKp89Zvzc/Yso+K9nbRs85xYfFPP5BhDYPK09r+7UFghgSxaHZHs55Idj869svaCcQBWKKG6I/F6LFsJs/yvZwSyRAxFvvdPZsbsksjBaVT3YiQIQa5yBNdTGW3Ui65iAkEAIh4eLqAOI17ao7yNdHQgWur9EULFuiHAvVi5ARHxaFvGDREX4StO9Nr1AGHJf7ADNsvl0IL9y3KkUyEmYxOJMUm1ib7Ck2gfKD0DQjU8ZDyema0YqK7BctXpsJzpPsTMOMkJQZBHTmkWgXZphX8uaeAaj0UVf7aS2+0kXAeFcV4gX+an3zEzZ9zhM5j8wg4vOHE0ycY9AzaCTPDirVF1WBEsU2rAlgW5SwpvGGu9yhAXtWbVrPF4QcQlXW9ohiYDAGwndw0rZ8SW+CyRowLwgCg+3hw7/buqNNyg69a6KajyU9646xpotQAL/PB/HjFTkAE3Q8f259KgBulX/I/p9oLo22tOYMWIZRiJIG1WWR3ZpgtcSlNLELPlyJxDALOF8LSlCtJms1qxDO5lJLjgoLBAu3ye5cD4L8rxhmPgy0/dweNtpTWZdRoh05ul+HieRIKk64DFl8xM3+XZRKJ4nCB5icjuSKpFYH8UPLhAPTVBv30CzhdIm13qH2E6PWDNvMVhArdygwdMvsTuOSWnYCcGzIcYcpOTTXOGbBEnBxm0q4dN4ODq2YqRM35yrB/DS8LVL6bTVEouYhu6IgKDgUdO3mXUgg2hrOjR4gDySzO8egLoSDu+Gh1NuNmmswBSQZilOGh07g3A7X53sc/ueR60XPtvpFwuYnbtzFDtOO3ZfXWzRsLzQCmtfiMOKGQnZfFLXKTu+WGpT19lqEjODheKuQfpQWjYU4Yh41j+TBJbEF4wcomUnY8NK5EK4caVP1FIzVaWiKnEkhusTxwLHBx/xIM0xO4kJ8sVxc96p6W0EwXAVi4RcdaTyOfoZXqkdsNQ1tSLsng08IQOxYfH9qmhh7EbQhoBUeyhNupB52BYT6rPjhLXgkisIl+4AVi4dUqdZVwsyytFhtrErBc5SwnUcbXS8GCgm6D+Dr2Qf/Nf0b+jpoz2GDltADAMCWWF2YkyAk44Yrd6IxMASTw2uK8N/KFp7M2q12B/A5VHA5lDC9s3R4/z7syvskhahFQSnPdNaT0k5x5f/zhSoMqdsZZz7fL+04K7oVmysN/BO3sR/wjuG4KId+uZOe4eTa0Xy90veps5wjwq+CeGWM5pQWkCe5nIZeqIKIqLaNFy29fk6Gb6W0ojFfwMHW2A+KF2AdmseTSJ7un8hcBmeBjnfVZcoyWDs4TzYsj/GhaeJK1N6HXnmfRNDRxQscezBZHALk8z6pkQXj63BR/SkIrvf0ctmKstBa2Zx/xrzS9zOBis+d7aZY6uduD8AcV+OBBmW6B/ViwJ1z3gIsjnA1nVQ60scoOrunYzudYuuBUzGZc/EXMut75em7ZySZnJnTmY82+xjdDF/SqjCi4vqKsFF6w0ggSZK21uS+2t6T9DnwELQMUx62n9SQPF8zn0fWeoIuG1sXwOpiDwfxiu6ol1z7Zhx6xCOxKe0iKK+aixv/O2C7h7MgB7hBWCWITnAC52X3geuil3w7JsPwpWY40I7THouYmnq4G9aJMRzf9Rq5JLj87fe8BQ4rTBV23O7EGtbNksDuK9FGpA482aTK/eDG6HDcRLggnsjpqRdV7614hF+tHEIytitTnDXMyyNssRWZiiBua6Mo5KXUcC+WQ7L94kHMcFmQQuElVzdIrt+ktEIUARXuuEcoGJaqMSnC0FqE4GxJEzTkJFqKS1YbPkExFlBJk6ZPwLcTULBOCZILs5TIzESzZDwLlDdyQ1oLD3UQJDfwWiJIi4DAfFCqWHAyQYxCKK1fC+DpZMylaoMCl1M4UNlcl2hjovHUnxcF77JcqHNUfaGNyjMi4nSfxjtUBS+ac/2iTV4EaI4yLeWECBdZotCVKkzkzlqeat1kAOBQrdLrhxKLEEWr0c2sq9hFPY1hThcDfu+u4YSWSyCqs/Epypf4ziX07QdvuXPc7PGOKTbPBpk/tVLW5nVEWzmNc3VzEuQ2XZe0IUfBT5u1FCIMiopyng4mUVkkQrcEorZ70FwtGlOKS8xOPI4NJA21xCAupiRtMUxj7hM4AbvREAa0Fu/wZQFWUU+TR6U01Hzk7VJ1edgSGoooSIWjurRkeQFyn5cMr16fvHqDS3wSI2TbeMuLcFgDKdpaxV1Hpq4cXBDRDNl0uVdq4yMpfXdvtkKtQoJj81Ld7A8fWwMeP82fhEwZtp+yu/3nNOTXczy9HKMggskXkDiQq4jmA8MBGpsA9vSeHA/z1bzHnezsBNKSSvhrF0hkxRFwJEPXHKo07q/tGlt101HwRc1pqlHxbptaCqfgFcxJeohXjlSKsch20kvxbWsOIX6SDoOINLyEtVXFheY6nld4v2pGZ6JY2o6XrKsWEt5Bg7cGrBAxKqBQ+M53gwb319iSeq333NcjKa5hclyz0yOi3lvaUp/mwHocH7v9aG0A/+NK6N5TzjMNYq9ge02Bx9/0ycgvjXooUQSmjJKpuyuSpEIGife4B0Ueidck2fhu7DFY6IZ2aZoE/Cu151r5LsyZ5DLggFFRW8T1Dz9yeW2Daus4UcrJ8jO0tQLPXsn0J67TrbDcrWui7VmP6lHDP4Lqx0LwC0BeoNUNvBjJY5/0eLMlsaPUKGkjzgjejZPLvI0//V14CxBa21EoogCwa3/7rWnUImrSelYZO+7NQ5bjRp4NRJbBjKRgUxQawKkWkjx+lrks9Bthfgf0m0rhdVS8AM10zpF5pTU444wr/Ti/WQ9OQ7L3OTKeNpQQvivSx/wntCgrYLorSadSuK0sCVxWuDX9DuwBXYKakRKqWi61M3XKQip3JG1JNlQ6Z9OHMmUhDddlGIIqZaXmVJcLMCxSpCzdHNewGoa3fZ15PsdK2PIzniQxxVL27xQDz3/W+RZsjfDgzRepB24mK0rvRg+J3XXCptRfOwn3vnoSYRKNVdf7JcHYULH9OgVyYiH2QU2UQXHf+XrM2HK/xPDfe2ci9KioLusNkbCriSYuAgcW3lBFuvDNmxtupOhP0/VUqgasIldm8VAxrKPiR8JmgA9cM3NvSqKV+PWn1eQjSm10Lm16638w+qSKPB5bx/q5E44+qYf/uWqPCC4tBCWqq/K7PN5JkY2EJqr257v+W7wzrnMhnDqpeO5X2hHV5ZMIUU9X1zCUkmNWWDIbwYadftl5X3vuv6sSNz0RlNKEHvfi8b9QeQ2BjmpyE5/JXQJJfwpKvCANd4eMLggN7e5IrvDKI+mTchXBNeusZWIpfkLOV/pzZYwIiU/D3jFe2vQxCxxWlpOk8YwYPVgN4Y8SKsu00o+pOYnAVe6f+86cldztWDnb2KmJDBRZ03oI5vMLyUMiKcUYiwQ0Bawzl1+ehRcnj57FkCP8GDWszMAAnERxnKcFKhql5h24F00q9kdA3Tu6xT2C2RhlU+si8Df1pOovQCnUuggeol1FBWekSK2X6lWEVl2MpqRDrddtOqUDUo1a6S0TFKFJRkBB1bp8D9HUFq0MIIkXoVUXvc1qZncNUa9ZK0PZSBcZYVuwH8CVqQgnSSnn3l/g7e0imTYy+y4GOIhcsFHbV+XshGo5kzGQR40EQjyrNHdZi1dgAF1PgaT2JyKl+rUOX9c6brbkpQ3cnJ9LysUV8zDbbF5K4J1NwzczWEl7cDO6VOZ/SbzAwC/xCKnV5oxKZt5FANWwc5sJNUvvqBBN7hp/xTRJT4lYjKZf6Au1SXr+ntxfuQCv/p67+QJxkt5SkTBN8g0pV7s8la+NJ+kpXRCph6zDStgkl7bOnaEct4gyPAzVML7CC4MTl6yiSbQGR3PI0OeP46ESQ28ySUCv98Fq2TIVP4APykx9QlgoCwatHjkMXgQEH6yauCScBPIEkOJPTpQkIBSIpj8jpf4/JHonqoPVyf9+VqOkszbNaRIh1P6hdBJVmv5h0u8f6saKNN6EmPuH+yeQmn+TrsnP5pR0vgDi9u9jT3rVyoe267foryL+GFLjqNb2glAXILi7M1lbQArDgrZPjuRu/CMu1moOU9lwuKMSa8Cz1ILTE66XGmZYl2qoWlPA4XdVBEzeICCb90uQ2pKFqEOb66iJqtAYuC5+9J5zHTVECjXpO/r1PD5fR72+uYds1XfnBfp6Xr5RfPbqMedaTW4qWFc+HWw1JDz7+XaXI8gVWfWFV1F5LvLTbp760MOlKd2w7gxyHBh0dMtzCG5Z4J0Y5zgyObZB30KWou4AoDjFfFYLbjufdpL6NqUnukoXnr0D626RbCnGI6s8qlCDzxr5fBcyn/PGJtbqMF0ytgZoCKnnEuoODV1NL+YEb2w4o0K0lh/gucfk9DvgYHt37N2YnMu5BTy9+fvbSegPmbyzjR33cZiEkkOVKwKXcniNbm8dxD/6Dg5ncBoJp3JIHcKPol1Ex32QtKMdXNy0eHiyGTAxil9z+nWJj/ACKy7cFBF37RXHioWsqVNjwkCjmBWn8yKifqGvY4LTGXHAgrfpejHuj5781jyaTT+P1UvjdX0hIQuGhQ5fysgNxOzyPIgW9NDOxtjPW1ZNVZStUR2nt1rW0xqrDhILZfB50shCOR8r0BlqAYuQ8pNqoAQzb7thrCE6Nq3PaKODQCxqEouKmNTu2mAcau0euzytXh8Ig1r/AH1zVk+NIF6uptswOvXRJe8ctXppVsGdNuk5lHbNOuHOU6vXPcObpve0zVHV0wWKA+DQLiBQvb/02iTretgsFwdxDynuOEdJ2rh9lzkqwW/YJgHJedc7yzG6Uz5QcSZ++N3k6MHLUBJW9mwVgHC5bZo3kW8t718z2RsgNIzb51jvT1Tm4BZaydKsHFeyWMKmd7uTOk3diKtjTdJfzeRmJPJ7/jeUB1USP4B2jlPgGMDCamPPEe/7m5ZUcuDXX2LhPxzp0v3o74lodryhFXQ8vG6ahSZdkqiZUplUovHDa7kFMHaQ8r2dNlf9C/aiDuxxiWhTwx9hyCSA2VaDMQpgV+JcTCKsyJfGXczn0G++fKdFfAL3u+MIbJPxsDuQz6tZLdH1CFLXJ1ImkjVJxpKOR4G9w7rK8y/bWVvFW58Cli+0eHIOocblgcnxU2CcNePNLCxz7snPe2eUjISwc2YKTWK9NX0h51PxlfIKGsd6/G5qPmGpBQN1jowD3VOz4NzVi6UAfPlJ4t59HfBPh96EP3rVv0Odjt6TlGz7huf1z+LsFjhn4bxpPnxufx1ShNCH9m82CpzPa3+AcH8+A8sDLvNaXslL8vzgQ0abLVyILqll7pGUEz3cYtAxUfWCCPlZxvGJnnrH3xosuDdvMs05T4QOXNiXxVU5O6/z5F3YRC8Qm6iaKijAPaOcxM8QUTad7DtchVRc7ouJCVZrtVP6cBDd1Jdib2AsFlCq1eCClwj9/k1HZB8O44uwzoOBbQKJ2O0RFA4OTGgqOrVDBIHedsMfroNW/R1Utmmp45y9DfrGjYUYXMSnSLgXjlkDDhRx1l2TR9aUWQ+EVRbtU52GDjETVCFEj2crTvkruf++sUPlNprVcH2M5fJbHdi4LXCJmPzPY2PnM6s0ECjCGRAamZDvtByjqzoTT8vC5Y6/fO3cbY5IjpHwhiIwY3NVKynUpENmFIPbiliDg4Lp/CSxuHGFdC5tYpfp7VIUChpdDW/m2MAxNXEw5iTP48Mo9Epn0yrJh9nQeC31KGKfVbBpkbfsNrnGj2Plmk31QiXAhnFACBqUsLPS4P+67VO24TJ9Znd30TyVKgGJY+EA7bnRfPfXRxrpHxTSwuVrvxV2nPeXSr8sb0I4CCBqzCCY5Xy41srrnPhOtirL8rz917jXrig0zo0gnGw3GvN/YQoHjv9b9/9O1LDc/lcDje2y09/BgsLm60K0Uz1iiOPQfTgYczkF19rZE++VXq2Dbq0rw0944zgwsJTUheYoJ80q4wnFtZxUq0Ic/Eu7/5rMyB76IHzYF647hZSi0VERahkENUYsdc6yJppRgGL/6o13BG3sRu2Q3uXCFnI1+uzhMFK7VlfGGWh9w/rPQ9JVn84q6wWkFxK1yJE9ujvRLoop6Qlx5q0auymb39zUKd4CvrlwOzlwvt4yHqDj6gYgYRWHqFJ9SJDdy9nAugeWM/kGiZD92iFS9Su3KkAHg/UBJ9nrEUFCSrzpo7cOFPIJ9FqUjEV2nsw1JEnfkyLeU516QicZ10jdXHc5fr2QJT2iV3n0Kj+SRaQcsFukYHnS5iGtyFi+1dTbIWx5AcMSIccnhq5QKr23ew9ypWekaLMRidw/O9AsfUZ1+ux+V5zi9BtBVH3Ydj9Fo418Fky+JeqrJ22GBtXoAv0+u6zdJrpEuD2NwStyyGcN8uMvk7L9+hWZ2ZeDqR5hRZuPbfchHUCyDRh6TZDeMY+ZTZwmRDnm9OnmuAaHAnLeroLeWUCQejwcQdd2hHAkeh7UWDA0Sbc4C5LfM7LiqMR+wqx6tQ81rQfCsO8VQSkl3qAReSM3qRIVMK9jEpcuQv4uGaQA9paA1LT4qofLIyM6sC6vagxTq2YX7ML2/v5mj1YaebuBVzZ1+zprbyvR/2j/yMHnqu727luhIVzI+XtwQnsQ39+rtj0K+93wCyaIq132woz9Rdnlluzfv0PtMmvvs74WTAXSyzlWutjP2qG41ZhDoFY/gS9aWV96wGvH6/IqYY4LSlUjwuxkxcu2Na1F9uOrA3m1mwqNZJO9bhcqwrJQJcJOSYm+kHPH7/qPF8P55vdQBJQbvLmWiiJZ2x28k4bopFDnjz0YmHGpen6tt3cP9U8nRalRBGDJcw5mbBJ/U/vdmQiseJiBy0NxveY00NV2pvsztu6k/GNJ0gCzNKZcLKjHp5v15C+3hHGB5yHdtpsZnAhQD/RxaVw0RV5mGF/CuMg/UfTYFCrzcqUHuCg6e/0dXg9ag/uBFB3dfoQCM/AwN5d1y0yeNDxeYGhdI1YGKCNWDUK/FVD4Zm1T+Fb11tSfqlIy2GGykUOUh7QSA4bR9928dBNFkLUHbr3MLJg2R6IQEZxz+mGj8uCuSM/2pUhlbPk1p7mE04z2d0NWGffr3nummcMprKKGUtHlhZorX55NueKxi28ky6xpQAHqrSN+x8JxWeIzcs1n+969B0UXK08/p5CNd0KDQ+kTaOWanTDV5VCInvKElhcBwgWoE/yFtbykPV6BpHxiFgaUlxapPGAf/SqtOh6UfykEm5ZiVvnlkTZVeKnvBIhaz51SdYOccqkrQYmtqoiLYqkB7eVCo4D5ILei5PB1PSJZeiFXvpW+642oITxFCSTw5feRDXbcIpUJfEUzjpV8IzGfs7kNq4htP3cJxxvzFpgRHAe/4wFR/lhHQxCpnzl7d9T9UX1hyMM0c3BmPFPvqeU/EoLryPmUeTLoTgoxze8e3GFiODOk7WbDaa5p1WlbS9Eg5EedVrzJZL7/Vjj04dspCGoAaXwhR1TvDsgtJYwe6uwohjjndqOZMJrVvY50dcQh3FjTn62mL2VVCRzOwEV6QYW6YUfIkBWpy8ZBhGzeeLPtVzveWGEt5gLXcFdTwEVD9i1cqYhxeIKYCVgXY7dw8eNzhO6lSJhrRI7vmz8Tq8Kycq7g7gaGB39tyF6yCNYEO7fTVAuLlgMqbMaWstvyYT9Cyafzj7ERFD+C7OE/xKf5LjZzUAsuBjawHC4VvjIMZG9m0mmeVsPeY44uuf0mo6h4j/I3c5dVRoa9SYqmeBsnQfEWZLfc/MDkPfSitLyemRSwaxmYD0LNBn7YQGOY70K1kUndB1LJ9drqZAoJ4qOEyUeFLIiixBlcxkKtDO5Y4u/7+Me5V9aPG8PaE1+3CgQ7rhQAFBq95npoxheca0Yp5neseMq2xOxPXPVch1VaFRcvDcnQjn8UL7rNbV45eXMM9NgVFsrjcsAh4GAMCUVjnSL6B2enFwtAMac5/7Ao3HZg6iR26Y5QgN9XJ8J9xXQmej4UBpMrciDVMpsC6I0TsYF7AXvO6eEGNkgqPGwQ5NNIrU3bjX+CVMIzjovkzc9YdAmdbO/xXb/XXr+W+hu+68+EcbO82dQZF0AjFI+Xr7wgmoJ2qZQPoeHgAhgf8HUtuFngjMWoFsXjGXJvTEe3VXCy0KlESPbOpAjxdJZ5cGXD8o4W6tQ25/q32ssF1+iZbj+ndgJtE5Wkp0yzCppBtqtab//A2+2y14oL97ynUeSL6rPArL5AZCira6lhcVLPYGkxBlB1kMgyZxZSIzvbJjljmrI4KHES9YsaYmUf6IsmTZGNt8Bcfiuqo+1ZJMA4qIUkYaUzM6iYxp/c67TBaGJWNIsw9E+kqIEdIpqcpqsCTJyyYqfJj6AGflIygSkrwEEPRpocDCqDW3cvUo/2m/DV5J0h3NUEfswJAVtNRvYJ4OK+Fne9MNixf6KWVaN0AgCUXeuNQ9cXLYr4iPL0eD5rkwVKhlQm/t7G/zoeiaDl8iku1MmuqCI/CZhLtFD3piywU32mXVGedJI469LCjZn2G4bD0U0ldG6FhNh0NA0wihMUMEnlMIWm7DGzDMacgITftWfPeP5EapUygU2ROmYw4w00DkzwtmV85Ko9V73h3Ua79mAupfeY1KMP64tveJcAxc6+Sh+CYo5icsFPm/nAj/+ZDxBfUbpFbFZULnWFOevwSB2OfOIPxWeGJtxFEFQMnb/bvQTpaiOOZiGMPMeY+NWdqDNyKh+MB/ZLkHTLh+C8w8gg2VGDpslc5rDDCqjy6AzTRUXEQpaqM824wpai2iqi0Vzm+5z9s4FnmQ1MuE8FjNnSpWrpUjJMZiEzLodPZQoy0eJbqRiItpRJNGpH6szJgXtH9Ok2xSPm2/A3S0cHxXwmVdV5WTBssnfy9/QJMHs20U0Ji/aEkCrsTbeGDu5I/V4QnGI1ntwcY3/OhonsEBXOdoQOHUfMWWbpzU/c9THs7XX3wRXzEffFLJi8U0cgHN4NzqSfkoCisQ8SRwHxAtL3loC9bNNOuHnFo/lENT+YQkvUDE22czZG2fFaCVhPTXTRd1tH6JP8sBUNMs6reZZ5kTEjDQlbCbEc5r5Km3VeLuFEQmvY7UyLdV5Fc+0Rc3gvxDsh+nHFjd1RHnJiJ2gUvUKcxDhMWIA3zUMG5oKsAWJw2MorL9iLzH8VudRy4UxxQGrmpeUicmtOnD9kj2xquc6HXX6T2XFdEntdDaa0HIu6w5deHKIYRyRXg58rarixMItKhuxOWgLlYOrWZfX2jz/7KFZaP3d+GPNhinwimYCsB7bTUaaSrTEq9HDGZhIFkMm45JgzM9ug5oMEzO27gKNdMK2YZl8Idk1Wd4hLyBKITro1WRK3t2UIKF6/licANhX3N1kOfWbaVAgrkBc5eLSn03Ce7mVKIqtBToyjE71tmRJqEEeeoaFt5ke9pt13zA1sLMiM1JSVwKNgfSJXPN3HT1ShjXk/L8lNPQowscDSSvTTjdXChOcdo2sjXXTjefVt6acwRtRW9ygV7FhRmxUmXJFZMRXtrQuK9JtQxjdekH0Z9fTfB6y2QOwzgWq9At3NqWu2KjxZSMfpYmZIJeDVSQpb+Os70E3f3LCiph/w3KUifRdid7BofZq6xcTh3sK8NYBTpEFAvCxx4S7MGbkwpuDV4TVXUoEZw8R5cJ/ecE0SOcNCQMO1acKEGbovW6akG0tAkO4ArQhlnbVAsY5OOx1MhiKMGsN7JmfsrRoZmBzfx1yIWH8PkwyIm9ia4PVjAq4BSyeGCDs1rbM/0m7AUmCQK4HYTVvY9fPsjRA+pp3cBwP675IF7kl09BHtke6OsqzgO4XLStxy+7CoLkGbXZXcgCEr4bxobTCF65YBGUgKCOKcFLcmg1gErCw1SJ0y3ILiJc/5m8qkKJqgUZIwrJ4/iO01yfFwCuAOZbdTbuocY4Yt+5bJdq7Pk7jw0yJXC1YTta9UgjgJLiay1U9N1TFXPK6Miv6M0wp0+w7PWZ2eWfqCRkuej2QnK+mqQ1Q3q5fZHdcbdY+1gKcse+aCuvUlJglGT5fHQg67m70a8n1IpBv0IOEl9qos5jjC81BeEMj5ZX5H+tNEuX/6LMtFgzk7nKPF+KEFKC8pZjTQ3J7ZLQDPBDgzWpQ8IzaatfcyuDVEyo02i62fy4PlF+yxGb8jUQfEoHTrpfKGYy2Yf2bmZcfc4NdP6ygzs1e9r5H8CWYwvE5egIfxV0XLhR1sjuUjvI56tyBgFZeJsTvhhE5JSq+ymzMBFdRPflgh5oWUUEBwokWHiMEQeWAGaj8VSajJlREbKP1IJKEqO6Cg4iXBRyW2x49W0EGz2JR92f5iQXIdlj4dO3EDnmjWvwxExQ4oP9T18grlTJAOOR/LhBi8zIdlZADtWcdTNMUXUTyEl7N6j7NS6UOxB7aS99/GIg/hoIvyrF7VLMfSZb9qzVPVw9nb3yHDZMoCOo3Mtjq8k0yJF5bfzR0QxZd04GcCejwKpaMxAUjTZ8Qp5CrAkt0qtEFhlyuoXDOeNAdhrdB7OX1FxnnYxdxNvEuMtTAvnkvIoC0YtRrPy22zKjAcD/3NmC1Ir18Ipaph/Zr14nAV9sjCwjg8rNesv+00XJInyANZQfkDX/hvF6dc3osjLUjIRFDXNDvHfg83mWv2w/uC1AafOpR7poOWkEX1nUfoAEVKzSUm/oilCrSH6evf7YUgPrUUhfiULAcCijGLyzY1iM2n4+CwLsbYDUHh5SXbcEwPV4/xFI13ErscMQy8Qbny3heKbXbhQ7seRqkaCXsPBwaP8kbRVYH6aA/QT5VJa2EyX9QYBBY+fHxUDz0evLdLM4Axz4nmApLlzGiqUatMG1TIjJ3yfIwarOrQIeM0T6MRZBcWQm091XmW9//9eVGxoorTfQXRztaDHPdIQSb0M3bDYcjMl1c1ewNXXAoXJLwnCxa6kqgXUR4K8QRWXa4n3G9XkO5yUl+sx16zyYpnkKxAU8psNVeGWHV7RUJ9Wc4HS3FrgJqS+mEzwoXY4GrREkAn75upAJ59KKKBnlFhpWAei+z+x4euVexJel7H5QmGJMebFGVCyiGAMynKxeW2XiW7CkDPrVbb/SlCqVTIF8QD6F0GYaV0UjtSxZaxvvJbArU9oQdPJJ6SpeapG9IMjrqLCV6WgrUATDe76U7NHfs4UrnIse6hphg1OqagYYQdFG2DUAhGPnE01hlQASdiLq41bw2gLcUsg+f4aUDlooL4gjluALwC46bmaqQNyxNts44MiVeKzCq4y3Lc1wo60VYf4pDtcSpt6ubfdXWJQhpyKt7NYcMTNt2nn4Al1goANpmimQEQc2biTFE63qGIYmbL7W0sVaYw3mDY3AV+Lc1zlOskIZsBWzcljbqzn5x8YDuvG/aTfbHpZ2j8VJ3SpvJmaErb7jQvhbkX6QYcUeYVb45Gr9vtG/Rwz8yywzyRszbyHbIbW8Rx94YEqwa1sASEBYgyIfwRfq9PpOjED+Uw/NhKGeVKnNF6vWsmsWCUnbJcbqsvwRpv8DlAfDLf+R5uKyUQqQVKo64Go7S9eQfrE0BQEwciTeA1XJ1Z3gJMzLCOcOkpqxMgOB1lrX2kENurvm+3JBc1x86FLdH7uQu8ljuyYudFyKT38W2nSnG1LX/U7rTlxbfN0GDVzKD3oHkpolexoUjX3FRcyA08ElbkXKl4WQfcphkpCvLLJaVSnjFWQ6w7qAu04gqIeSX+lRj4emm21ZjLb2WCDO0LIZV5BcIJYjGt4LR4tCw6gkDaGRBClTZsE9NYdPGaA3q8gRylebqMRgcxy3z34dmXwagzN/AMy1qy58U5QraCuXsCq17eviZAoZ2XTw9hZf89ryW3aAB2XqWrnK7ZMdITwTPXWCglVLWYzrqvCLiHm8BZCw5/Lf7qe8VzNewWVmqz65qji7Ymlnj5LjiAb5wJC7i1ol1csXOZdX/E0NlF7WWoy8IQLh0YRgUt5pvfBnRt6Np80ZbvR+8wp3YkwqpQYHSlvQhx1VJQ5qMPZzvzWHWTi9ReOTSyjN5Ae+WJyOdWQHEGG+sDMMU7tYRTIACxFmQtAgx4ypV137qeltR8y6RnyH6JRcdlY0PDSFiSsVSu10anG//qKq2TB1ZQhVw1ItEpZqbhosfNYwvU1sEsLziOkR1wl/BoXFo+77YUwYyMaFCJCZ6kFBghak8O69a3NtZ8wUhi5WBwvrWVhEuQ/fTbyY1ECY2wBx6ib7UQzF+qba0ouvb9YR1wox+ylUkcnCJU9JW9VQTwoUsJPv/ppSRP1wYj4EA7NPeUwCzDix5aUFXoqaJ5vWSPo7TJqv8GrgSfcL3Adk34exry1P2Y/TeK5AuWminUi8l+t35lyXhiJZVKwVVcjku25buaaLFeDUxzJAQtEGzmCCtjAojVwCWKhViiYfBKdHo2lgWnisgjTN3yS31iv5nJbtEkIdiAOYR8DPwUjHdUAKAwDYjdFuRRlrStWp+qJSQT8m40FwDrkgbEYFVwnSijFFPdoelsXC4NgqJoRtY1MMBd1sRGDVqQHyJJZwNyd0cGQUYwHLLI4dhKRArF0b1UtfQJA0jPcmgTS4IGTZ2SouWQIEkavhVI0/KbIT8wIH60AYLDfeCLPNxwiWggfMsRdd61fZmsS3MwSK4VIufKu2uSCrU7RSE1YEnXQNaVAmqAKqQLeViYhbxGIylCnvOO+5EY4LiMbZHMZAvhh6Giy8N0duuNlFM015woR47IjIPRylV1mS5P8proWM/NnC6wCrVGHFixnx6QAWKwFXsYyY0gG59oU83hqJrxi2VipFYFIxawB/L2j2CtwcP7gIey9AWkZIO+IMavGTF4nlyBNjsGYrsq0WyrWHgxKMEYt5b1q2qvBYF0pUNrQcQWA+g2Q+OWQ143JXyHO04thfYAP2T43O55s+dOyjTaPRL8nPKw7r5tLOuFXFueDVgT0Qo6tKvPHKfuaAHGA2wIFe/i6+GKt9CBT8UqkMUth7Pu4VmLE+mkProVa1HObyzaquieNdeWsa5H2CWIy8977Wh2aq0hLdO1dIGLKTlQ1uKhxBxczzroVN8D/qlC3ZL99ql+Ll9PivHoswtbcE/i4BaC923QsFeIwfleQZlP6ep7Xd0mm766BPMyjBaValNuFGjoE4gpJWw3LV9iJW102qBnlTpAjOxrvVB1L2JaHq6iJNHb9LhE5FuhGrCffE4llSHfuVJ4lnwmWkTVi51rgJvAwmmQRctpRWa6+21GKrAe5sIZvvJV6DYPl3ZZW9zdoTMWCB7LbqrrLOZTvSFATFGqwHTK53wLLz+WU2zlx+heBWesAY8cFfN0pls9aotxW1k0Xs1kUi+7W6/5zIanhNzcIB4ijFsPqU8kb5NtR0pBfmQSG7V0GLcyU4k4QfTSFseeYxrN59vEhOsNeWHqDuq775eICcPetcYRZVx85AfuydoMJqFz6w4xrsK5cnM67bWGEfgpzH5rpqHWE/SCjC30o93695tbRyTI44ezXqEj/5joN8WRF637BnGRWG+NPMV9382S+5z7reYCjc/9blg6iM02nx8d046LDZhK+DFfOgHhKvt8YLbKRn0f14DfubuBkeRp+El3xDJVr5ajwV1NbsvH7cs30eKzoJ46P3iWEirqJetecgHiWF2J7qqE9Caz+In4ws5emvnOceiL53k+cJv5vNPJRZuAeafThOi9fbKd9iYORdZWR0/IRE/0mPvphl4RdiH+StOB53vgdcg42mkr12pLtD+IZdbu1zd/l+/eBwVgIMuNxSOddbaKrEsK7THjBnzA8LxkXmL+DfKTeU1WtsRT5mtpWsvE43EsUWKiERA8YUTdQpC50rHS+nemVVBkYDi4vAGFK9hskbNq4lWJjn6v5FlPohaCtcZOhIPqI6zzvB8c57Z33ausXwc35zi5fJmMEbxdnm7vgY0/K9Yn31V9dsYZWnwqNl9PxO+eDs5lKu54YHQL71ZnmfnFn8rXEG4eiBxlDKaIy05+qDMviNdj8+rAweHFBMoiFmye2n/VINx7s9BuXRRbEIS5OMIR6oWwb9Aixwni+z9HfHqsqpa8KPmlO3be+zJbiEdRxmXqCYS+Wh85NJuIbx7/2PrggDErXnUawWr8e9qpMq9cziD1KalGSiHUzOKRpO2hGmhzE9A5eE/ncxdMjykElEMtmhCogr5Fp/XvohmKFC5XucBP4z+z/zTmM5v3pCGwLurG1t1Ra1RItklPCfBMG5HFJsFL0a6DZ4m1SCObuyFdrGiZdkxLK+7cvS2lfDo2rN0PRlovM1A5YFscanHAVeVnAPnZ/ZCdcnRpgxrNKSpk+nxZ9p8jPByQR/k9DSI2J59gWGv0SHzWYukWi9AcUmoN2R05gIguvwrzfdrObgM9dtB4SQDbq+OtHGBBKZnet7GCB7yQy6NO4NaAb6z0oIx/TApBYh2970DWUVIpBcDYSlSb/VdM7jrgSkeWdEteOTDMia8HeIWhseVJRAogRQK+37gZZg+FoU5MNLI1miNXEe21SDJuKpCMgDfAQ5xISzanoS18QlUtMBwPc9Rrc+lQNxVgP5AOp/XAPgbmSTlvJ13iqWiNdeLeJnFafAPWs+1da/ZGU/sBLSlsauPEI0isJ6c8hPQckciYvS3YysxofJUP0oRyWsWJZ5odyy8d1rnD1PM7YThoqgWXKDffGeW05lTSnYl1VK7veldNjKtRX6AAw+L1ygXmL7/J9Ac7WIxZ4uR2DRVFKz1k0HQFIv9ukQuhdLEcGrwh8lkDWRgOnlzl0h9JkK7cs0x3Dy2sdF470y56kDvuR4d0g4KbKyPJXn4+33NoNmjIDodUdCokPI5qCUZEyLFWAcUN0/gZo243AdBoHC2hrKo9xr01qcxFHO1pEjhCzvlNoF+Cf8cglrJJRlBRUlH8ySPJRHGyeD0ujbIg1reaJ7Sq2EJrlL+25b+baEiZggWXo5Fb2k8cevGvYggjb1sSd0nhLftRojls4I2jp5zTh8PBgOHWezYrn94xC88g9DDAUTuSmwNs7gfRxlszPL3x7xvJq+a1oK9AABFlc5CsXrViApDTmJSwLqpNR0upSpCTOFKLj9RMbXvkNnS88tXqeLPyW+8vfHR+Cv9hDI++FTeXDvTNcywI1m41WC/e0PKsafykOYheQ5HdMjUI6LoMkhG2vR75DpVP/dhfAhgj/kyZykyrG4lKeH3rkkB16NMZPghcqq5Su/BlQlSwMLhlF6TAQ5205kQkZYEFKhzEYPoapjPLQ/QhQbqumcnt2U/vzIswY9OJxRVgKRZxudU2oAdJBKoG7YcHcc6qhFDz4kfx2yC39TTRBxKXl5mVxu6ZTZDc1qXlmMYmI+jTLiafSao3J8u7LTnSSqcSG/l/n8TFKYABrmOrgn9eVwwgTPg/OiYoGGaZfth3fdHtaqpos/wN5lE9LrLawoV8WyL/cMWTew/vpW+WTOVVYYbFJaCCKYOayYTmQT4ounL4mtiJWtvurBKbm9flAsUHVWxOVCQlx9aWAhhoNf9ge+UAGj54Xx7ux7T1zvBe+IO10qBGmJAOWitNQ7MzHWaD262kb7X2b6E28DDbzmPeXvRCcpKjgeViZl3qqHyQM7ztx/PQRgooMCyr8Hai7/GjR/4OL5pp/nbi++rH2W0IdJs+RxkOlQqDxaeAEDqeLysGtxW6I9eS53xYaor1Mn8WxQnrvLMBVVH6zq5yKDqHRltnxA1lHSfKssIU45dQHo+s/cnGNq6Vj+jfUuk9u/6UOMASsa95Z8KK9A+OScKehKhy2Gej9PfJWDYAP+CNivSqXJ/IwKU95lMsmdfqjC+V5/qEvA10BFrsplRu4ddVOFuF+uQi2XRhP6gvy/Ugo3kQzMBcHavSQnkuc3jJDUMgjg8smzfABIMfJkf+Mjlv8OYrpjEGd4Ci+K4ywOo8RMEuDtfD6Y7SA+FekyUzPA+reCXpDeTMC4sBL8hSCMepw0GO9oYEhiQRwqkbL0qaDh4k+PEMdIjTAomFCxdWPX2WJSGjmNYC2DDUMDhTVi6eXut8QcQLSKNyd67q0cvMonBq5u2diyx7hqiG0BbWr0S1SusJUGQ+mhvSm1txt3bqM4Pc8uGmSFwJn55HybUE9n4fkVTdXyJl3FfaA8nDCmgl4riZiGKI1cEDA5BbZODMdSiYFBpQffvIYwS/nI5Z9AWnYe9Cff9p+lJ4LzGTmIF2B+vC747myxK9baRzbyuJSJLg13A0E8fM8Isj9y+CWyFUH7W6vII8vzegn+uRHGQGAWkxu63Lcpe5uNH8U/se8/T0XALRIsZGyW7kY4IoFWJ1NrQ9XFABu0RqsO3o5Flk07eJfvdum9WaCMo0QiWU6j+s6QC/LKCmu6fFz8cU04LYump1IEGK8xPl0WEh1B7E7EGCfs1bhp5Ph7wun9UEtxC3gXltJlRwsCMkan4sNKedP5rw0/kHKU9+EN898PNaUi801F+ToIua10BZlvGC+khJ5fIRULGH/eLNbl2Ia9G89ZDhtS6wnwCjXU6XadCxFFeWuAjZRIJQs2zyq8V/bg/jEH+oE9gBdhE20+MaOp0PvCfsdh9CKo1lLvumXv8XQHpUqoSBuE4GHjbK1RAqGNLqADpOVqUS6q7lDWL/9VUWTH4x7T5koAkR9p/MWV5ej0vhHxDBves5HFgyU52Ucby7Xv2zAO1nC/ZU6nUIzjFPM/F6i5CGje8ksLHJ7cGWi17TEtBY811ZcLXCp5AS0RHDKiGIqrGFGsbHRGe6yZm/11s8OZrh1B/cUUULJ84b5jVa62eI8ra7btfNeUbC9kUuUTN+3Ol7KCVomHs/8boZVdD1J+jlpJdTAC62lJBdPmALrhDaptIegpASjNo5HjPQKExtOMBCZp/FO2TdQFQ1bsUGDhDby7gkrRSUFXhNbmXZ7+y7TWOqz9IXu9Y5gBDA+uUxi51HvZes5Hw+i3ShZ6A8PfVcVuvGSvyA7HWIFG2bBPxeG7k8+JwsDifMBS+v0XiY9etizy11Y+os0bL3hxNGv1xYauLktW6GV2m1+5/z4ZLmJp+RqooUXicrno+wlrHibbLVWDisMG8a6XmJZGrzUz716twXEtxsZ7OGIJYuukDcpynAQdO8F2xM9PXp9m4V/uq3hsJmevMJYkDjpi6Ph9Vgnvm3fwGZ4eL+c9UQwcn9K39byrgRy7P95W/ejqlG2HuglC2/46cIDOyBQXbhRMzRJtZB12sgGh9sYG/Ga4AiNYv2Z2yo34NRMLfEwHoX4g3elY4g2S8G/S/fBLyQkDCf6KGLxwFp1rq7R8zqp2p785W078Q3Q/yQD5FRD/lNHqaocrTzwZDYjhlhfyvt15glNEyt+iLohCnut1d+VDOx9s0SMy5MD81g7W/YZhJ022eggZncr0PrYdTNzKeQN8yNy7FCD+4dp/c9hUEod/jP8J61jg4eXyoH+erpJ0WC/qftauHFs6a1VPv/AlhrEem9xHO7v9r4CZo8OPL6Lic9aIrszK8s2h+xP8wTE75LqsQgnc3Mk748UEz/BKH2tC8PTvonnHz0uQrx/jCjk8oY4APqiZLkH7XuKJlADNLnRAZYKsuOj5mCE/sUFZ7dQ+LxPU/NQVN8bYvyV6/fzCtMPSEh51NeylNYgFh0QcFJWkj88AqUHNN78x1xsu2Vxvrr/17RkuOpLWVElapo81I2jQDiLqA8nFMxpQq4tYaeXqClL/bTNyJresSh30lDApD5aDuGCCbIPOMjWjheT8zLrFILq8HvpHVe1mfAjviqSc7qQBwUFgD4ePUSW4KoJoNeak/tMA9KAbRR6Waz5lsrsKEYwtDUqVwJ60mSfOp+Yt7NANZEWKsbuBVRwDYOBCxxqGAbBx42cKhM4SVuHVTm7iq3CiiTdiu3BihY5UDBNg48bOJQA6ARTfna7F/kUMIihxI2cmBMsBVu2hMm5DZu0hOwxoGBNQ4MbOdAwhYOLKxwQLCBAwnbOBDFrgf8lWAbhwyVboTu/WLBMdwsr2AbhwzHzzAAjuNQQzxKsNFWTtp+AEH3yCPxBX5IAb9P6BiDZkWPUKjWDWnXh5zAUYSE9TIXVzo4UJVY/R3TSezuO5F8SAevfFl0t/fkmxDeHtMor6Rcy2jDGltag5Fb1NBDC47aND5c3c5i9Oqw+bJuEcWRCsRUWtR3Etcc65mMpdR3DYewTngPWfWT42u+dIysEMcHX2GgLxMGr9nUY+gkQ8IP4wboqBbRMbvBXi+WB1t8jVjaa8SOLthJNg9RH59z9AZXu+ckc2pVbiRWYkU/ZWCAqAF9/uCEMXHq1ZZeMN1ZdDsebY6QsTTIQ1+FsAce23Kq0PQyPPW5qcwQZyxG+abZtWqJWlcn13oY0a+yms/O2K5tvhMYRZ2y1w/c0BgZq0YHKdNxslw5itS3e0zheNPzwIqtid7GfJlw2FvC3nDasIcVaiW9dFBNBWQqVMnKMcho7WKMWlEhD4ETxUwAb/n0ryTd4aHuAkkra9+9I6ut9R5aiMNwNlUvk0fBjnfesF+2qSMD7mZlcWMJ7C6s5yf2F6UPBC9ru2eI0ZaR/SJhFQj3TwVGjmzMeDBEhCyCw2FZa2uar4duQChqwwq9Ll15OH3AdQV0U6JnDfnbdVZroRsSCyIZsXbF28J/Ifxi2aKxeAKwqtbxRiD3BKB6yuVGhiYsJh5GwVE2CmT3/KODrSIG0HO07CQnxNV9a4qlyHC+WtLohK2QwGRKwIN+xkX7uwLsSAyktazKV5Tz58WGJHc7YYviTbMhWsILvIswSMUwaR9vV5WLrLbsgX9/ifUWj7ilIq3tmwGX/PaUBHr8y7+X1C4DwVcztnRSR49ynPQl1NCE+nCTqRW1KQSZdAhznYfu4N0Fxt6mAd++YKGYOeIuBv+ZZClr8jn1HcYkpcYbnu4d4xkfS/WmhSWG1eDqNhof7n659uwUJ0zzFqruJVQQKq6SJBm6jvKqySPEu6vTDmUokPc1YBhnGYXFWulCejK87LuNGap3ZpAbe4P/xJYcJd9As/Pv2gsxIhKLrS7kSFnmJ20p+9/1t9APkDkB/TnkItZCUyba4j4igh40Z6NEwwMj0liJWUA38UCt0NzgSe8QPd2x6YzQrx0bMactp1MsdE3eGXEtdHAk7pQeXIeeCt/p68NCiaeTwHP+nqO0UOGwMtuHtNNBsuARq0COQJ3jEt/9OIhxe84AGJkpojVMp8I9VJJZZ8Y8pVBXNGIXCtnxCYtKDFyrJ05UK1WZZzNfhzeSb8+M170/JlXWcTv8DyYxVcuvdtSR+moG+l9qGuWfuf0bkQ47jtHJv8PrPeD5DVQEtSeg+FK2R5HWLb87Wf6xQcbb+S2vPe1uMyVsENTWmBsnhgGTHtpALahb2tlGfeCQB5EbbxCGU9kSrZr7UUEET6lYFcCOvPNNsMnt7PluywNAcurDfUnGP6ChLFNrMgW149637uTcgTM53fXEvS+kTNEjD1d+LUaFVMVMYai4q+mLWUh0RGYmCK5s6Dl+YZzGCN2fveJbHhUBR8kZK1l1iQsWWq+791XX7UGOVIAXJQHBYbnoedQJKGr7omilesHGj8fmuO4/lHZl0PxQeSP1TLlyfKB2OITaiSY49W+q3U5ZDVU/Fkp1lxR3Yn00viPKPHyAQ/zUlHt3M34ffTo/yCxQ9JBY3s9TzUQV74gCRd9y5MHZfgwhxPVX50Vs26c7AvHaUQlZrpKTD2ntwcbEkDKL/oz+15wcuVi95Sr597ZoA1KIx2nDCXs1ql6220wM/eBVCtDL6eH6ZGboOSX3PBf+81Z7oMaoOOnsz/ALVPOlzYfaW31N/Y33z/CrnqyUiYD1CmqOc6OSD5VaKfyW49qo3yy0O3rqXDAGuiopVEDQqu1Typc/VQKqkkBthOgfbfr0r5+mRqUaUXGiNqpJ1UbUnqiNrqlfX32Jmg6fV3dreGdMA0ng+ozi+kZ6b+eJ+t1F5VnljxwsRe12pyrxUx91EJ+L0LpDlVIZQS0MH2l7C1kQMNV5nVT0i24aQ74xNEAqOT3T/RB6tAdz0STiVKNhDelHuzBPIkQGPXpG2GM+0VNCXnxFWerspm9mdxe1WCidvDFdxwuVby2fj1/fWo94lfh3nW8BqOkxM90oe7OnuqjFtzem63ih8q3l8/HrW2t8M5TyeTRRzQxa5ISmJyNm9qcNFYit2pLTyUwZ5zcFQhW7Ecw8oNbjdkLoWfOtFMCQmUeOtDkK8AmlnoioqwDchusbhXOBuAlmrOqeoTVH55Hhuru6pj09Irro/weGD9VE5wHOtGmuxWhrFMiPzrTBk+6pcDfaWul5OjnV1hTgbG7ZwFYP4eDqZlxZs3I/tmTd/llwczCJos2txEtIVgCkAcx3EHDCRrdCmE6vELFBuuK6HQfbcBXJToQJJhVSRoDJI2/zi4rjooraYnZsgFi4bXfRiBD2fZwupCHUt6XCAxLbJvI5qhAVbBHpBDet1EFP7ziDMMuZJ47Dnz0sEOyFFhnhFi7Vurl8Xf+HEGGdkJN32Kfgwj8Vh2h0W7mJ+37R7LgMNG+wOBo3iKMr8LGx/goEP1otbql3x+Spgfnm1Vw3C+oxdxnJS1UNPHcDPNVW4nvoCgr5G6cxr23jSi3dZ6R2Ibchx/WnvTEJbJM5qGPM3QQm5mpDGGr0mO9y8fLdjqD19sUyelgMo/n4KUuEo0bFWrJ2r7S31uRscM5uygZrbrOOy0QnP0q+M84PXeee26Z8yJi6nc7lHaQynd0IvlXBhxy4d8btJt2vCgD/FUQ13Kh6pZ9loTAD9W1YceFPpaY3OA3KCE75czayixKcOsi/DJQ9kO8zG1dQS3EMcFmGnDv3NW9BJNtLglTvlSihyncD7vv+kVjx0UwvJINIXjeiW1OxrESOGREbZygLUwJsBs0UUpWqJzNgIeJZFPWF3fSZAho6hW7XFw/klpb4/nf2Mg5GU9fWLRQIwj53eTGuIL4pQU+yjZh0GCTt4tPHx9jZj2x2/O57zpLzBENMA20l3OOK3vdiC+2oXaNsI5Gd+zE5S14BSWjbf2vCSmUEpIUccWG5w1EetjTMy+/GBlrhjrlcoB2IlRZCJ47h5vq++H6Tvz/yHO4y1tviN6fG8hReCZZoZ5dr5kXTBK2UlupgGmjM+bLB5hRlY/v50ltBXG07sWois8un8XIUk9NPhc4FEbwjXeOH8l2DPU61Vjr6tMeijaW3KOGW9Jwwoj2rFVJhf4eqxfD9rjMdiSFw9fwjoPTxNSMKWw9A0fPBsoh6zKZlpJ+t2db7nrBxSVR4TwSvPWu4tPkqdjKHMxR2MmRLafK/Up0st58ZREeHIrz02YY34WdYYrtmMQ+Zisb+vZomVlFAIeQPhiwgV/RnzsL7rZ9N9jRSmPI2ELS+tNMeLU0QttNYRzNuyNx1osLROwpvRSpvp96bTTtGqT3vXajs2yjhOVu/zp2Qhp01t+g42MgNWm1wYHrHoO3OGbqgH9A2/geSysth37SMaW5+0yGL2l6BSMANOWA7njMl+qQxGRFENuCrMn3yk3Onk8wbN03Va6GCclrqzLb85hE2aL2nSam0ZtWLxzGMUfk0912/IPBSL31zD4G8i4pCCUq1Hu8O8cF5LeEyiE4Posor7OVK+rlLLdIbOkwus7DWv7MDLeEdgwjjIFj71Mk0TLTDT91iQDUlTTdU1j9s6okAVDa/byyQpgCPIeDKxpuJBdJCvzHkAzJAzhRUYpScdYHBrPou/ZIR/PmuL1XbPtWcNLRUDQEmF6mt+l9eHPJ/0XYHoR1o24Wgu7o9bW1KmjXXhgAqGifWZqSZaK0b1PJT+/PCFeF5jRyNIJYXvPC/Nzp5ob9zPMcydbOZJxKwnFikTqn8zJ9YcyNT0g+PVkWVVCreMnmFgoBJ0LYF0psSm+nRtwnpRYmBNFXenPSlRK58o4jwFskLlPj5A/URNyX9KFGQ3pabk76USOC71imX+1dTPBOJQVIaGGJv4+DgxQ7LIENZ3b0N9Gdl/kIHpILNK1C8lfSnAwrBHx8g3n360wMNsLEHnka+U/WnQ3xFwfTfzmN/EALbfvPOOMqr5JZPz7J3eMsISdlZ0oQd4+A+zUE3GN6sRVDjcg3wl/4t1O+H4pswNvrqM6TOb/hsBfNlisfbM0hw4CUvJs5M5rdqskAh0aKot7l5l/FJ9mrZein+3Nhs+U23C+gT9h4alcHMA/WMt5jOIDI8EcYZzDtKiAw1CcU9IGl0wQqyQdZcKn5TDwXaKCVe4Xt7yMQ9AzJyTkYGN4bq3wqRaWCPBLeR4BwbLj1wBqHKMGZ72T1u3hN/R4LG5RmziXxJ/stuXRuOj5HQjc3Y9P4ZzLFkVqZie31E2iBh/GNyb9lXWq2Ddwk36qTzb/lHjAp5s91ubygQsR0ET6jBBPGjCcBXAHowMjm8Ip1FJdL9ZDF2N8oBoHn21a0nxpSUm2Dy88Mrck+yfViYjHtN7sH/Ai+0e9oielQlk/HN7DXAN9tJz5UpagUD7+0xRWRN7RsoTkToAvFLx4LFIKJ8UAI4ituyu1I1Z3bzEvBguixNnYuuA8z2kK/PiTglKrsq5K7Ov1qAGeFXIjelijsP2Q7tfla0I9ypPhbqLz/cZszrvzkb+5PUmtlucT9mtqSRWIwfQXDL/FGZc+R6imrPKyBjuxrV3CxloVK2+/41QUTKJhTpppWhdIvdlC1riTfRCn9Dy/N8yWHHcCM07t3ihmiGcbCnW0ZqmwMtLoDCtYeMqGRD+9tYk1SuinJzanU3CbDP+Fud3aypchEC9bZy1rdIB1lV7ZdcbqJkIFu92gKv7P0zmmGhSCSNlrMaaOGOMiLSlEU8DqLeH5bCBpNHxulkg72XuDHjlWM6Mn5lZodrTKR1+uSbXR1ovrxDs/9E99trvsQlfToDyFVr8aanRoSLnxfzmfQDfcfydW9PXdu6BAbfVSwS8wGBYHwyqGbtt8mYlSbV+qVWcW+5TYYcRMALGWUCbrZ1Th6+tSFIbZmeqJ4g6KA1M2HSsLX+pOUmnUUCETulnmzQLkwWPc4y43iemCllipPfjBKh0GaE3FEpYo0ohPHrpa3t5mTfBXPk9c3PMlJQJMy4Wf/aYaUL0HT5vKZkOyYNv/q/ChfoXyl2LICwlzjEiim7D7hjvAPm5665VzWGn20BXv590wmJAtVzOQfi/fMEYkaDpdL//j6bJPUWIgbQM78UzAP0r76uA9GfbcOLBydaYQt4iCqdlUiJbh4hK90Pq4iJhw3uSmI9KO2Z+4+HjibQdgvRbhOoduqKP0RdZ83/IfxtPfY4eeV3z2oVo+HMJrKaaV5WozIlh3eMDCe7S6gLYhcOocENecFj0mguac3xDxQaUEkkBjmMCAIpnVrFSXLx7sDYxnSHYy1bupMKVNXrjkIdjgg7Ur6cwmgGNinkfURxj2e/onBNGeE7h9sU44o7SRjfjrR5kdJGMiOgrhBbItOHaZbWkaYx4D5aT10SL7+4DpSWj2vKHj828JGvW9aTDiCuFG5xjXF0EDbVEv75J+bQr95KG8feNKEEuHBvs3acQPmBKZUaJ1T5LWt66NHnNpvF6GFEJT4P7aK1Wiy88vVFvgQ18CGQWTmmQTZsCElsHtrU+J9qTxW4qZaB6JVj4OQtDYz4BsJRyF82QZC9KklIS/eqfD/PzU99HgHmChftx/wUG/LxsUqzr6Y5OPrlh6jrFHklnyV3wZJ1AEh/xaaR2Xz/MGUrP4Nas9pH7EoBn/dVIXCmqTVqwQPyQc2B0C9SfL9tZk3m+JwfZGlGSwchdz5aNjPApUxHGwz2KDNEl/yoAMQZKojCyHTUBbo9tAWw2skrUiYFDWxSI73464yXXIbJ71zL0VLRG7a309cN/I8bWumKz5G6mKVDqeo9Ev5CZ4IHXbNrDIzA38HZdL5TkkAbMMBy1e1SYAopqV9uMSqb0PSJDhSdnSl9qU92agaeW7pmbXxGjuGAHIj/o3G141pxecLXRbKld11ijIBRmL3N3Nom+1D4Wkkvo2M7F0tIA+WEUj2KUixitBBcgbS9+QKk+HmXvn1/Z8ExfDhF9fk8EQxw8KMkbyt7KoHTkEMy9WpgNpcuUAw7NJ8rjK5l1C0Gb/VpwOznQgbPDr4qKYC8AXCvYA7gtQGaFQxPsUkNSkNMo30sADmNTuMnMNDoNbH0dNqDJQ8aosumki+jVdeHf3MTop6tJlO0wbP7/0oIuYfNfIqcalONsfPDEguB0L0n617NIZORyWiN0M02C2oPdxOlYIbvDF7BEI8zn8XpNGzhllh2K/J1xSyPeuXZNCsVT7mkNESxhiJyljjhcy6y+/h8oiTxLFzIFA9WSbcyCR2ADibxLII9GvFKD1eV3nFWK6G+RT6u4InJ2f+djKnEyUzIIITzUDwYAUszD7JFJ63DT7Q15lcLlklLiSUBZnuhzBKrbacIHZ0yIRQs1ISXB7TV46vKidzqkK1E9djfafhhPw6jB3AV7ujtBTYibZvZvsbq0AJeNGvCajMInNwt0fI8H/5J2/HwKXgqzcp/pRYYFbG6uDwGp3mrQMRFPZpDjCykBovhQ+sFX1HHwJPbbRJK7nQOb6+RvvNvjRl/cgp7YrbLqrYA1gUe9CWiWedAPIo6llDhmWnYuDesnv+POwkvt7zhPEy/pxrVoly/Iw5BUIizSaubAmmCJLOjQqKhwTpj3cGm7LiwbMIIAY1/swN9OZT8HMjricpD+mDFEm1RLm2//ZpQ+617L2/moWBXUWWWLoh+mNQ1BRChQs7k3CZ3SJR0sqG9PsSYK+RtdnGHcPrsgJAGlbsuaKeLM57lhXQbhe3gXHeOoWfuXSZf2PqJbIFAmUZtRuF6PxEuEEjpxJxv6RwBf85dN5yvG3xYCdVetCF7+6p8bm4EoEekoWj5Ze/pJl4ZxW9RZy/Ao1x6qimZpreR5wd0leOdyiyh3iSMzjzVuZ7wvZF63hvZnT7A5CDYke7nJi0LMhLI99YGJO64xSooIFY4gypMtbjZAC8KDnZ1I0pXEFNfatlb5vEXOAa6RL3+ZakD1LPGprcd5q1r/G5a04CZ02mM32+5zhaxjnK1CieLQhPmNUiQQLUMUYI6KYkeE3Z/LnaSigkHCJ+gxq1iCJosIac3gNRckq0VD4shD7CQ92iPI+TZwAbdcVZs3+aasua6EbjMWhUDLNJED4hz17RvPBfb1R0OTRePVhDPtz+SORjWr3u98CNd4aP1xPAL70RkEgLwVmJw/vB1NqXfZNue9BcZinNSbAOimfxLGuK0hDrrSQ9fRWLM5yHO26nJ+F1SvgVszqrrqIiHidrHUqSyQms/OE7yvvKAl2NQyfx7pI3Nm65PD95Ut9vcwIur0cNR3SElamUy9NS77zDjzkQlkm7mlI1tlgAGTVhoG4S1Zav8bhqGpfZM5CSfGJZSaBahD7ctawnhHSu3bvScFrZEh2hMlzaoFLasabyj5TlxnYZNR6/ROKsrHeXUC1Cfj3SKgIbh5IOeigxnMx6SpcLrQbyzQ0BkqvCScgVKF4eIWbtbgEt2jG9uGeR6ZvEJV3NP0cHe5pGB4fuw8xzujqh7xLdyk3lHDHJYlDVwU6f7HqoTpg6nfokbTV1N1zzBTrP+2hzoEyVbzAvrIobQcQG/7pZPQrVivMfCtPHhw39hyS/trlGO1qAx3yf1UExlwoQNZUpSv6SHvb5bzvuXf9XalEW/t5fXM+oZKyGpOx65fRJXH+aGL5WzbyfW0sfLqmddcQDoBGjBYcfYy6Df/5Xt3L+YJzG34Z8MOwgatIIyyR8UGwJ0RAjPcvbp4EqQmnlVS9WSVfVAgExlX/qh5ZodQYQy8eNrPVD/O1sYxkD/vHfKyz8QX3GuBw7ht0vyLbC0sQuiuOxpUleR3joG69T8dyJjICCJh47CgK+6NcB8oDIIEWByNFHblfn/SxtgmrMRI/r5KKf/nEjzvWhQTx+lRrl3o+5UoAlagntni7ECSGuG4nx9WInYD3oMUuHSo7eMtOUiWd7iZKsi7Qzb43gaiPe5AsIecUmD7cHFNf/Z3Lf4XkRYM+M6qNNMN9gEZsIYX55IwruSCd567j2rWhdgeyZjpccZH6VTGRsnrU1IrvvtY206ANev+tJ4iBr31+6axh9ZPQ6wwEywDacw4Hz8f/sNS6ZHAf15jTj0fHktYguaFGXY6b283FewpXdCmKPhn8h+pc8qd/fY3/LMuPlX3OTgZRjfZiz2WzOOjyd6V4w7Nv3SJoCGVPByeXrpMhzt3rdJ4yrO3jIWFMlC5ExvztKW3Mm8qSGAagmuShx/bZ4A5eNUYcxsV8UY1P0R7j8cLCQ9Bdx7CIxHqVJ3QkZN3J8e2lCkGE+2Cjq5RLR2k91j+9ezNXdeClp6tJGRHJFjpNRnt+EVu9/8Zx37jdElBNirmlxroZFAtlH6kjojnks5eL57ijK5HFrFnfHEEe3SuVOe0BT5ImfEZ8MRK3U2ERvhctK4sSOKi7pFM0gDkg61RelXRXmuEBa0EcZMUDa51Z0H6etk6EiJsqyLie1j81O5tDkXbH60zX73B0/zcIIDYaztfdiF+9RyTgw/URxjAJO9aBqq21ut5OF0NWu8tC01FiPWhZ2fRN9qCb3xFYqU0vZEh/nTGNOszw2mi3+p78Wn6rqQNdgN2y5XSsz6Ijy8oENKA2GAfTnRSZNfTtRiDw9Ms5lqg3K6ksOkb3SugmBP93QSpkknVFXaAmuqDEU+SJ6gi3PLgQDWRTIiMxcg1BbDnqjJ24zdpgWTi5pRY46N9yHx0JpgtU2wKI7hMW81hQVAXuDbbe5lX1OW7q1vThlNntsYS+1r3CVXz1tO8d5nz8wry4VlJux/5qij+tE0fFUuRfVh4cpv1rNo3VOrE0xaWNpeBCUzy9GEUPvg6rNh5QwE7yC6GoryzGGk3cE2x5UJ2oPCVIv0nZKaZudkbWVqqR8pOlcWUWXMLL5o9Eqiz8oLbpw0/CJj8f/q2imtZ3/bNwmCaRCbIJ45KLDA2wkG3JZraMSXpzTRbZgUeAYoFUKje87FJavaCrblGi7bSwtBtSXfvT07CVVTo0xczOSPvMT5Sh4Ck9A1HLx2CtQrpkBwMMTCaySmZX0ERvqS5ct46tc0SvyaCGKZHwhaoK1uq8bcW5m9t9NbhVSo8pAVvh+/wpYjrC3AYkeeKtqAVB6HCBXgN0r1KBwWzwoTogdUPGSPiEaE8BG0ZzyofydC2AOBcmUeCp3p6gAojUXHawEQZ3IATw15Su6GQfjUHLANg2AMxhFjJWoGZp6yoBGJ5VMv7u5M7cnRa/55GOq8JeQ6xGU6cxxkF0poK4az7nbh6EW816SUqAS8eyEceFsD3Srj4N7vjfaoV2WNexWOuBQKGNzhu6SS7mzQKprclgMiJ5R7M/ic9WQInuIMSYTF5EsiLcAp8jXtECsYzrb+zBTjPrTusTqS938B6jM7FnXRJDJ5uYxCYxSTrE5xssBphe5gvbjMPGYjdoQ5JQp+ygQjThn0B8UTgBbp19uBb92aXvzL8vIFMzG/tQK2I4K9Z//7Ct/FWID2q0zK9rz6wMme3ev94UOp50n7xhD1N0f8RF8yrT7c12bXlAygOvoz4YcbbpEn0ACXown4fwdBoBpmuxteSEShBi1eKg5eBK436o/mIqzoiqhWPQwVrZuOSn5UKhLhh3PWxWf99UrMjjM7EBYb392X30/0xJCIdVpdeqw90CU3eHqk5wSwaPsJTpJRS8xUpIU0v2J6v/59XYAoD1/rGPGbWY5AkN1i8O3nA+gm6Cz0uesKkatCs4NjZZL4zIWwZu123I6UdQZ/59B7y7r3rM0UAY7cPvQgf5ODzxJ3/pNtUzZ86zFTzBxxS0XOIMCVNDwstycYCC3uG3ReYGZB4FN6bclMRsyx5MyKtu3Vpjz1c/3IfLQVZN8D8oHWmn9Cm707hHmfKBC6bJOWFmye+mKDZr2lnMNyEBMFV8yq1JT4J2g35P8RzzfXfvbKKIEsZeteiwvre951KUYIPJn4A/6SAXkF8NdLoKmvyyqFthlqhqsRWC2CnJStmikDCtPSSKjpsf9JgWhVcHfGgu9D84sL687WFqBlPoXLO1S9of7MnLp5Esuy5zPnlqBFa5GoJd8B1vDQUi7ytJAqa55mS02qnWc0rR0xRcHoEeixhkQTwZsMcThVN7T3Wpv8WsIl32cA6Vlrw1IVOqqdeF9u/difPm7vBz/BOGHvYwfEkNqauBp0KXtUD3tHlNK0si8W51fQQ/EQJlIvWGo8Ye4ek57OYWKQCdHY03HneCJBHxilRK6RQ8Q9Gd/VOzICChs402kX0uNjKNJo9zCjnO/Jq303qSenOwCzjxi8+NHVZS8iIfOQAJoVqQAoIYGNyAJQj0gI04CUUUdIFVQAKe2+rGL3BuolYQgTh7S0edV5qFT9784iqEfWYLEUW4Aq8p7w4UiaUvKTYGdJU1PGEtIE8bTK9LcCLrRl7BgX8EKkaA7rXYS5EtdyfA+CMyOhu8xsQKGjrFSATVr1nbE2qc1QTEY8QxqtTwAOkiBBYZESz9vVDg2DPkbK68xAkg7Y+SnyxquwTIWS8FBii+nc6p0cIzReg+FtzelG0NMsRfhqaUQb88qnnRPMzqb7xutuKGbkkW+JFU+h+sgbiBdIgm/HlGYQfnqZELpQQ6yReOAhEsQoIgFoKFIBB0WCOWISgcyRHOAgQKBykChkD9J72ckBzkhgzEKH1iY4yDyfmRwHfCdSgcZDwAtwklm4AEmJtLqLkEJqvUWp+nfHAiiQhCIMVqL2TxbHCwCYg2TQa5htOBspWjVsQSrVV1mMCymVuogi6+yMKkZyzC8SggIj6XrU9LbXQAUUZESWXcQbfrhAbF6F2wK4J0mnSiWvcms8LBKTlW8oPBxSWznNDjg/wG7jDXBBJ2ftKpy9K7zIENlZZgl40AP2rpITQGYWFqCKW0SS11Efw8N2YIGS7HqzaYPqTE6drqB4SEEDndReEpuZhBAM+NGCAXlIGP+VpFO2gIIllVhnpCN/wCsiBQ+MDfDcUJ+DbBqSALWoacjKSLGGHf9CJS50zwP4p6SKzaKCP6JtYKYoRelglBKsgESJljBQNZ989YTVQbHyU3GyRtseoIVIVrRFuoNZDDgUd+suIx/wLeG2uKBoXDdQ/EE28kqT/n+Nacb2fUvAYybFrB2ziIIK/ynghjFXUEvBNpxitZKqacdBuJJqDqFKWFMSxb5TCYRLojiGwtxdbQIjWNtOAVhJBJdWVal5yTOBCayt4BqLwHclFbxcxdw9Q0KxkRDQLkl0gYA2+VDGSwrU2Aya4zkhLdYD8Uaqfcmm45YsZIeVNSCjkULO9IOY2qSVMxhpEuhSG9xoA7SY05lsn4btrauvufpfb0G786S8azsJ+qob8rbBmSIMElho9tYQPATopv7ULyNYHsPKIYQoG6B4mqwqQY5HN9QgS7ueosX+n6vL6El5vanC2ybZErUbRiQG6CYtxA1Ozgsqq8hUPwm/lmT/ky6AAReKOmy0TzLGKInIM0b98KekuhkMZ0qaKPWomMCDJsGT4OhZYHF+7kTYJYGSJBqlaePA6Grrj6KOkcwNijWAlTWU75l9VbgoYj5xsGDSwgVuLimEBW1AOFFg+msGe9xmATNNKnkTAym1wDCMTQjnaDe3E4olqAuAekkQgZTEIcKTkGSmSmjCJCRVpKaQa0orEFdAWyC1nDZAtD1tgAh52gBR7bQFAsZpC8R40zrkldQ6JIPUOmRw1DqkXRSoUuoh4Tf2BK1KgmimpBJxlkRj2JJo3FsSvLa3Qk7dQLykaCx6/3W+yKskcPVJtQ7EXRLXstyH+kMwwaRfPZY12/EGrMYsrARtmN2ATbcscjvJl0Ft58RStHarqc2Kbbjaqgikry9BnhR4RG1hNgGiioQwgQzBSeeNcHEJnvWFNuE82zPF03kiffDHJGY6xFxJXJGwySSYz0hXgE1T7M/2dDiHUn7uJ32Xbz1v0+7Qa0A57cwuuYPJt1/ZHQXIkBLYW70wAXaIJW6rX8FWfIBOUCrLoAdn2MRZt+lWmIH7I9lZNbYH+4WrKKNOhXdOgNzP52DjmdekV/w9nwvWSmS3HAe5GgKw+VvHXkZL6GKfwrIJuKAePgWPHmk3Yh9yFxrqq7Oe1d8T3JArwkvJ9jKV+U4VQzdY4nOq1cKecLlbBmI+KU4e1FOx418vgxuU5PhSwx1WmIEVTrIwHbeOqO3ux0tQl7+axcKwkAaLMcVBQ5SKqwikTmiJErC8vj2ERSmOt2lnxiSEdyjpVaBFM+wvXVoP9oy9fbt8usCjNdUrnInQJ5s7yL5WruzJuzvKvtk6DwLmivcKK4bvwAE/Va2xiENFPEfBoNQmnfrhPXhSpRy4OIVpl1B+uQonooSvfFIA6kQpn5uTrkAtka+zBnfghzq5f5wF6/YxZ3jFN610vx26/vC5nF3T8uVkqh37NGg3y4OiapVMqmLjYPHMT/+giZWEnPf13/7IDlOs9HUtILFClzf1rEYDRE+pQK8NQdiRD4C/iZYwNZ/3eC0V1A8MgzttkOCj92b1dM6ScUD4nNLLA/AEpTMDNVDaGc08cET74gO02Zs25kiDjEyCSxI17MgHgBSUYLCzBaQWohEKSq85TgBmBIDS45AIANId6ety4kORDgMLRXr4BYEi5bjUIsT3shHkE+mx3AAA4Yn0oIzrRKohnEgcYBMJZ9xfesRmIlVQTKR7Je4S6Sd9aLL/J3eZ8Ur9GR84mwgJ9hIpgiOR/umkGwk9FQvYAwMb/YNxtlmZvGz+7AGvzoVWtGyu5Cf3cJBsWfie7FIzftdHNJaztfSwOU/1ve57B5NHIROgENUlSxiBISWr9KdZeTWRyBDHVd4Z/rykkcQyFCnxf2Xkg85yGjD8NjfFd1dGmzxESFu1wRE2SlmbwXCld+OGFr7j+RXVIgbE3gKdwNeya9D4mVwlnmIKX6kQMFHUfJSPGfmHP+B2LBxfi+u2yHh4o5eU1U5GSOtmRMGV4n+6UtT/OQdQlxPRulsFByGqu5cMlrgnN4q4Q3S0kieXDHxhBP56sGozI3Il/iuL8GLzCS7BoxxnrDNK9wCoPTOiHhw2ifraL5buK8SkUpM+3hFKNCPIHELIUHmXD1mpu51nKAS4lp3jEbMAQNVmRL0awmZvxLoI4LOsQA6z3K0BBeu6Tk7jZgRD5WIVPqzw61+3x6DaFodYP+DnqqIxaF94woyD07ycOolLqp0uFYMorR/85mzFJzDKHD+GzwS1Z5VeBeY3B9OVONkGVOIDMZTc41FIAUA3T0Reafz6CgBAq+iJyIsrGuz1UwDAXlNPBEfCF7l/yW+l08o82VYndHyNQCt6hQRiT4QxxAtVXl4myJgu+kXs6mDAvWG26ACM+ZQsrf2+uZCJVIERRdBiYG6lT7RAYAUMnNQCm2AiQ8SJvSA1tCqeVsr+K9YotRLtGz5jOIH9ER/14P1XNir2GRd5+BNwnsb9cHnEbclfkRyafgRRk3PU64EjNSEW5IghXx9HKE+nDjIokA5ePoDD7gJZuOUIWh+FTXM246gTuhsiSJyeVEq+rGkJ1R295gKj2Wg3x/qJLB+Psj5gk/HqMzjCNkgpR8gSAVORzotEwTRkspLz1q9KyZiwaYvcKx4+iiRSRaP3niCG7tRFhKbWvEcxzxqoR7Qnra/vEQCcNmCTPJRo8+GMh5gwPHYBAegCNlGJW8YfKfbRbStJZuwGnDGiygLq336FDmjpQup9JC92HPzDxVWo7uxSvPYQ41BZVFYUdDl+Ez5mS1QFm2TRdmF2fSqs+gP3C1SxoFJH/Ik5ZpRe8tEXbJJHKU3NMU9jlown2hwgymLsJPPjD6M02KSccB5ed7BJHXlrMR+A7mUqhE1y5wnLzIGq7L6qSdik7H77TUpPsEuGZvDFqtI2joLQgmy2/e0h/M6CqDiRYx16sCuWInC8CX8DuQNRMWeIMpbTr4ebWzadFZHOXWh90iP/3S/xKKEqxB1RNrOMvZL+0Uw/kKhKedVWAzbbdX+TL5q4+5xW5FUMs49qcXrI1ywsUhHo2oWR4DUMmwLmzLKkFjMNu4yOMu3xugdlcfZgxkpd1qAcGmA/dbQtW67ADxf34IoQ3oJlcXUozJguHETcy//bYwjVdWhEIpBIswurZELqsyw+bZG/RMHYW018UEf6qkkZzLvEUxps5c20ylz0mMoFUKJIE5limt3bWei2wVSaOemJsOgHJS1Yjymmdk+s+IZmNrzUhsP9utCRJh6MwZDBQ0bZI8089F9xWQRJU02IWUjfr/ixgabUYlMDiypYEZscxI1W7V95U2DVSTTIcNqzQKYAIY8oSqhDY1BrdBunZh+OYP+zF9x5xj/mnTvPYiimBWXeFDDPD73a0n5k+6+GZZ9LsacP8NGalwIafwF4NhI46xwWBKwh6uJQFu/DP70xYBcMwASFZuMXa3KRCsRaIAT3hYQ0QmzA93ct8AMaEDQ86VzCGpaDzpx65sCbUoretARLK/ZKBXvD585lrCFgDcXqZ+zdm8JJMeKfGKfJSEEDMUJUGepIgFbmEpZj0OhbO8551MpbYaquLUWRLba2/PgyZ2Roum1hG+bK0uAcYoNaFKLHUlrJjJTAZ0+KNh4oL2azVoujku+c8b2DGc9LuFIGaDcjGJIQvTiEziBAkBCZKky46wEU/RL4aYMic9ls0Vv0OF9MukHGbBdmTzXw+D3XE8IldCkcLFHyqHlsrAsXmkV6oPS0T7UV7apsV7qKNn4jUoFQXoFLA3HZzM5QW/1WE9ts/QhtA1DGnIS5iV8GRFcTnFSufEAYDb1vx3T746AjQCFzktfyHACoXre4AqACABvtM0RiwxpeJPn1AwReLefDFg/pUFXmYSankd0/snNh/w/9h5xDVuFF66Vnmw6cl2LM2r53ZecWEZjWb9cGObQjpZgOSJI7LzqldPHM/PzPKM7GFcpWKYFvVI4/dIvK1RhpyHhQqvzZD/iWaWmnPgLNOcukS4LanQtPy0tt8VLnZ1Y5gdet4BS+aYxL8GhV3ARP7McpdLY0Y0PO/dwh/5xI1uo0KHqZ1GNDPkN3CyIp5xBHkR52E/2yK4nzwkgBbWsdo9kU/mrik3TKdPL0TnK1rMLt9zeuWh68l4qSa5tl/ZGYyo/sRBha7tOx3FtIF8jOyFylGp0+yLfEVEyYVP5ih7nbPWQd+4WDRIzLmVRvQH5L0W4rghtY9wJmrRPyrTsDelYuypfe2v248bM44wXtZb2VLymllEypvPIKYyTrQgDXK5fPcyRTF9y4JhydLymlhARWLAv60rf+mtHNCbHn9XfOEIOdbSFk+ZZ1DZqSn1W69ORWbNinGQlh9bXnKjRzLPlLpYXr9yX7Xgjgd3cGhGY0GQ9MkbvZyLGd8VHrBMChAfO1/0YAKoQabobZjYUOl4K3FqwnjLpSC2rqtgGEYxvUtaIKs0MAYpNrNDyvSRkh0rGFj0byc4A/SfIa+G+XB57ZutUmU/aB9mIKWVfP0BB20DeFOxh8R6elI3yTIaNjL2wiYFDvPhnSWcFg616Vw0l2CIq2NgnJhsvFdCwgleNTPdjimc7tWK/0rWDNPCmuXKDlYMXcp/bUGrwf+yGtwxRoO3Oyur1hh6G22itoDdan4cSl195CWYUnSihwqddtHWK+XVXrkOTBnGwhkH2ktgXwkKV+LTMIgApi4wkQoWI/+ayUGiDyXOlKThiErJxKgGjp0jjrp8ZcFYmzA6n8pFZMVmwVPLGsezq0in3VszlGpkHMJDbc1aWSFwWdcUJ9wC8ftB8YTZnu72BeKDVdj7EIAG7U1J7LAODo7nCiprb8IXINPmeItH+zvKflPk3d0+NFhhB5RLIAcJ2m/kiD6uNoVAd9su9NUfExjtPUd/cJ6WFMtAF+wujlVJZp6lwbC20p3JM7jGPTnD1YvesAzNLUI0ez86IwklGaOuTSu3fzymOUz2HxJL+uGABIWaSpa3d3ASXLCCRDyKPFx9sferHSnWBFOO1hO/zEfDZnh5jb1rvBNFn+F2DSCI046zWxokmhbP/NK+g9lYbuiHAzFRxVvhN8SKajEAgdwkFZDoQ0yFXHf0NIRyfd4+h6mHMILnpqEsFTAX+q2KJSedapZa0CI1mqMjg8C4D0xL0A+hE0Dfbc7DF2onwNAAHQ8RJUB59FpQ1h8Sl61/AzJ0G1jZE55nYyh9jK2TmWJVdla5IfiQ4AOAiqxx6yX+Isqy9W+aF4B6rPwgE1Ba+j9FacqGDkHx79HwC4BqoJp9+NFesIJDRap+9bC7qtJfBt4S+AWQI/oaIB+Glg+M8pm5+dmGUs9XrihryOs2uVc0KQdkUAqQFB7iPG+Qm6axOoV1sMXaKS4UJbzWH2Aih5aYCmAb7rotMPj7iLiqr4kKKdA+STU9eZDBf3HBhzM1UU8p/eMgBD4zUMxzZj9gdA+XMC4dJm+VgJuBa4nD8mCYo1Ge3QgIBcE0vtQoMAwF7yNPlUeQvy3T1DEnkgKEHB0iY14Eq+YtUoYv0wgOpxcMSSgyM1HkFe/KHPnYgbeD/rnKpephnQv0YqjjHfaHRQyplR0FSLRDtAQGj5AUQN45gdIhhimXTmuflUQdZ8UOTZQZ8uru+J2F24jEuELvwZplAkPbH1O0msQuAf/DdMejo8WypapLG8+fGWko8iOfxQU/yLG7/ifFQtqb5GVY0NYpBUZciLhogwP/AESGMEF61qQYc0CPqk4YLcD9b/Jbar9+FoTD0rsUpdP8kZJNQk944irwkUXjt1YBSPcl7n0B7yEp58C4F7DfaU2xdWYO4T2HPBUN2KiVA1p+m05XXO1JG5pOQYLNtCcKKH9JdDYaC0mlw/7sXUN+gevcZp8bpfDCCTayEw84+eu9uhF4bJWqJiEOQAhte68cGkppKkE/DQ/ttsznpxKtRQ7m9OiiKrp+/REfjDs6RZCIjpvItZG6KxxaZ6WZ5g7c5aVug5GxIpHwCubcId6sudYm4Zwhbqq7XFx98EACkWgkP0HztDEiwE9nNqstyX5FcIqEUv5Tu3X1cHAGTSKwTegRDTg+xMSKnn+pGjHnsrLD8YfbzkCoH8eMCh6pUJgejW7f/3lAEwndQKgS74nC8ywCPCCYDwCkFtrRial339vZ8=" +} diff --git a/packages/core/patches/three@0.185.1.patch b/packages/core/patches/three@0.185.1.patch index 8b575d1a4..47f188126 100644 --- a/packages/core/patches/three@0.185.1.patch +++ b/packages/core/patches/three@0.185.1.patch @@ -25,7 +25,7 @@ index 0bb9262cd029f411933d077fd44197a51ec5e8e9..868a634b4294be5f32664912a50813d5 * Constructs a new batched mesh. * diff --git a/build/three.webgpu.js b/build/three.webgpu.js -index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64db40becd 100644 +index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..e594543d1e604de1ac9f45c6260e213ad0e2d7b5 100644 --- a/build/three.webgpu.js +++ b/build/three.webgpu.js @@ -141,6 +141,15 @@ class NodeMaterialObserver { @@ -294,27 +294,36 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } else if ( levelSnippet ) { -@@ -18320,7 +18403,8 @@ function createInstanceMatrixNode( builder, instanceMatrix ) { +@@ -18318,13 +18401,8 @@ function createInstanceMatrixNode( builder, instanceMatrix ) { - const uniformBufferSize = matrixCount * 16 * 4; + } else { +- const uniformBufferSize = matrixCount * 16 * 4; +- - if ( uniformBufferSize <= builder.getUniformBufferLimit() ) { +- +- instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', matrixCount ).element( instanceIndex ); +- +- } else { + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change -+ if ( false ) { ++ { - instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', matrixCount ).element( instanceIndex ); + let interleaved = _matrixBuffers.get( instanceMatrix ); -@@ -18414,7 +18498,8 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { +@@ -18412,9 +18490,10 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { - const uniformBufferSize = Math.max( matrices.count, 1 ) * 16 * 4; + if ( ! isStorageMatrix ) { + +- const uniformBufferSize = Math.max( matrices.count, 1 ) * 16 * 4; ++ Math.max( matrices.count, 1 ) * 16 * 4; - if ( uniformBufferSize > builder.getUniformBufferLimit() ) { + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change -+ if ( true ) { ++ { interleavedMatrix = _matrixBuffers.get( matrices ); -@@ -18455,7 +18540,7 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { +@@ -18455,7 +18534,7 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { // Synchronization of dynamic buffer updates per frame if ( interleavedMatrix !== null || interleavedColor !== null ) { @@ -323,7 +332,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 if ( interleavedMatrix !== null ) { -@@ -18496,11 +18581,28 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { +@@ -18496,11 +18575,28 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { const instancedMesh = builder.object; @@ -337,25 +346,25 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 + if ( interleaved !== undefined ) interleaved.needsUpdate = true; + + }; - -- const previousInstanceData = _previousInstanceMatrices.get( object ); ++ + // Scheduled history must reach the GPU in this draw, before the geometry uploads. + OnBeforeObjectUpdate( ( { object } ) => { - -- previousInstanceData.previousInstanceMatrix.array.set( matrices.array ); ++ + const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ]; + if ( scheduledPrevious !== undefined ) updatePrevious( object, scheduledPrevious ); + + } ); -+ + +- const previousInstanceData = _previousInstanceMatrices.get( object ); + // Without a scheduler, retain Three's draw-time snapshot for the next draw. + OnObjectUpdate( ( { object } ) => { -+ + +- previousInstanceData.previousInstanceMatrix.array.set( matrices.array ); + if ( object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ] === undefined ) updatePrevious( object, matrices.array ); } ); -@@ -18599,6 +18701,7 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => { +@@ -18599,6 +18695,7 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => { const indirectId = getIndirectIndex( batchMesh._indirectTexture, int( batchingIdNode ) ); const matricesTexture = batchMesh._matricesTexture; @@ -363,10 +372,10 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 const size = int( textureSize( textureLoad( matricesTexture ), 0 ).x ).toConst(); const j = float( indirectId ).mul( 4 ).toInt().toConst(); -@@ -18626,6 +18729,19 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => { +@@ -18625,6 +18722,19 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => { + const bm = mat3( batchingMatrix ); positionLocal.assign( batchingMatrix.mul( positionLocal ) ); - + if ( builder.needsPreviousData() ) { + + const previousBatchingMatrix = mat4( @@ -380,10 +389,10 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 + + } + + const transformedNormal = normalLocal.div( vec3( bm[ 0 ].dot( bm[ 0 ] ), bm[ 1 ].dot( bm[ 1 ] ), bm[ 2 ].dot( bm[ 2 ] ) ) ); - const batchingNormal = bm.mul( transformedNormal ).xyz; -@@ -18745,7 +18861,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve +@@ -18745,7 +18855,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve } @@ -400,7 +409,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -18777,7 +18901,8 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => { +@@ -18777,7 +18895,8 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => { if ( skeletonData !== undefined ) { @@ -410,7 +419,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -18844,7 +18969,8 @@ const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ], +@@ -18844,7 +18963,8 @@ const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ], if ( state !== undefined ) { @@ -420,7 +429,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -29911,6 +30037,18 @@ class RenderObject { +@@ -29911,6 +30031,18 @@ class RenderObject { */ this._sourceMaterial = renderer._currentSourceMaterial; @@ -439,7 +448,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 /** * An event listener which is defined by `RenderObjects`. It performs * clean up tasks when `dispose()` on this render object. -@@ -30098,7 +30236,7 @@ class RenderObject { +@@ -30098,7 +30230,7 @@ class RenderObject { */ getChainArray() { @@ -448,7 +457,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -30548,6 +30686,7 @@ class RenderObject { +@@ -30548,6 +30680,7 @@ class RenderObject { } cacheKey = hash$1( cacheKey, this.renderer.contextNode.id, this.renderer.contextNode.version ); @@ -456,7 +465,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 return cacheKey; -@@ -30680,6 +30819,8 @@ class RenderObjects { +@@ -30680,6 +30813,8 @@ class RenderObjects { _chainKeys$3[ 1 ] = material; _chainKeys$3[ 2 ] = renderContext; _chainKeys$3[ 3 ] = lightsNode; @@ -465,7 +474,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 // -@@ -30707,9 +30848,18 @@ class RenderObjects { +@@ -30707,9 +30842,18 @@ class RenderObjects { } @@ -486,7 +495,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 renderObject.dispose(); -@@ -30719,6 +30869,8 @@ class RenderObjects { +@@ -30719,6 +30863,8 @@ class RenderObjects { renderObject.version = material.version; @@ -495,7 +504,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } } -@@ -30731,6 +30883,7 @@ class RenderObjects { +@@ -30731,6 +30877,7 @@ class RenderObjects { _chainKeys$3[ 1 ] = null; _chainKeys$3[ 2 ] = null; _chainKeys$3[ 3 ] = null; @@ -503,7 +512,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 // -@@ -31165,6 +31318,14 @@ class Geometries extends DataMap { +@@ -31165,6 +31312,14 @@ class Geometries extends DataMap { */ this._geometryDisposeListeners = new Map(); @@ -518,7 +527,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } /** -@@ -31190,10 +31351,69 @@ class Geometries extends DataMap { +@@ -31190,10 +31345,69 @@ class Geometries extends DataMap { if ( this.has( renderObject ) === false ) this.initGeometry( renderObject ); @@ -588,7 +597,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 /** * Initializes the geometry of the given render object. * -@@ -32771,7 +32991,7 @@ class Bindings extends DataMap { +@@ -32771,7 +32985,7 @@ class Bindings extends DataMap { */ updateForRender( renderObject ) { @@ -597,7 +606,22 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -32918,12 +33138,13 @@ class Bindings extends DataMap { +@@ -32832,10 +33046,14 @@ class Bindings extends DataMap { + + } else if ( binding.isSampledTexture ) { + ++ // Async node compilation can yield while a viewport node changes targets. ++ // Refresh its reference before creating the first GPU binding, not after. ++ binding.update(); + this.textures.updateTexture( binding.texture ); + + } else if ( binding.isSampler ) { + ++ binding.update(); + this.textures.updateSampler( binding ); + + } else if ( binding.isStorageBuffer ) { +@@ -32918,12 +33136,13 @@ class Bindings extends DataMap { * Updates the given array of bindings. * * @param {Array} bindings - The bind groups. @@ -613,7 +637,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -32934,11 +33155,27 @@ class Bindings extends DataMap { +@@ -32934,11 +33153,27 @@ class Bindings extends DataMap { * * @param {BindGroup} bindGroup - The bind group to update. * @param {Array} bindings - The bind groups. @@ -642,7 +666,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 let needsBindingsUpdate = false; let cacheBindings = true; let cacheIndex = 0; -@@ -32948,7 +33185,7 @@ class Bindings extends DataMap { +@@ -32948,7 +33183,7 @@ class Bindings extends DataMap { for ( const binding of bindGroup.bindings ) { @@ -651,7 +675,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 // every uniforms group is a uniform buffer. So if no update is required, // we move one with the next binding. Otherwise the next if block will update the group. -@@ -33952,7 +34189,8 @@ class RenderContexts { +@@ -33952,7 +34187,8 @@ class RenderContexts { } @@ -661,7 +685,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 renderState.clearDepthValue = this.renderer.getClearDepth(); renderState.clearStencilValue = this.renderer.getClearStencil(); -@@ -34020,6 +34258,10 @@ class Textures extends DataMap { +@@ -34020,6 +34256,10 @@ class Textures extends DataMap { * @type {Set} */ this._htmlTextures = new Set(); @@ -672,7 +696,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -34169,7 +34411,16 @@ class Textures extends DataMap { +@@ -34169,7 +34409,16 @@ class Textures extends DataMap { */ updateTexture( texture, options = {} ) { @@ -689,7 +713,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 if ( textureData.initialized === true && textureData.version === texture.version ) return; const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34181,6 +34432,21 @@ class Textures extends DataMap { +@@ -34181,6 +34430,21 @@ class Textures extends DataMap { backend.destroyTexture( texture ); @@ -711,7 +735,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } // -@@ -34318,7 +34584,16 @@ class Textures extends DataMap { +@@ -34318,7 +34582,16 @@ class Textures extends DataMap { } @@ -729,7 +753,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; -@@ -34328,7 +34603,7 @@ class Textures extends DataMap { +@@ -34328,7 +34601,7 @@ class Textures extends DataMap { } @@ -738,7 +762,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -34345,6 +34620,17 @@ class Textures extends DataMap { +@@ -34345,6 +34618,17 @@ class Textures extends DataMap { } @@ -756,7 +780,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 // dispose handler if ( textureData.initialized !== true ) { -@@ -34377,9 +34663,134 @@ class Textures extends DataMap { +@@ -34377,9 +34661,134 @@ class Textures extends DataMap { } @@ -893,7 +917,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -34557,6 +34968,8 @@ class Textures extends DataMap { +@@ -34557,6 +34966,8 @@ class Textures extends DataMap { */ _destroyTexture( texture ) { @@ -902,7 +926,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 if ( this.has( texture ) === true ) { const textureData = this.get( texture ); -@@ -39725,7 +40138,8 @@ class VelocityNode extends TempNode { +@@ -39725,7 +40136,8 @@ class VelocityNode extends TempNode { const previousModelMatrix = getPreviousMatrix( object ); @@ -912,7 +936,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 // -@@ -40270,7 +40684,7 @@ class PassMultipleTextureNode extends PassTextureNode { +@@ -40270,7 +40682,7 @@ class PassMultipleTextureNode extends PassTextureNode { * @param {string} textureName - The output texture name. * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. */ @@ -921,7 +945,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 // null is passed to the super call since this class does not // use an external texture for rendering pass data into. Instead -@@ -40291,6 +40705,8 @@ class PassMultipleTextureNode extends PassTextureNode { +@@ -40291,6 +40703,8 @@ class PassMultipleTextureNode extends PassTextureNode { * @type {boolean} */ this.previousTexture = previousTexture; @@ -930,7 +954,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 /** * This flag can be used for type testing. -@@ -40312,17 +40728,33 @@ class PassMultipleTextureNode extends PassTextureNode { +@@ -40312,17 +40726,33 @@ class PassMultipleTextureNode extends PassTextureNode { } @@ -965,7 +989,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 newNode.uvNode = this.uvNode; newNode.levelNode = this.levelNode; newNode.biasNode = this.biasNode; -@@ -40503,6 +40935,8 @@ class PassNode extends TempNode { +@@ -40503,6 +40933,8 @@ class PassNode extends TempNode { * @type {Object} */ this._textureNodes = {}; @@ -974,7 +998,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 /** * A dictionary holding the internal depth nodes. -@@ -40809,6 +41243,7 @@ class PassNode extends TempNode { +@@ -40809,6 +41241,7 @@ class PassNode extends TempNode { this._previousTextures[ name ] = texture; this._textureNodes[ name ].updateTexture(); @@ -982,7 +1006,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 this._previousTextureNodes[ name ].updateTexture(); } -@@ -40821,6 +41256,17 @@ class PassNode extends TempNode { +@@ -40821,6 +41254,17 @@ class PassNode extends TempNode { * @param {string} [name='output'] - The output name to get the texture node for. * @return {TextureNode} The texture node. */ @@ -1000,7 +1024,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 getTextureNode( name = 'output' ) { let textureNode = this._textureNodes[ name ]; -@@ -41163,6 +41609,7 @@ class PassNode extends TempNode { +@@ -41163,6 +41607,7 @@ class PassNode extends TempNode { */ dispose() { @@ -1008,7 +1032,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 this.renderTarget.dispose(); } -@@ -44431,6 +44878,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ +@@ -44431,6 +44876,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); @@ -1055,7 +1079,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 /** * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map * with a binary `[0,1]` result. -@@ -44485,8 +44972,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, +@@ -44485,8 +44970,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, }; @@ -1066,7 +1090,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 const texelSize = vec2( 1 ).div( mapSize ); const radiusScaled = radius.mul( texelSize.x ); -@@ -44517,7 +45004,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, +@@ -44517,7 +45002,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, */ const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { @@ -1075,7 +1099,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 const texelSize = vec2( 1 ).div( mapSize ); -@@ -45586,8 +46073,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) +@@ -45586,8 +46071,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) */ const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { @@ -1086,7 +1110,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 const texelSize = radius.div( mapSize.x ); -@@ -45623,9 +46110,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo +@@ -45623,9 +46108,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo const shadowPositionAbs = shadowPosition.abs().toConst(); const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); @@ -1099,7 +1123,15 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 const result = float( 1.0 ).toVar(); -@@ -53841,7 +54328,7 @@ class NodeBuilder { +@@ -48407,6 +48892,7 @@ var TSL = /*#__PURE__*/Object.freeze({ + getShIrradianceAt: getShIrradianceAt, + getShadowMaterial: getShadowMaterial, + getShadowRenderObjectFunction: getShadowRenderObjectFunction, ++ getShadowUniform: getShadowUniform, + getTextureIndex: getTextureIndex, + getViewPosition: getViewPosition, + ggxConvolution: ggxConvolution, +@@ -53841,7 +54327,7 @@ class NodeBuilder { const mrt = this.renderer.getMRT(); @@ -1108,7 +1140,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -55244,15 +55731,17 @@ class NodeManager extends DataMap { +@@ -55244,15 +55730,17 @@ class NodeManager extends DataMap { * Returns `true` if the given node uniforms group must be updated or not. * * @param {NodeUniformsGroup} nodeUniformsGroup - The node uniforms group. @@ -1129,7 +1161,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 // check for update -@@ -56056,6 +56545,21 @@ class NodeManager extends DataMap { +@@ -56056,6 +56544,21 @@ class NodeManager extends DataMap { } @@ -1151,7 +1183,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 /** * Triggers the call of `updateBefore()` methods * for all nodes of the given render object. -@@ -56065,9 +56569,14 @@ class NodeManager extends DataMap { +@@ -56065,9 +56568,14 @@ class NodeManager extends DataMap { updateBefore( renderObject ) { const nodeBuilder = renderObject.getNodeBuilderState(); @@ -1166,7 +1198,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 // update frame state for each node this.getNodeFrameForRender( renderObject ).updateBeforeNode( node ); -@@ -56085,10 +56594,11 @@ class NodeManager extends DataMap { +@@ -56085,10 +56593,11 @@ class NodeManager extends DataMap { updateAfter( renderObject ) { const nodeBuilder = renderObject.getNodeBuilderState(); @@ -1179,7 +1211,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 this.getNodeFrameForRender( renderObject ).updateAfterNode( node ); -@@ -56125,9 +56635,14 @@ class NodeManager extends DataMap { +@@ -56125,9 +56634,14 @@ class NodeManager extends DataMap { const nodeFrame = this.getNodeFrameForRender( renderObject ); const nodeBuilder = renderObject.getNodeBuilderState(); @@ -1194,7 +1226,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 nodeFrame.updateNode( node ); } -@@ -61118,6 +61633,18 @@ class Renderer { +@@ -61118,6 +61632,18 @@ class Renderer { } @@ -1213,7 +1245,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 /** * Can be used to transfer buffer data from a storage buffer attribute * from the GPU to the CPU in context of compute shaders. -@@ -62111,6 +62638,14 @@ class Renderer { +@@ -62111,6 +62637,14 @@ class Renderer { } @@ -1228,7 +1260,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 /** * Initializes the given render target. * -@@ -62731,6 +63266,7 @@ class Renderer { +@@ -62731,6 +63265,7 @@ class Renderer { let materialDisplacementMap; let materialDisplacementScale; let materialDisplacementBias; @@ -1236,7 +1268,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 const previousSourceMaterial = this._currentSourceMaterial; -@@ -62756,6 +63292,7 @@ class Renderer { +@@ -62756,6 +63291,7 @@ class Renderer { materialDisplacementMap = overrideMaterial.displacementMap; materialDisplacementScale = overrideMaterial.displacementScale; materialDisplacementBias = overrideMaterial.displacementBias; @@ -1244,7 +1276,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 if ( material.positionNode && material.positionNode.isNode ) { -@@ -62763,7 +63300,15 @@ class Renderer { +@@ -62763,7 +63299,15 @@ class Renderer { } @@ -1261,7 +1293,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 overrideMaterial.alphaMap = material.alphaMap; overrideMaterial.displacementMap = material.displacementMap; overrideMaterial.displacementScale = material.displacementScale; -@@ -62825,6 +63370,7 @@ class Renderer { +@@ -62825,6 +63369,7 @@ class Renderer { scene.overrideMaterial.displacementMap = materialDisplacementMap; scene.overrideMaterial.displacementScale = materialDisplacementScale; scene.overrideMaterial.displacementBias = materialDisplacementBias; @@ -1269,17 +1301,17 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -62886,18 +63432,17 @@ class Renderer { +@@ -62886,18 +63431,17 @@ class Renderer { // - const needsRefresh = this._nodes.needsRefresh( renderObject ); - - if ( needsRefresh ) { -- -- this._nodes.updateBefore( renderObject ); + this._nodes.needsRefresh( renderObject ); +- this._nodes.updateBefore( renderObject ); +- - this._geometries.updateForRender( renderObject ); - - this._nodes.updateForRender( renderObject ); @@ -1298,7 +1330,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 this._pipelines.updateForRender( renderObject ); -@@ -62907,7 +63452,7 @@ class Renderer { +@@ -62907,7 +63451,7 @@ class Renderer { this.backend.draw( renderObject, this.info ); @@ -1307,7 +1339,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -76397,6 +76942,9 @@ class WebGPUTextureUtils { +@@ -76397,6 +76941,9 @@ class WebGPUTextureUtils { */ this._samplerCache = new Map(); @@ -1317,7 +1349,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } /** -@@ -76613,7 +77161,12 @@ class WebGPUTextureUtils { +@@ -76613,7 +77160,12 @@ class WebGPUTextureUtils { } @@ -1331,7 +1363,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 usage |= GPUTextureUsage.RENDER_ATTACHMENT; -@@ -76686,6 +77239,22 @@ class WebGPUTextureUtils { +@@ -76686,6 +77238,22 @@ class WebGPUTextureUtils { destroyTexture( texture, isDefaultTexture = false ) { const backend = this.backend; @@ -1354,7 +1386,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 const textureData = backend.get( texture ); if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -76726,7 +77295,7 @@ class WebGPUTextureUtils { +@@ -76726,7 +77294,7 @@ class WebGPUTextureUtils { const colorTexture = canvasTarget.colorTexture; const colorTextureData = backend.get( colorTexture ); @@ -1363,7 +1395,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 return colorTextureData.texture; -@@ -76751,8 +77320,8 @@ class WebGPUTextureUtils { +@@ -76751,8 +77319,8 @@ class WebGPUTextureUtils { // @@ -1374,7 +1406,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 colorTexture.samples = samples; colorTextureData.texture = colorBuffer; -@@ -76775,64 +77344,59 @@ class WebGPUTextureUtils { +@@ -76775,64 +77343,59 @@ class WebGPUTextureUtils { const canvasTarget = backend.renderer.getCanvasTarget(); const { width, height } = backend.getDrawingBufferSize(); const samples = backend.renderer.currentSamples; @@ -1395,13 +1427,13 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 - } - - // -- -- const depthTextureGPU = backend.get( depthTexture ).texture; + let cache = this._depthBuffers.get( depthTexture ); -- let format, type; +- const depthTextureGPU = backend.get( depthTexture ).texture; + if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { +- let format, type; +- - if ( stencil ) { + this.destroyTexture( depthTexture ); + cache = undefined; @@ -1424,17 +1456,17 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } - if ( depthTextureGPU !== undefined ) { -- -- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { + // Output and direct overlay passes alternate effective samples 4 and 1. + // Retain at most those two buffers for this canvas size and depth format. + let buffer = cache.textures.get( sampleCount ); +- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { +- - return depthTextureGPU; +- +- } + if ( buffer === undefined ) { -- } -- - this.destroyTexture( depthTexture ); + buffer = new DepthTexture( width, height, type ); + buffer.name = 'depthBuffer'; @@ -1467,7 +1499,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 /** * Uploads the updated texture data to the GPU. * -@@ -76987,6 +77551,143 @@ class WebGPUTextureUtils { +@@ -76987,6 +77550,143 @@ class WebGPUTextureUtils { } @@ -1611,7 +1643,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 /** * Returns texture data as a typed array. * -@@ -77060,6 +77761,8 @@ class WebGPUTextureUtils { +@@ -77060,6 +77760,8 @@ class WebGPUTextureUtils { */ dispose() { @@ -1620,7 +1652,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 this._samplerCache.clear(); } -@@ -78986,7 +79689,7 @@ class WGSLNodeBuilder extends NodeBuilder { +@@ -78986,7 +79688,7 @@ class WGSLNodeBuilder extends NodeBuilder { * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. * @return {string} The WGSL snippet. */ @@ -1629,7 +1661,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 if ( levelSnippet === null ) levelSnippet = '0u'; -@@ -79004,7 +79707,14 @@ class WGSLNodeBuilder extends NodeBuilder { +@@ -79004,7 +79706,14 @@ class WGSLNodeBuilder extends NodeBuilder { } else { @@ -1645,7 +1677,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79545,7 +80255,7 @@ class WGSLNodeBuilder extends NodeBuilder { +@@ -79545,7 +80254,7 @@ class WGSLNodeBuilder extends NodeBuilder { // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) // Also textureGather always need sampler. @@ -1654,7 +1686,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 if ( needsSampler ) { -@@ -79586,7 +80296,8 @@ class WGSLNodeBuilder extends NodeBuilder { +@@ -79586,7 +80295,8 @@ class WGSLNodeBuilder extends NodeBuilder { uniformGPU = buffer; @@ -1664,7 +1696,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } else { -@@ -79974,7 +80685,8 @@ ${ flowData.code } +@@ -79974,7 +80684,8 @@ ${ flowData.code } for ( const { name, property, type } of builtins.values() ) { @@ -1674,7 +1706,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -80230,6 +80942,8 @@ ${ flowData.code } +@@ -80230,6 +80941,8 @@ ${ flowData.code } getVaryings( shaderStage ) { const snippets = []; @@ -1683,7 +1715,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 if ( shaderStage === 'vertex' ) { -@@ -80252,7 +80966,12 @@ ${ flowData.code } +@@ -80252,7 +80965,12 @@ ${ flowData.code } let attributesSnippet = `@location( ${ varyingIndex ++ } )`; @@ -1697,7 +1729,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; -@@ -80329,9 +81048,12 @@ ${ flowData.code } +@@ -80329,9 +81047,12 @@ ${ flowData.code } const textureNode = uniform.node; const texture = textureNode.value; @@ -1713,7 +1745,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 if ( needsSampler ) { -@@ -80353,7 +81075,7 @@ ${ flowData.code } +@@ -80353,7 +81074,7 @@ ${ flowData.code } const { primarySamples } = backend.utils.getTextureSampleData( texture ); @@ -1722,7 +1754,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 multisampled = '_multisampled'; -@@ -81108,6 +81830,15 @@ class WebGPUAttributeUtils { +@@ -81108,6 +81829,15 @@ class WebGPUAttributeUtils { buffer.unmap(); bufferData.buffer = buffer; @@ -1738,7 +1770,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -81128,6 +81859,11 @@ class WebGPUAttributeUtils { +@@ -81128,6 +81858,11 @@ class WebGPUAttributeUtils { const bufferData = backend.get( bufferAttribute ); const buffer = backend.get( bufferAttribute ).buffer; @@ -1750,7 +1782,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 let array = bufferAttribute.array; const itemSize = bufferData._itemSize; -@@ -81209,6 +81945,9 @@ class WebGPUAttributeUtils { +@@ -81209,6 +81944,9 @@ class WebGPUAttributeUtils { } @@ -1760,7 +1792,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } /** -@@ -81295,9 +82034,15 @@ class WebGPUAttributeUtils { +@@ -81295,9 +82033,15 @@ class WebGPUAttributeUtils { destroyAttribute( attribute ) { const backend = this.backend; @@ -1778,7 +1810,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 backend.delete( attribute ); -@@ -81598,18 +82343,16 @@ class WebGPUBindingUtils { +@@ -81598,18 +82342,16 @@ class WebGPUBindingUtils { const bindingsData = backend.get( bindGroup ); @@ -1802,7 +1834,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 // try to find an existing layout in the cache -@@ -81830,7 +82573,20 @@ class WebGPUBindingUtils { +@@ -81830,7 +82572,20 @@ class WebGPUBindingUtils { let resourceGPU; @@ -1824,7 +1856,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); -@@ -82001,7 +82757,7 @@ class WebGPUBindingUtils { +@@ -82001,7 +82756,7 @@ class WebGPUBindingUtils { const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); @@ -1833,7 +1865,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 texture.multisampled = true; -@@ -82039,7 +82795,7 @@ class WebGPUBindingUtils { +@@ -82039,7 +82794,7 @@ class WebGPUBindingUtils { } else if ( type === FloatType ) { @@ -1842,7 +1874,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 texture.sampleType = GPUTextureSampleType.Float; -@@ -82131,6 +82887,9 @@ class WebGPUBindingUtils { +@@ -82131,6 +82886,9 @@ class WebGPUBindingUtils { } @@ -1852,7 +1884,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } /** -@@ -82553,6 +83312,15 @@ class WebGPUPipelineUtils { +@@ -82553,6 +83311,15 @@ class WebGPUPipelineUtils { _renderPipelineDescriptor.reset(); @@ -1868,7 +1900,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 if ( pipelinePromise !== null ) { try { -@@ -82567,7 +83335,7 @@ class WebGPUPipelineUtils { +@@ -82567,7 +83334,7 @@ class WebGPUPipelineUtils { } @@ -1877,7 +1909,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 if ( errorScope !== null || asyncError !== null ) { -@@ -83814,6 +84582,27 @@ class WebGPUBackend extends Backend { +@@ -83814,6 +84581,27 @@ class WebGPUBackend extends Backend { */ this.compatibilityMode = null; @@ -1905,7 +1937,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 /** * A reference to the device. * -@@ -83926,7 +84715,11 @@ class WebGPUBackend extends Backend { +@@ -83926,7 +84714,11 @@ class WebGPUBackend extends Backend { xrCompatible: renderer.xr.enabled }; @@ -1918,7 +1950,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 if ( adapter === null ) { -@@ -84153,7 +84946,9 @@ class WebGPUBackend extends Backend { +@@ -84153,7 +84945,9 @@ class WebGPUBackend extends Backend { let descriptor = canvasData.descriptor; @@ -1929,7 +1961,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 descriptor = new GPURenderPassDescriptor(); descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84180,6 +84975,9 @@ class WebGPUBackend extends Backend { +@@ -84180,6 +84974,9 @@ class WebGPUBackend extends Backend { canvasData.descriptor = descriptor; canvasData.samples = samples; @@ -1939,7 +1971,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -85461,13 +86259,12 @@ class WebGPUBackend extends Backend { +@@ -85461,13 +86258,12 @@ class WebGPUBackend extends Backend { for ( let i = 0, l = bindings.length; i < l; i ++ ) { const bindGroup = bindings[ i ]; @@ -1956,7 +1988,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 } -@@ -85766,6 +86563,7 @@ class WebGPUBackend extends Backend { +@@ -85766,6 +86562,7 @@ class WebGPUBackend extends Backend { const utils = this.utils; const sampleCount = utils.getSampleCountRenderContext( renderObject.context ); @@ -1964,7 +1996,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 const colorSpace = utils.getCurrentColorSpace( renderObject.context ); const colorFormat = utils.getCurrentColorFormat( renderObject.context ); const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context ); -@@ -85787,7 +86585,8 @@ class WebGPUBackend extends Backend { +@@ -85787,7 +86584,8 @@ class WebGPUBackend extends Backend { data.colorFormat !== colorFormat || data.depthStencilFormat !== depthStencilFormat || data.primitiveTopology !== primitiveTopology || data.frontFaceCW !== frontFaceCW || @@ -1974,7 +2006,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 ) { data.material = material; data.materialVersion = material.version; -@@ -85807,6 +86606,7 @@ class WebGPUBackend extends Backend { +@@ -85807,6 +86605,7 @@ class WebGPUBackend extends Backend { data.primitiveTopology = primitiveTopology; data.frontFaceCW = frontFaceCW; data.clippingContextCacheKey = renderObject.clippingContextCacheKey; @@ -1982,7 +2014,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 needsUpdate = true; -@@ -85828,6 +86628,7 @@ class WebGPUBackend extends Backend { +@@ -85828,6 +86627,7 @@ class WebGPUBackend extends Backend { const utils = this.utils; const renderContext = renderObject.context; @@ -1990,7 +2022,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 // meshes with negative scale have a different frontFace render pipeline // descriptor value so the following must be honored in the cache key -@@ -85846,6 +86647,7 @@ class WebGPUBackend extends Backend { +@@ -85846,6 +86646,7 @@ class WebGPUBackend extends Backend { material.side, frontFaceCW, utils.getSampleCountRenderContext( renderContext ), @@ -1998,7 +2030,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 utils.getCurrentColorSpace( renderContext ), utils.getCurrentColorFormat( renderContext ), utils.getCurrentDepthStencilFormat( renderContext ), utils.getPrimitiveTopology( object, material ), renderObject.getGeometryCacheKey(), -@@ -85916,6 +86718,12 @@ class WebGPUBackend extends Backend { +@@ -85916,6 +86717,12 @@ class WebGPUBackend extends Backend { } @@ -2012,7 +2044,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..a308a892c59283b3b9a71477b3b02d64 * Generates mipmaps for the given texture. * diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js -index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30dd19e0cb 100644 +index f707acc24f5711fdae065ce920b08435b0869c1b..6581a8b0e69dea0ce5bfdc644f75a40f6aaac963 100644 --- a/build/three.webgpu.nodes.js +++ b/build/three.webgpu.nodes.js @@ -141,6 +141,15 @@ class NodeMaterialObserver { @@ -2281,27 +2313,36 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } else if ( levelSnippet ) { -@@ -18320,7 +18403,8 @@ function createInstanceMatrixNode( builder, instanceMatrix ) { +@@ -18318,13 +18401,8 @@ function createInstanceMatrixNode( builder, instanceMatrix ) { - const uniformBufferSize = matrixCount * 16 * 4; + } else { +- const uniformBufferSize = matrixCount * 16 * 4; +- - if ( uniformBufferSize <= builder.getUniformBufferLimit() ) { +- +- instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', matrixCount ).element( instanceIndex ); +- +- } else { + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change -+ if ( false ) { ++ { - instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', matrixCount ).element( instanceIndex ); + let interleaved = _matrixBuffers.get( instanceMatrix ); -@@ -18414,7 +18498,8 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { +@@ -18412,9 +18490,10 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { - const uniformBufferSize = Math.max( matrices.count, 1 ) * 16 * 4; + if ( ! isStorageMatrix ) { + +- const uniformBufferSize = Math.max( matrices.count, 1 ) * 16 * 4; ++ Math.max( matrices.count, 1 ) * 16 * 4; - if ( uniformBufferSize > builder.getUniformBufferLimit() ) { + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change -+ if ( true ) { ++ { interleavedMatrix = _matrixBuffers.get( matrices ); -@@ -18455,7 +18540,7 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { +@@ -18455,7 +18534,7 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { // Synchronization of dynamic buffer updates per frame if ( interleavedMatrix !== null || interleavedColor !== null ) { @@ -2310,7 +2351,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 if ( interleavedMatrix !== null ) { -@@ -18496,11 +18581,28 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { +@@ -18496,11 +18575,28 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => { const instancedMesh = builder.object; @@ -2324,25 +2365,53 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 + if ( interleaved !== undefined ) interleaved.needsUpdate = true; + + }; - -- const previousInstanceData = _previousInstanceMatrices.get( object ); ++ + // Scheduled history must reach the GPU in this draw, before the geometry uploads. + OnBeforeObjectUpdate( ( { object } ) => { - -- previousInstanceData.previousInstanceMatrix.array.set( matrices.array ); ++ + const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ]; + if ( scheduledPrevious !== undefined ) updatePrevious( object, scheduledPrevious ); + + } ); -+ + +- const previousInstanceData = _previousInstanceMatrices.get( object ); + // Without a scheduler, retain Three's draw-time snapshot for the next draw. + OnObjectUpdate( ( { object } ) => { -+ + +- previousInstanceData.previousInstanceMatrix.array.set( matrices.array ); + if ( object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ] === undefined ) updatePrevious( object, matrices.array ); } ); -@@ -18745,7 +18847,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve +@@ -18599,6 +18695,7 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => { + const indirectId = getIndirectIndex( batchMesh._indirectTexture, int( batchingIdNode ) ); + + const matricesTexture = batchMesh._matricesTexture; ++ const previousMatricesTexture = batchMesh._previousMatricesTexture ?? matricesTexture; + + const size = int( textureSize( textureLoad( matricesTexture ), 0 ).x ).toConst(); + const j = float( indirectId ).mul( 4 ).toInt().toConst(); +@@ -18625,6 +18722,19 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => { + const bm = mat3( batchingMatrix ); + + positionLocal.assign( batchingMatrix.mul( positionLocal ) ); ++ if ( builder.needsPreviousData() ) { ++ ++ const previousBatchingMatrix = mat4( ++ textureLoad( previousMatricesTexture, ivec2( x, y ) ), ++ textureLoad( previousMatricesTexture, ivec2( x.add( 1 ), y ) ), ++ textureLoad( previousMatricesTexture, ivec2( x.add( 2 ), y ) ), ++ textureLoad( previousMatricesTexture, ivec2( x.add( 3 ), y ) ) ++ ); ++ ++ positionPrevious.assign( previousBatchingMatrix.mul( positionPrevious ).xyz ); ++ ++ } ++ + + const transformedNormal = normalLocal.div( vec3( bm[ 0 ].dot( bm[ 0 ] ), bm[ 1 ].dot( bm[ 1 ] ), bm[ 2 ].dot( bm[ 2 ] ) ) ); + +@@ -18745,7 +18855,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve } @@ -2359,7 +2428,110 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } -@@ -31165,6 +31275,14 @@ class Geometries extends DataMap { +@@ -18777,7 +18895,8 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => { + + if ( skeletonData !== undefined ) { + +- skeletonData.previousBoneMatrices.set( skeleton.boneMatrices ); ++ const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousBoneMatrices' ) ]; ++ skeletonData.previousBoneMatrices.set( scheduledPrevious ?? skeleton.boneMatrices ); + + } + +@@ -18844,7 +18963,8 @@ const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ], + + if ( state !== undefined ) { + +- state.previousBoneMatrices.set( skeleton.boneMatrices ); ++ const scheduledPrevious = skinnedMesh[ Symbol.for( 'threenative.velocity.previousBoneMatrices' ) ]; ++ state.previousBoneMatrices.set( scheduledPrevious ?? skeleton.boneMatrices ); + + } + +@@ -29911,6 +30031,18 @@ class RenderObject { + */ + this._sourceMaterial = renderer._currentSourceMaterial; + ++ /** ++ * The version of `_sourceMaterial` this render object was last validated against. ++ * ++ * An override material is shared by every object in the pass and is used as a per-object ++ * scratchpad, so its own `version` moves for reasons that have nothing to do with this ++ * render object. The source material's version is the signal that does. ++ * ++ * @type {number} ++ * @default 0 ++ */ ++ this.sourceVersion = ( this._sourceMaterial === null || this._sourceMaterial === undefined ) ? 0 : this._sourceMaterial.version; ++ + /** + * An event listener which is defined by `RenderObjects`. It performs + * clean up tasks when `dispose()` on this render object. +@@ -30098,7 +30230,7 @@ class RenderObject { + */ + getChainArray() { + +- return [ this.object, this.material, this.context, this.lightsNode ]; ++ return [ this.object, this.material, this.context, this.lightsNode, this._sourceMaterial ?? this.material ]; + + } + +@@ -30548,6 +30680,7 @@ class RenderObject { + } + + cacheKey = hash$1( cacheKey, this.renderer.contextNode.id, this.renderer.contextNode.version ); ++ cacheKey = hash$1( cacheKey, this.context.sampleCount ); + + return cacheKey; + +@@ -30680,6 +30813,8 @@ class RenderObjects { + _chainKeys$3[ 1 ] = material; + _chainKeys$3[ 2 ] = renderContext; + _chainKeys$3[ 3 ] = lightsNode; ++ // Material groups share the shadow override, but must retain separate GPU bindings. ++ _chainKeys$3[ 4 ] = this.renderer._currentSourceMaterial ?? material; + + // + +@@ -30707,9 +30842,18 @@ class RenderObjects { + + } + +- if ( renderObject.version !== material.version || renderObject.needsUpdate ) { ++ // When the renderer is drawing through an override material, that material is shared by ++ // the whole pass and mutated per object, so its `version` alone answers the wrong ++ // question. The source material this object actually carries answers the right one. ++ ++ const sourceMaterial = this.renderer._currentSourceMaterial; ++ const sourceTracked = sourceMaterial !== null && sourceMaterial !== undefined; ++ const sourceReplaced = sourceTracked && renderObject._sourceMaterial !== sourceMaterial; ++ const sourceChanged = sourceTracked && ( sourceReplaced || renderObject.sourceVersion !== sourceMaterial.version ); + +- if ( renderObject.initialCacheKey !== renderObject.getCacheKey() ) { ++ if ( renderObject.version !== material.version || sourceChanged || renderObject.needsUpdate ) { ++ ++ if ( sourceReplaced || renderObject.initialCacheKey !== renderObject.getCacheKey() ) { + + renderObject.dispose(); + +@@ -30719,6 +30863,8 @@ class RenderObjects { + + renderObject.version = material.version; + ++ if ( sourceTracked ) renderObject.sourceVersion = sourceMaterial.version; ++ + } + + } +@@ -30731,6 +30877,7 @@ class RenderObjects { + _chainKeys$3[ 1 ] = null; + _chainKeys$3[ 2 ] = null; + _chainKeys$3[ 3 ] = null; ++ _chainKeys$3[ 4 ] = null; + + // + +@@ -31165,6 +31312,14 @@ class Geometries extends DataMap { */ this._geometryDisposeListeners = new Map(); @@ -2374,7 +2546,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } /** -@@ -31190,10 +31308,69 @@ class Geometries extends DataMap { +@@ -31190,10 +31345,69 @@ class Geometries extends DataMap { if ( this.has( renderObject ) === false ) this.initGeometry( renderObject ); @@ -2444,7 +2616,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 /** * Initializes the geometry of the given render object. * -@@ -32771,7 +32948,7 @@ class Bindings extends DataMap { +@@ -32771,7 +32985,7 @@ class Bindings extends DataMap { */ updateForRender( renderObject ) { @@ -2453,7 +2625,22 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } -@@ -32918,12 +33095,13 @@ class Bindings extends DataMap { +@@ -32832,10 +33046,14 @@ class Bindings extends DataMap { + + } else if ( binding.isSampledTexture ) { + ++ // Async node compilation can yield while a viewport node changes targets. ++ // Refresh its reference before creating the first GPU binding, not after. ++ binding.update(); + this.textures.updateTexture( binding.texture ); + + } else if ( binding.isSampler ) { + ++ binding.update(); + this.textures.updateSampler( binding ); + + } else if ( binding.isStorageBuffer ) { +@@ -32918,12 +33136,13 @@ class Bindings extends DataMap { * Updates the given array of bindings. * * @param {Array} bindings - The bind groups. @@ -2469,7 +2656,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } -@@ -32934,11 +33112,27 @@ class Bindings extends DataMap { +@@ -32934,11 +33153,27 @@ class Bindings extends DataMap { * * @param {BindGroup} bindGroup - The bind group to update. * @param {Array} bindings - The bind groups. @@ -2498,7 +2685,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 let needsBindingsUpdate = false; let cacheBindings = true; let cacheIndex = 0; -@@ -32948,7 +33142,7 @@ class Bindings extends DataMap { +@@ -32948,7 +33183,7 @@ class Bindings extends DataMap { for ( const binding of bindGroup.bindings ) { @@ -2507,7 +2694,17 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 // every uniforms group is a uniform buffer. So if no update is required, // we move one with the next binding. Otherwise the next if block will update the group. -@@ -34020,6 +34214,10 @@ class Textures extends DataMap { +@@ -33952,7 +34187,8 @@ class RenderContexts { + + } + +- if ( renderTarget !== null ) renderState.sampleCount = renderTarget.samples === 0 ? 1 : renderTarget.samples; ++ const sampleCount = renderTarget !== null ? renderTarget.samples : this.renderer.currentSamples; ++ renderState.sampleCount = sampleCount === 0 ? 1 : sampleCount; + + renderState.clearDepthValue = this.renderer.getClearDepth(); + renderState.clearStencilValue = this.renderer.getClearStencil(); +@@ -34020,6 +34256,10 @@ class Textures extends DataMap { * @type {Set} */ this._htmlTextures = new Set(); @@ -2518,7 +2715,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } -@@ -34169,7 +34367,16 @@ class Textures extends DataMap { +@@ -34169,7 +34409,16 @@ class Textures extends DataMap { */ updateTexture( texture, options = {} ) { @@ -2535,7 +2732,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 if ( textureData.initialized === true && textureData.version === texture.version ) return; const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; -@@ -34181,6 +34388,21 @@ class Textures extends DataMap { +@@ -34181,6 +34430,21 @@ class Textures extends DataMap { backend.destroyTexture( texture ); @@ -2557,7 +2754,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } // -@@ -34318,7 +34540,16 @@ class Textures extends DataMap { +@@ -34318,7 +34582,16 @@ class Textures extends DataMap { } @@ -2575,7 +2772,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; -@@ -34328,7 +34559,7 @@ class Textures extends DataMap { +@@ -34328,7 +34601,7 @@ class Textures extends DataMap { } @@ -2584,7 +2781,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } -@@ -34345,6 +34576,17 @@ class Textures extends DataMap { +@@ -34345,6 +34618,17 @@ class Textures extends DataMap { } @@ -2602,7 +2799,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 // dispose handler if ( textureData.initialized !== true ) { -@@ -34377,9 +34619,134 @@ class Textures extends DataMap { +@@ -34377,9 +34661,134 @@ class Textures extends DataMap { } @@ -2739,7 +2936,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } -@@ -34557,6 +34924,8 @@ class Textures extends DataMap { +@@ -34557,6 +34966,8 @@ class Textures extends DataMap { */ _destroyTexture( texture ) { @@ -2748,7 +2945,17 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 if ( this.has( texture ) === true ) { const textureData = this.get( texture ); -@@ -40270,7 +40639,7 @@ class PassMultipleTextureNode extends PassTextureNode { +@@ -39725,7 +40136,8 @@ class VelocityNode extends TempNode { + + const previousModelMatrix = getPreviousMatrix( object ); + +- this.previousModelWorldMatrix.value.copy( previousModelMatrix ); ++ const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousWorldMatrix' ) ]; ++ this.previousModelWorldMatrix.value.copy( scheduledPrevious ?? previousModelMatrix ); + + // + +@@ -40270,7 +40682,7 @@ class PassMultipleTextureNode extends PassTextureNode { * @param {string} textureName - The output texture name. * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. */ @@ -2757,7 +2964,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 // null is passed to the super call since this class does not // use an external texture for rendering pass data into. Instead -@@ -40291,6 +40660,8 @@ class PassMultipleTextureNode extends PassTextureNode { +@@ -40291,6 +40703,8 @@ class PassMultipleTextureNode extends PassTextureNode { * @type {boolean} */ this.previousTexture = previousTexture; @@ -2766,7 +2973,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 /** * This flag can be used for type testing. -@@ -40312,17 +40683,33 @@ class PassMultipleTextureNode extends PassTextureNode { +@@ -40312,17 +40726,33 @@ class PassMultipleTextureNode extends PassTextureNode { } @@ -2801,7 +3008,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 newNode.uvNode = this.uvNode; newNode.levelNode = this.levelNode; newNode.biasNode = this.biasNode; -@@ -40503,6 +40890,8 @@ class PassNode extends TempNode { +@@ -40503,6 +40933,8 @@ class PassNode extends TempNode { * @type {Object} */ this._textureNodes = {}; @@ -2810,7 +3017,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 /** * A dictionary holding the internal depth nodes. -@@ -40809,6 +41198,7 @@ class PassNode extends TempNode { +@@ -40809,6 +41241,7 @@ class PassNode extends TempNode { this._previousTextures[ name ] = texture; this._textureNodes[ name ].updateTexture(); @@ -2818,7 +3025,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 this._previousTextureNodes[ name ].updateTexture(); } -@@ -40821,6 +41211,17 @@ class PassNode extends TempNode { +@@ -40821,6 +41254,17 @@ class PassNode extends TempNode { * @param {string} [name='output'] - The output name to get the texture node for. * @return {TextureNode} The texture node. */ @@ -2836,7 +3043,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 getTextureNode( name = 'output' ) { let textureNode = this._textureNodes[ name ]; -@@ -41163,6 +41564,7 @@ class PassNode extends TempNode { +@@ -41163,6 +41607,7 @@ class PassNode extends TempNode { */ dispose() { @@ -2844,7 +3051,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 this.renderTarget.dispose(); } -@@ -44431,6 +44833,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ +@@ -44431,6 +44876,46 @@ var RendererUtils = /*#__PURE__*/Object.freeze({ const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); @@ -2891,7 +3098,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 /** * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map * with a binary `[0,1]` result. -@@ -44485,8 +44927,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, +@@ -44485,8 +44970,8 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, }; @@ -2902,7 +3109,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 const texelSize = vec2( 1 ).div( mapSize ); const radiusScaled = radius.mul( texelSize.x ); -@@ -44517,7 +44959,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, +@@ -44517,7 +45002,7 @@ const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, */ const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { @@ -2911,7 +3118,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 const texelSize = vec2( 1 ).div( mapSize ); -@@ -45586,8 +46028,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) +@@ -45586,8 +46071,8 @@ const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) */ const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { @@ -2922,7 +3129,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 const texelSize = radius.div( mapSize.x ); -@@ -45623,9 +46065,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo +@@ -45623,9 +46108,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo const shadowPositionAbs = shadowPosition.abs().toConst(); const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); @@ -2935,7 +3142,24 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 const result = float( 1.0 ).toVar(); -@@ -55244,15 +55686,17 @@ class NodeManager extends DataMap { +@@ -48407,6 +48892,7 @@ var TSL = /*#__PURE__*/Object.freeze({ + getShIrradianceAt: getShIrradianceAt, + getShadowMaterial: getShadowMaterial, + getShadowRenderObjectFunction: getShadowRenderObjectFunction, ++ getShadowUniform: getShadowUniform, + getTextureIndex: getTextureIndex, + getViewPosition: getViewPosition, + ggxConvolution: ggxConvolution, +@@ -53841,7 +54327,7 @@ class NodeBuilder { + + const mrt = this.renderer.getMRT(); + +- return ( mrt && mrt.has( 'velocity' ) ) || getDataFromObject( this.object ).useVelocity === true; ++ return ( mrt && mrt.has( 'velocity' ) ) || getDataFromObject( this.object ).useVelocity === true || this.object.userData?.useVelocity === true; + + } + +@@ -55244,15 +55730,17 @@ class NodeManager extends DataMap { * Returns `true` if the given node uniforms group must be updated or not. * * @param {NodeUniformsGroup} nodeUniformsGroup - The node uniforms group. @@ -2956,7 +3180,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 // check for update -@@ -56056,6 +56500,21 @@ class NodeManager extends DataMap { +@@ -56056,6 +56544,21 @@ class NodeManager extends DataMap { } @@ -2978,7 +3202,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 /** * Triggers the call of `updateBefore()` methods * for all nodes of the given render object. -@@ -56065,9 +56524,14 @@ class NodeManager extends DataMap { +@@ -56065,9 +56568,14 @@ class NodeManager extends DataMap { updateBefore( renderObject ) { const nodeBuilder = renderObject.getNodeBuilderState(); @@ -2993,7 +3217,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 // update frame state for each node this.getNodeFrameForRender( renderObject ).updateBeforeNode( node ); -@@ -56085,10 +56549,11 @@ class NodeManager extends DataMap { +@@ -56085,10 +56593,11 @@ class NodeManager extends DataMap { updateAfter( renderObject ) { const nodeBuilder = renderObject.getNodeBuilderState(); @@ -3006,7 +3230,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 this.getNodeFrameForRender( renderObject ).updateAfterNode( node ); -@@ -56125,9 +56590,14 @@ class NodeManager extends DataMap { +@@ -56125,9 +56634,14 @@ class NodeManager extends DataMap { const nodeFrame = this.getNodeFrameForRender( renderObject ); const nodeBuilder = renderObject.getNodeBuilderState(); @@ -3021,7 +3245,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 nodeFrame.updateNode( node ); } -@@ -61118,6 +61588,18 @@ class Renderer { +@@ -61118,6 +61632,18 @@ class Renderer { } @@ -3040,7 +3264,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 /** * Can be used to transfer buffer data from a storage buffer attribute * from the GPU to the CPU in context of compute shaders. -@@ -62111,6 +62593,14 @@ class Renderer { +@@ -62111,6 +62637,14 @@ class Renderer { } @@ -3055,19 +3279,60 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 /** * Initializes the given render target. * -@@ -62886,18 +63376,17 @@ class Renderer { +@@ -62731,6 +63265,7 @@ class Renderer { + let materialDisplacementMap; + let materialDisplacementScale; + let materialDisplacementBias; ++ let materialAlphaTest; + + const previousSourceMaterial = this._currentSourceMaterial; + +@@ -62756,6 +63291,7 @@ class Renderer { + materialDisplacementMap = overrideMaterial.displacementMap; + materialDisplacementScale = overrideMaterial.displacementScale; + materialDisplacementBias = overrideMaterial.displacementBias; ++ materialAlphaTest = overrideMaterial.alphaTest; + + if ( material.positionNode && material.positionNode.isNode ) { + +@@ -62763,7 +63299,15 @@ class Renderer { + + } + +- overrideMaterial.alphaTest = material.alphaTest; ++ // `Material.alphaTest`'s setter bumps `version` whenever alpha-test-ness flips, and this ++ // override material is shared by every caster in the pass. A scene that mixes alpha-tested ++ // casters with opaque ones therefore moves that shared version several times per frame, ++ // and every OTHER render object drawn with it then fails the version gate in ++ // `RenderObjects.get` and recomputes a full material cache key that always matches. ++ // Writing the backing field leaves the `alphaTest` getter — and so the cache key — still ++ // reporting this caster's value, while each render object tracks the staleness signal ++ // that belongs to it: its own source material's version. ++ overrideMaterial._alphaTest = material.alphaTest; + overrideMaterial.alphaMap = material.alphaMap; + overrideMaterial.displacementMap = material.displacementMap; + overrideMaterial.displacementScale = material.displacementScale; +@@ -62825,6 +63369,7 @@ class Renderer { + scene.overrideMaterial.displacementMap = materialDisplacementMap; + scene.overrideMaterial.displacementScale = materialDisplacementScale; + scene.overrideMaterial.displacementBias = materialDisplacementBias; ++ scene.overrideMaterial._alphaTest = materialAlphaTest; + + } + +@@ -62886,18 +63431,17 @@ class Renderer { // - const needsRefresh = this._nodes.needsRefresh( renderObject ); - - if ( needsRefresh ) { -- ++ this._nodes.needsRefresh( renderObject ); + - this._nodes.updateBefore( renderObject ); - - this._geometries.updateForRender( renderObject ); -+ this._nodes.needsRefresh( renderObject ); - +- - this._nodes.updateForRender( renderObject ); - this._bindings.updateForRender( renderObject ); - @@ -3084,7 +3349,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 this._pipelines.updateForRender( renderObject ); -@@ -62907,7 +63396,7 @@ class Renderer { +@@ -62907,7 +63451,7 @@ class Renderer { this.backend.draw( renderObject, this.info ); @@ -3093,7 +3358,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } -@@ -76397,6 +76886,9 @@ class WebGPUTextureUtils { +@@ -76397,6 +76941,9 @@ class WebGPUTextureUtils { */ this._samplerCache = new Map(); @@ -3103,7 +3368,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } /** -@@ -76613,7 +77105,12 @@ class WebGPUTextureUtils { +@@ -76613,7 +77160,12 @@ class WebGPUTextureUtils { } @@ -3117,7 +3382,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 usage |= GPUTextureUsage.RENDER_ATTACHMENT; -@@ -76686,6 +77183,22 @@ class WebGPUTextureUtils { +@@ -76686,6 +77238,22 @@ class WebGPUTextureUtils { destroyTexture( texture, isDefaultTexture = false ) { const backend = this.backend; @@ -3140,7 +3405,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 const textureData = backend.get( texture ); if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); -@@ -76726,7 +77239,7 @@ class WebGPUTextureUtils { +@@ -76726,7 +77294,7 @@ class WebGPUTextureUtils { const colorTexture = canvasTarget.colorTexture; const colorTextureData = backend.get( colorTexture ); @@ -3149,7 +3414,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 return colorTextureData.texture; -@@ -76751,8 +77264,8 @@ class WebGPUTextureUtils { +@@ -76751,8 +77319,8 @@ class WebGPUTextureUtils { // @@ -3160,7 +3425,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 colorTexture.samples = samples; colorTextureData.texture = colorBuffer; -@@ -76775,64 +77288,59 @@ class WebGPUTextureUtils { +@@ -76775,64 +77343,59 @@ class WebGPUTextureUtils { const canvasTarget = backend.renderer.getCanvasTarget(); const { width, height } = backend.getDrawingBufferSize(); const samples = backend.renderer.currentSamples; @@ -3253,7 +3518,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 /** * Uploads the updated texture data to the GPU. * -@@ -76987,6 +77495,143 @@ class WebGPUTextureUtils { +@@ -76987,6 +77550,143 @@ class WebGPUTextureUtils { } @@ -3397,7 +3662,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 /** * Returns texture data as a typed array. * -@@ -77060,6 +77705,8 @@ class WebGPUTextureUtils { +@@ -77060,6 +77760,8 @@ class WebGPUTextureUtils { */ dispose() { @@ -3406,7 +3671,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 this._samplerCache.clear(); } -@@ -78986,7 +79633,7 @@ class WGSLNodeBuilder extends NodeBuilder { +@@ -78986,7 +79688,7 @@ class WGSLNodeBuilder extends NodeBuilder { * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. * @return {string} The WGSL snippet. */ @@ -3415,7 +3680,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 if ( levelSnippet === null ) levelSnippet = '0u'; -@@ -79004,7 +79651,14 @@ class WGSLNodeBuilder extends NodeBuilder { +@@ -79004,7 +79706,14 @@ class WGSLNodeBuilder extends NodeBuilder { } else { @@ -3431,7 +3696,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { -@@ -79545,7 +80199,7 @@ class WGSLNodeBuilder extends NodeBuilder { +@@ -79545,7 +80254,7 @@ class WGSLNodeBuilder extends NodeBuilder { // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) // Also textureGather always need sampler. @@ -3440,7 +3705,17 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 if ( needsSampler ) { -@@ -79974,7 +80628,8 @@ ${ flowData.code } +@@ -79586,7 +80295,8 @@ class WGSLNodeBuilder extends NodeBuilder { + + uniformGPU = buffer; + +- uniformNode.name = name ? name : 'NodeBuffer_' + uniformNode.id; ++ // The base builder supplies a unique name within this program. A process-wide ++ // allocation id changes otherwise identical shader source for every buffer. + + } else { + +@@ -79974,7 +80684,8 @@ ${ flowData.code } for ( const { name, property, type } of builtins.values() ) { @@ -3450,7 +3725,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } -@@ -80230,6 +80885,8 @@ ${ flowData.code } +@@ -80230,6 +80941,8 @@ ${ flowData.code } getVaryings( shaderStage ) { const snippets = []; @@ -3459,7 +3734,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 if ( shaderStage === 'vertex' ) { -@@ -80252,7 +80909,12 @@ ${ flowData.code } +@@ -80252,7 +80965,12 @@ ${ flowData.code } let attributesSnippet = `@location( ${ varyingIndex ++ } )`; @@ -3473,7 +3748,23 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; -@@ -80353,7 +81015,7 @@ ${ flowData.code } +@@ -80329,9 +81047,12 @@ ${ flowData.code } + const textureNode = uniform.node; + const texture = textureNode.value; + +- // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) +- // Also textureGather always need sampler. +- const needsSampler = texture.isCubeTexture === true || ( this.isUnfilterable( texture ) === false && textureNode.isStorageTextureNode !== true ) || textureNode.gatherNode !== null; ++ // The bind group layout freezes sampler inclusion once, in getUniformFromNode ++ // (nodeData.uniformGPU). Re-deriving it here from live texture filter state can ++ // disagree with that layout and shift every later binding index. ++ const nodeData = this.getDataFromNode( textureNode, shaderStage, this.globalCache ); ++ const uniformGPU = nodeData.uniformGPU; ++ const needsSampler = Array.isArray( uniformGPU ) && uniformGPU[ 0 ].isSampler === true && uniformGPU[ 0 ].isSampledTexture !== true; + + if ( needsSampler ) { + +@@ -80353,7 +81074,7 @@ ${ flowData.code } const { primarySamples } = backend.utils.getTextureSampleData( texture ); @@ -3482,7 +3773,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 multisampled = '_multisampled'; -@@ -81108,6 +81770,15 @@ class WebGPUAttributeUtils { +@@ -81108,6 +81829,15 @@ class WebGPUAttributeUtils { buffer.unmap(); bufferData.buffer = buffer; @@ -3498,7 +3789,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } -@@ -81128,6 +81799,11 @@ class WebGPUAttributeUtils { +@@ -81128,6 +81858,11 @@ class WebGPUAttributeUtils { const bufferData = backend.get( bufferAttribute ); const buffer = backend.get( bufferAttribute ).buffer; @@ -3510,7 +3801,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 let array = bufferAttribute.array; const itemSize = bufferData._itemSize; -@@ -81209,6 +81885,9 @@ class WebGPUAttributeUtils { +@@ -81209,6 +81944,9 @@ class WebGPUAttributeUtils { } @@ -3520,7 +3811,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } /** -@@ -81295,9 +81974,15 @@ class WebGPUAttributeUtils { +@@ -81295,9 +82033,15 @@ class WebGPUAttributeUtils { destroyAttribute( attribute ) { const backend = this.backend; @@ -3538,7 +3829,31 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 backend.delete( attribute ); -@@ -81830,7 +82515,20 @@ class WebGPUBindingUtils { +@@ -81598,18 +82342,16 @@ class WebGPUBindingUtils { + + const bindingsData = backend.get( bindGroup ); + +- // check if the the bind group already has a layout ++ const entries = this._createLayoutEntries( bindGroup ); ++ const bindGroupLayoutKey = hashString( JSON.stringify( entries ) ); + + if ( bindingsData.layout ) { + +- return bindingsData.layout.layoutGPU; ++ if ( bindingsData.layoutKey === bindGroupLayoutKey ) return bindingsData.layout.layoutGPU; + +- } ++ this.deleteBindGroupData( bindGroup ); + +- // if not, assing one +- +- const entries = this._createLayoutEntries( bindGroup ); +- const bindGroupLayoutKey = hashString( JSON.stringify( entries ) ); ++ } + + // try to find an existing layout in the cache + +@@ -81830,7 +82572,20 @@ class WebGPUBindingUtils { let resourceGPU; @@ -3560,7 +3875,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); -@@ -82001,7 +82699,7 @@ class WebGPUBindingUtils { +@@ -82001,7 +82756,7 @@ class WebGPUBindingUtils { const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); @@ -3569,7 +3884,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 texture.multisampled = true; -@@ -82039,7 +82737,7 @@ class WebGPUBindingUtils { +@@ -82039,7 +82794,7 @@ class WebGPUBindingUtils { } else if ( type === FloatType ) { @@ -3578,7 +3893,17 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 texture.sampleType = GPUTextureSampleType.Float; -@@ -82553,6 +83251,15 @@ class WebGPUPipelineUtils { +@@ -82131,6 +82886,9 @@ class WebGPUBindingUtils { + + } + ++ bindingsData.groups = []; ++ bindingsData.versions = []; ++ + } + + /** +@@ -82553,6 +83311,15 @@ class WebGPUPipelineUtils { _renderPipelineDescriptor.reset(); @@ -3594,7 +3919,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 if ( pipelinePromise !== null ) { try { -@@ -82567,7 +83274,7 @@ class WebGPUPipelineUtils { +@@ -82567,7 +83334,7 @@ class WebGPUPipelineUtils { } @@ -3603,7 +3928,48 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 if ( errorScope !== null || asyncError !== null ) { -@@ -84153,7 +84860,9 @@ class WebGPUBackend extends Backend { +@@ -83814,6 +84581,27 @@ class WebGPUBackend extends Backend { + */ + this.compatibilityMode = null; + ++ /** ++ * A reference to the WebGPU instance that creates adapters. ++ * ++ * @type {?GPU} ++ * @default null ++ */ ++ this.gpu = null; ++ ++ /** ++ * A reference to the adapter this device was requested from. ++ * ++ * The adapter owns the wire instance every operation in flight completes on. Chromium ++ * collects an adapter the page stops referencing and the instance goes with it, so every ++ * operation still running completes with a shutdown the page reads as ++ * 'A valid external Instance reference no longer exists.' and presentation stops for. ++ * ++ * @type {?GPUAdapter} ++ * @default null ++ */ ++ this.adapter = null; ++ + /** + * A reference to the device. + * +@@ -83926,7 +84714,11 @@ class WebGPUBackend extends Backend { + xrCompatible: renderer.xr.enabled + }; + +- const adapter = ( typeof navigator !== 'undefined' ) ? await navigator.gpu.requestAdapter( adapterOptions ) : null; ++ this.gpu = ( typeof navigator !== 'undefined' ) ? navigator.gpu : null; ++ ++ const adapter = this.gpu !== null ? await this.gpu.requestAdapter( adapterOptions ) : null; ++ ++ this.adapter = adapter; + + if ( adapter === null ) { + +@@ -84153,7 +84945,9 @@ class WebGPUBackend extends Backend { let descriptor = canvasData.descriptor; @@ -3614,7 +3980,7 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 descriptor = new GPURenderPassDescriptor(); descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); -@@ -84180,6 +84889,9 @@ class WebGPUBackend extends Backend { +@@ -84180,6 +84974,9 @@ class WebGPUBackend extends Backend { canvasData.descriptor = descriptor; canvasData.samples = samples; @@ -3624,7 +3990,66 @@ index f707acc24f5711fdae065ce920b08435b0869c1b..95669d28b54250a3857dcb7d1b1b0e30 } -@@ -85916,6 +86628,12 @@ class WebGPUBackend extends Backend { +@@ -85461,13 +86258,12 @@ class WebGPUBackend extends Backend { + for ( let i = 0, l = bindings.length; i < l; i ++ ) { + + const bindGroup = bindings[ i ]; ++ const bindingsData = this.get( bindGroup ); + +- if ( currentBindingGroups[ i ] !== bindGroup.id ) { +- +- const bindingsData = this.get( bindGroup ); ++ if ( currentBindingGroups[ i ] !== bindingsData.group ) { + + passEncoderGPU.setBindGroup( i, bindingsData.group ); +- currentBindingGroups[ i ] = bindGroup.id; ++ currentBindingGroups[ i ] = bindingsData.group; + + } + +@@ -85766,6 +86562,7 @@ class WebGPUBackend extends Backend { + const utils = this.utils; + + const sampleCount = utils.getSampleCountRenderContext( renderObject.context ); ++ const bindGroupLayoutKeys = renderObject.getBindings().map( bindGroup => this.get( bindGroup ).layoutKey ).join( '|' ); + const colorSpace = utils.getCurrentColorSpace( renderObject.context ); + const colorFormat = utils.getCurrentColorFormat( renderObject.context ); + const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context ); +@@ -85787,7 +86584,8 @@ class WebGPUBackend extends Backend { + data.colorFormat !== colorFormat || data.depthStencilFormat !== depthStencilFormat || + data.primitiveTopology !== primitiveTopology || + data.frontFaceCW !== frontFaceCW || +- data.clippingContextCacheKey !== renderObject.clippingContextCacheKey ++ data.clippingContextCacheKey !== renderObject.clippingContextCacheKey || ++ data.bindGroupLayoutKeys !== bindGroupLayoutKeys + ) { + + data.material = material; data.materialVersion = material.version; +@@ -85807,6 +86605,7 @@ class WebGPUBackend extends Backend { + data.primitiveTopology = primitiveTopology; + data.frontFaceCW = frontFaceCW; + data.clippingContextCacheKey = renderObject.clippingContextCacheKey; ++ data.bindGroupLayoutKeys = bindGroupLayoutKeys; + + needsUpdate = true; + +@@ -85828,6 +86627,7 @@ class WebGPUBackend extends Backend { + + const utils = this.utils; + const renderContext = renderObject.context; ++ const bindGroupLayoutKeys = renderObject.getBindings().map( bindGroup => this.get( bindGroup ).layoutKey ).join( '|' ); + + // meshes with negative scale have a different frontFace render pipeline + // descriptor value so the following must be honored in the cache key +@@ -85846,6 +86646,7 @@ class WebGPUBackend extends Backend { + material.side, + frontFaceCW, + utils.getSampleCountRenderContext( renderContext ), ++ bindGroupLayoutKeys, + utils.getCurrentColorSpace( renderContext ), utils.getCurrentColorFormat( renderContext ), utils.getCurrentDepthStencilFormat( renderContext ), + utils.getPrimitiveTopology( object, material ), + renderObject.getGeometryCacheKey(), +@@ -85916,6 +86717,12 @@ class WebGPUBackend extends Backend { } diff --git a/packages/core/scripts/apply-three-patch.mjs b/packages/core/scripts/apply-three-patch.mjs index 131a2214d..9b8b9a877 100644 --- a/packages/core/scripts/apply-three-patch.mjs +++ b/packages/core/scripts/apply-three-patch.mjs @@ -5,10 +5,15 @@ import { readFile, writeFile } from "node:fs/promises"; import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { brotliDecompressSync } from "node:zlib"; const THREE_VERSION = "0.185.1"; +const PIN_COMMIT = "2431a09f46f34c560bc8e44b33be0e567723d5b9"; const PATCH_NAME = `three@${THREE_VERSION}.patch`; const UPGRADE_PATCH_NAME = `three@${THREE_VERSION}-prd269-upgrade.patch`; +const UPGRADE_JSON_VERSION = 2; +const MAX_BUNDLE_BYTES = 8 * 1024 * 1024; +const HEX40 = /^[a-f0-9]{40}$/u; /** Apply the package-owned Three.js patch to the dependency resolved for the consumer. */ export async function applyThreePatch(options = {}) { @@ -32,10 +37,10 @@ export async function applyThreePatch(options = {}) { const patchPath = path.join(packageRoot, "patches", PATCH_NAME); const patch = parsePatch(await readFile(patchPath, "utf8"), patchPath); const upgradePath = path.join(packageRoot, "patches", UPGRADE_PATCH_NAME); - const upgrades = existsSync(upgradePath) - ? parsePatch(await readFile(upgradePath, "utf8"), upgradePath) - : []; - const plans = await planPatch(threeRoot, patch, upgrades); + const upgrade = existsSync(upgradePath) + ? parseUpgrade(await readFile(upgradePath, "utf8"), upgradePath, patch) + : { legacy: [] }; + const plans = await planPatch(threeRoot, patch, upgrade); // `planPatch` refuses any file it does not fully recognise, so every plan that is not already // `patched` is safe to write — including the files an older release of this patch already // touched, which is the state a project upgrading from the previous `latest` is in. @@ -217,7 +222,190 @@ function patchPathFromHeader(header, patchPath) { return relative; } -async function planPatch(threeRoot, files, upgrades) { +function invalid(message, file, cause) { + return new Error( + `TN_THREE_PATCH_INVALID: ${message} in ${file}${cause === undefined ? "" : `: ${String(cause)}`}.`, + ); +} + +function isSafeRelativePath(value) { + return ( + typeof value === "string" && + value.length > 0 && + !value.startsWith("/") && + !value.includes("\\") && + value.split("/").every((part) => part !== "" && part !== "." && part !== "..") + ); +} + +/** + * The upgrade patch is either the current versioned JSON document or the legacy unified diff an + * older release shipped. The JSON is detected by its opening brace; anything else stays the + * legacy reader's input, byte for byte. + */ +function parseUpgrade(text, upgradePath, baseFiles) { + if (!text.trimStart().startsWith("{")) return { legacy: parsePatch(text, upgradePath) }; + return { json: parseUpgradeJson(text, upgradePath, baseFiles), legacy: [] }; +} + +function parseUpgradeJson(text, upgradePath, baseFiles) { + let raw; + try { + raw = JSON.parse(text); + } catch (error) { + throw invalid("upgrade patch is not valid JSON", upgradePath, error); + } + if (raw === null || typeof raw !== "object" || Array.isArray(raw)) { + throw invalid("upgrade patch JSON is not an object", upgradePath); + } + if (raw.version !== UPGRADE_JSON_VERSION) { + throw invalid(`unsupported upgrade version ${String(raw.version)}`, upgradePath); + } + if ( + raw.pin === null || + typeof raw.pin !== "object" || + raw.pin.version !== THREE_VERSION || + raw.pin.commit !== PIN_COMMIT + ) { + throw invalid("upgrade pin must name the supported three version and commit", upgradePath); + } + + const compatOldHashes = new Map(); + if (!Array.isArray(raw.acceptedLegacyCompatPairs) || raw.acceptedLegacyCompatPairs.length === 0) { + throw invalid("acceptedLegacyCompatPairs must be a non-empty array", upgradePath); + } + const seenPairs = new Set(); + for (const pair of raw.acceptedLegacyCompatPairs) { + if (pair === null || typeof pair !== "object") { + throw invalid("acceptedLegacyCompatPairs has a non-object entry", upgradePath); + } + if (!isSafeRelativePath(pair.path)) + throw invalid(`unsafe compat path '${pair.path}'`, upgradePath); + if (!HEX40.test(pair.oldHash ?? "") || !HEX40.test(pair.newHash ?? "")) { + throw invalid(`invalid 40-hex compat hash for '${pair.path}'`, upgradePath); + } + const key = `${pair.path}\0${pair.oldHash}`; + if (seenPairs.has(key)) throw invalid(`duplicate compat pair '${pair.path}'`, upgradePath); + seenPairs.add(key); + const hashes = compatOldHashes.get(pair.path) ?? new Set(); + hashes.add(pair.oldHash); + compatOldHashes.set(pair.path, hashes); + } + + const acceptedLegacyFinalHashes = validateHashMap( + raw.acceptedLegacyFinalHashes, + "acceptedLegacyFinalHashes", + upgradePath, + ); + const generatedFinalHashes = validateHashMap( + raw.generatedFinalHashes, + "generatedFinalHashes", + upgradePath, + ); + for (const pair of raw.acceptedLegacyCompatPairs) { + if (pair.newHash !== acceptedLegacyFinalHashes[pair.path]) { + throw invalid( + `compat pair '${pair.path}' does not match its accepted final hash`, + upgradePath, + ); + } + } + + if (typeof raw.bundleBrotliBase64 !== "string" || raw.bundleBrotliBase64.length === 0) { + throw invalid("bundleBrotliBase64 must be a non-empty string", upgradePath); + } + if ( + !/^[A-Za-z0-9+/]*={0,2}$/u.test(raw.bundleBrotliBase64) || + raw.bundleBrotliBase64.length % 4 !== 0 + ) { + throw invalid("bundleBrotliBase64 is not valid base64", upgradePath); + } + let bundleBuffer; + try { + bundleBuffer = Buffer.from(raw.bundleBrotliBase64, "base64"); + } catch (error) { + throw invalid("bundleBrotliBase64 is not valid base64", upgradePath, error); + } + let bundleText; + try { + bundleText = brotliDecompressSync(bundleBuffer, { maxOutputLength: MAX_BUNDLE_BYTES }); + } catch (error) { + throw invalid("upgrade bundle is not a bounded Brotli payload", upgradePath, error); + } + let targetTexts; + try { + targetTexts = JSON.parse(bundleText.toString("utf8")); + } catch (error) { + throw invalid("upgrade bundle is not valid JSON", upgradePath, error); + } + if (targetTexts === null || typeof targetTexts !== "object" || Array.isArray(targetTexts)) { + throw invalid("upgrade bundle is not a path-to-text object", upgradePath); + } + + const baseByPath = new Map(baseFiles.map((file) => [file.oldPath, file])); + for (const [rel, base] of baseByPath) { + if (generatedFinalHashes[rel] !== base.newBlob) { + throw invalid(`base patch target '${rel}' has no matching generated hash`, upgradePath); + } + if (acceptedLegacyFinalHashes[rel] === undefined) { + throw invalid(`base patch target '${rel}' has no matching accepted final hash`, upgradePath); + } + } + for (const rel of Object.keys(generatedFinalHashes)) { + if (!baseByPath.has(rel)) { + throw invalid(`generated hash '${rel}' is not in the base patch`, upgradePath); + } + } + for (const rel of Object.keys(acceptedLegacyFinalHashes)) { + if (!baseByPath.has(rel)) { + throw invalid(`accepted final hash '${rel}' is not in the base patch`, upgradePath); + } + } + for (const pair of raw.acceptedLegacyCompatPairs) { + if (!baseByPath.has(pair.path)) { + throw invalid(`compat pair '${pair.path}' is not in the base patch`, upgradePath); + } + } + for (const [rel, contents] of Object.entries(targetTexts)) { + if (!isSafeRelativePath(rel) || typeof contents !== "string") { + throw invalid(`unsafe or invalid upgrade target '${rel}'`, upgradePath); + } + if (!baseByPath.has(rel)) { + throw invalid(`upgrade target '${rel}' is not in the base patch`, upgradePath); + } + if (blobHash(contents) !== generatedFinalHashes[rel]) { + throw invalid(`upgrade target '${rel}' does not match its generated hash`, upgradePath); + } + } + for (const [rel] of baseByPath) { + const needsTarget = + compatOldHashes.has(rel) || generatedFinalHashes[rel] !== acceptedLegacyFinalHashes[rel]; + if (needsTarget && targetTexts[rel] === undefined) { + throw invalid(`upgrade bundle is missing the target for '${rel}'`, upgradePath); + } + if (!needsTarget && targetTexts[rel] !== undefined) { + throw invalid(`upgrade bundle has an unneeded target for '${rel}'`, upgradePath); + } + } + + return { acceptedLegacyFinalHashes, compatOldHashes, generatedFinalHashes, targetTexts }; +} + +function validateHashMap(map, name, upgradePath) { + if (map === null || typeof map !== "object" || Array.isArray(map)) { + throw invalid(`${name} must be a path-to-hash object`, upgradePath); + } + const keys = Object.keys(map); + if (keys.length === 0) throw invalid(`${name} must be non-empty`, upgradePath); + for (const rel of keys) { + if (!isSafeRelativePath(rel)) throw invalid(`unsafe ${name} path '${rel}'`, upgradePath); + if (!HEX40.test(map[rel])) + throw invalid(`invalid 40-hex ${name} hash for '${rel}'`, upgradePath); + } + return map; +} + +async function planPatch(threeRoot, files, upgrade) { const plans = []; for (const file of files) { const target = path.resolve(threeRoot, file.oldPath); @@ -227,7 +415,11 @@ async function planPatch(threeRoot, files, upgrades) { ); } const original = await readFile(target, "utf8"); - const recognised = upgradeKnownFile(original, file.oldPath, upgrades); + if (upgrade.json !== undefined) { + plans.push(planJsonFile(original, file, upgrade.json, target)); + continue; + } + const recognised = upgradeKnownFile(original, file.oldPath, upgrade.legacy); const lines = recognised.replaceAll("\r\n", "\n").split("\n"); const states = classifyFile(lines, file.hunks); const unknown = states.findIndex((state) => state === "unknown"); @@ -241,7 +433,7 @@ async function planPatch(threeRoot, files, upgrades) { : states.some((state) => state === "patched") ? "extended" : "stock"; - assertKnownExtension(status, recognised === original, file.oldPath, upgrades); + assertKnownExtension(status, recognised === original, file.oldPath, upgrade.legacy); if (status === "patched") { plans.push({ contents: recognised, @@ -250,15 +442,67 @@ async function planPatch(threeRoot, files, upgrades) { }); continue; } - plans.push({ - contents: applyFile(lines, file.hunks, states, target).join("\n"), - file: target, - status, - }); + const contents = applyFile(lines, file.hunks, states, target).join("\n"); + if (file.newBlob !== undefined && blobHash(contents) !== file.newBlob) { + throw invalid( + "patched output does not match the base patch index blob", + file.oldPath, + target, + ); + } + plans.push({ contents, file: target, status }); } return plans; } +/** + * Plan one file against the versioned JSON upgrade: an exact current target is idempotent, an + * exact known predecessor (legacy compat hash or the previously shipped final hash) is replaced + * wholesale by the bundle's current target, and an exact stock file takes the base patch hunks. + * Anything else on a registered path is refused before any file is written. + */ +function planJsonFile(original, file, json, target) { + const rel = file.oldPath; + const normalized = original.replaceAll("\r\n", "\n"); + const hash = blobHash(normalized); + if (hash === json.generatedFinalHashes[rel]) { + return { + contents: normalized, + file: target, + status: original === normalized ? "patched" : "extended", + }; + } + const legacy = json.compatOldHashes.get(rel); + if (legacy?.has(hash) === true || hash === json.acceptedLegacyFinalHashes[rel]) { + const contents = json.targetTexts[rel]; + if (contents === undefined) throw partialError(rel, undefined, target); + return { contents, file: target, status: "extended" }; + } + if (hash === file.oldBlob) { + const lines = normalized.split("\n"); + const states = classifyFile(lines, file.hunks); + const unknown = states.findIndex((state) => state === "unknown"); + if (unknown !== -1) throw partialError(rel, unknown, target); + const applied = applyFile(lines, file.hunks, states, target).join("\n"); + if (blobHash(applied) !== file.newBlob) { + throw invalid("patched output does not match the base patch index blob", rel, target); + } + return { + contents: applied, + file: target, + status: applied === original ? "patched" : "extended", + }; + } + throw partialError(rel, undefined, target); +} + +function partialError(rel, hunkNumber, target) { + const where = hunkNumber === undefined ? "" : `hunk ${String(hunkNumber + 1)} of `; + return new Error( + `TN_THREE_PATCH_PARTIAL: ${where}${rel} matches neither a supported predecessor nor the current patch in ${target}; refusing to modify an installation this package does not recognise.`, + ); +} + /** Registered migration files must not fall through to permissive hunk-only upgrades. */ function assertKnownExtension(status, unchanged, file, upgrades) { if ( diff --git a/packages/core/scripts/generate-three-patch.mjs b/packages/core/scripts/generate-three-patch.mjs new file mode 100644 index 000000000..17bdba6a6 --- /dev/null +++ b/packages/core/scripts/generate-three-patch.mjs @@ -0,0 +1,328 @@ +#!/usr/bin/env node +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { createReadStream, createWriteStream, existsSync } from "node:fs"; +import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { Readable } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import zlib from "node:zlib"; + +const execFileAsync = promisify(execFile); +const PIN_VERSION = "0.185.1"; +const PIN_COMMIT = "2431a09f46f34c560bc8e44b33be0e567723d5b9"; +const ARCHIVE_URL = `https://codeload.github.com/mrdoob/three.js/tar.gz/${PIN_COMMIT}`; +const BASE_PATCH_NAME = `three@${PIN_VERSION}.patch`; +const UPGRADE_PATCH_NAME = `three@${PIN_VERSION}-prd269-upgrade.patch`; +const MAX_BUFFER = 64 * 1024 * 1024; +const MAX_BUNDLE_BYTES = 8 * 1024 * 1024; +const HEX40 = /^[a-f0-9]{40}$/u; +const HEX64 = /^[a-f0-9]{64}$/u; + +const err = (message) => new Error(`TN_GENERATE_PATCH: ${message}`); + +export function blobHash(contents) { + return createHash("sha1") + .update(`blob ${Buffer.byteLength(contents)}\0`) + .update(contents) + .digest("hex"); +} + +async function fileSha256(file) { + const hash = createHash("sha256"); + for await (const chunk of createReadStream(file)) hash.update(chunk); + return hash.digest("hex"); +} + +function isSafeRelative(value, prefixes) { + if (typeof value !== "string" || value.length === 0) return false; + if (value.startsWith("/") || value.includes("\\")) return false; + if (value.split("/").some((part) => part === "" || part === "." || part === "..")) return false; + return prefixes.some((prefix) => value.startsWith(`${prefix}/`)); +} + +function validateCompatPairs(pairs) { + if (!Array.isArray(pairs) || pairs.length === 0) { + throw err("pin.json frozenCompatPairs must be a non-empty array."); + } + const seen = new Set(); + for (const pair of pairs) { + if (pair === null || typeof pair !== "object") + throw err("pin.json has an invalid compat pair."); + if (!isSafeRelative(pair.path, ["src", "build"])) + throw err(`unsafe compat path '${pair.path}'.`); + if (!HEX40.test(pair.oldHash ?? "") || !HEX40.test(pair.newHash ?? "")) { + throw err(`invalid compat hash for ${pair.path}.`); + } + const key = `${pair.path}\0${pair.oldHash}`; + if (seen.has(key)) throw err(`duplicate compat pair ${pair.path} ${pair.oldHash}.`); + seen.add(key); + } +} + +function validateFinalHashes(finals) { + if (finals === null || typeof finals !== "object" || Array.isArray(finals)) { + throw err("pin.json baseFinalHashes must be an object."); + } + const paths = Object.keys(finals); + if (paths.length === 0) throw err("pin.json baseFinalHashes must be non-empty."); + for (const rel of paths) { + if (!isSafeRelative(rel, ["src", "build"])) throw err(`unsafe final path '${rel}'.`); + if (!HEX40.test(finals[rel])) throw err(`invalid final hash for ${rel}.`); + } +} + +export function validatePin(pin) { + if (pin === null || typeof pin !== "object") throw err("pin.json must be an object."); + if (pin.version !== PIN_VERSION || pin.commit !== PIN_COMMIT) { + throw err(`unsupported pin ${pin.version} ${pin.commit}`); + } + for (const key of ["packageJsonSha256", "packageLockSha256", "archiveSha256"]) { + if (!HEX64.test(pin[key] ?? "")) throw err(`pin.json has an invalid ${key}.`); + } + validateCompatPairs(pin.frozenCompatPairs); + validateFinalHashes(pin.baseFinalHashes); + return pin; +} + +/** Every `diff --git`/`---`/`+++` path must be an identical, safe `src/` path. */ +export function parseSourceDeltaPaths(sourceDelta) { + const sections = sourceDelta + .split(/^(?=diff --git )/mu) + .filter((section) => section.trim().length > 0); + if (sections.length === 0) throw err("source delta is empty."); + const paths = []; + for (const section of sections) { + const header = /^diff --git a\/(\S+) b\/(\S+)/mu.exec(section); + if (header === null) throw err("source delta section has no diff header."); + const [, oldPath, newPath] = header; + const oldHeader = /^--- a\/(\S+)/mu.exec(section); + const newHeader = /^\+\+\+ b\/(\S+)/mu.exec(section); + if (oldPath !== newPath) + throw err(`source delta rename '${oldPath}' -> '${newPath}' is not allowed.`); + if (oldHeader?.[1] !== oldPath || newHeader?.[1] !== newPath) { + throw err(`source delta paths disagree for '${oldPath}'.`); + } + if (!isSafeRelative(oldPath, ["src"])) + throw err(`source delta path must be a safe src/ path: '${oldPath}'.`); + paths.push(oldPath); + } + return paths; +} + +export async function generateThreePatch(options = {}) { + const packageRoot = + options.packageRoot ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + const pinPath = path.join(packageRoot, "three-patch", "pin.json"); + const sourceDeltaPath = path.join(packageRoot, "three-patch", "source-delta.patch"); + if (!existsSync(pinPath) || !existsSync(sourceDeltaPath)) { + throw err("missing pin.json or source-delta.patch in three-patch directory."); + } + const pin = validatePin(JSON.parse(await readFile(pinPath, "utf8"))); + const sourceDelta = await readFile(sourceDeltaPath, "utf8"); + const approved = new Set(Object.keys(pin.baseFinalHashes)); + for (const rel of parseSourceDeltaPaths(sourceDelta)) { + if (!approved.has(rel)) throw err(`source delta path '${rel}' is not a pinned target.`); + } + + const scratch = await mkdtemp(path.join(os.tmpdir(), "tn-three-gen-")); + try { + const planner = options.planner ?? planFromUpstream; + const { basePatch, upgradePatch } = await planner({ + archivePath: options.archive, + pin, + scratch, + sourceDelta, + }); + return await handleArtifacts({ + basePatch, + upgradePatch, + packageRoot, + checkOnly: options.check ?? false, + }); + } finally { + await rm(scratch, { force: true, recursive: true }); + } +} + +async function planFromUpstream({ archivePath, pin, scratch, sourceDelta }) { + const archive = archivePath ?? path.join(scratch, "upstream.tar.gz"); + if (archivePath !== undefined && !existsSync(archivePath)) { + throw err(`archive not found at ${archivePath}.`); + } + if (archivePath === undefined) await downloadArchive(ARCHIVE_URL, archive); + if ((await fileSha256(archive)) !== pin.archiveSha256) { + throw err("upstream archive sha256 does not match the pinned hash."); + } + + const prefix = `three.js-${pin.commit}`; + const members = [ + `${prefix}/src`, + `${prefix}/build`, + `${prefix}/utils/build`, + `${prefix}/package.json`, + `${prefix}/package-lock.json`, + ]; + await execFileAsync("tar", ["-xzf", archive, "-C", scratch, "--", ...members], { + maxBuffer: MAX_BUFFER, + }); + const workDir = path.join(scratch, prefix); + if ((await fileSha256(path.join(workDir, "package.json"))) !== pin.packageJsonSha256) { + throw err("upstream package.json sha256 does not match the pin."); + } + if ((await fileSha256(path.join(workDir, "package-lock.json"))) !== pin.packageLockSha256) { + throw err("upstream package-lock.json sha256 does not match the pin."); + } + + const stockDir = path.join(scratch, "stock-snapshot"); + await cp(path.join(workDir, "src"), path.join(stockDir, "src"), { recursive: true }); + await cp(path.join(workDir, "build"), path.join(stockDir, "build"), { recursive: true }); + + const deltaFile = path.join(scratch, "delta.patch"); + await writeFile(deltaFile, sourceDelta, "utf8"); + await execFileAsync("patch", ["-p1", "--batch", "--silent", "-i", deltaFile], { cwd: workDir }); + + await execFileAsync("npm", ["ci", "--ignore-scripts", "--no-audit", "--no-fund"], { + cwd: workDir, + maxBuffer: MAX_BUFFER, + }); + await execFileAsync("npm", ["run", "build"], { cwd: workDir, maxBuffer: MAX_BUFFER }); + + return assemblePatches({ patchedDir: workDir, pin, stockDir }); +} + +async function downloadArchive(url, dest) { + const response = await fetch(url, { signal: AbortSignal.timeout(120_000) }); + if (!response.ok || response.body === null) { + throw err(`could not fetch the upstream archive: HTTP ${response.status}.`); + } + await pipeline(Readable.fromWeb(response.body), createWriteStream(dest)); +} + +/** Emit the base patch and the upgrade bundle for exactly the pinned target paths. */ +export async function assemblePatches({ stockDir, patchedDir, pin }) { + validatePin(pin); + const sections = []; + const targetTexts = {}; + const generatedFinalHashes = {}; + const migrationPaths = new Set(pin.frozenCompatPairs.map((pair) => pair.path)); + for (const rel of Object.keys(pin.baseFinalHashes).sort()) { + const oldFile = path.join(stockDir, rel); + const newFile = path.join(patchedDir, rel); + if (!existsSync(oldFile) || !existsSync(newFile)) throw err(`missing scoped target ${rel}.`); + const oldBytes = await readFile(oldFile); + const newBytes = await readFile(newFile); + generatedFinalHashes[rel] = blobHash(newBytes); + // Carry the current target only where a consumer can hold a different copy of it: an older + // compat state, or a target this build changed from the previously shipped one. An unchanged + // target is recognised by hash and never needs its full text duplicated in the bundle. + if (migrationPaths.has(rel) || generatedFinalHashes[rel] !== pin.baseFinalHashes[rel]) { + targetTexts[rel] = newBytes.toString("utf8"); + } + const diff = await diffScopedFile(oldFile, newFile, rel); + if (diff.length > 0) sections.push(diff); + } + + const targetJson = Buffer.from(JSON.stringify(targetTexts)); + if (targetJson.length > MAX_BUNDLE_BYTES) throw err("upgrade target JSON exceeds the 8MiB cap."); + const bundle = zlib.brotliCompressSync(targetJson, { + params: { + [zlib.constants.BROTLI_PARAM_QUALITY]: 6, + [zlib.constants.BROTLI_PARAM_LGWIN]: 24, + }, + }); + if (bundle.length > MAX_BUNDLE_BYTES) throw err("upgrade brotli bundle exceeds the 8MiB cap."); + + const upgradeJson = { + version: 2, + generator: "scripts/generate-three-patch.mjs", + pin: { commit: pin.commit, version: pin.version }, + acceptedLegacyCompatPairs: pin.frozenCompatPairs, + acceptedLegacyFinalHashes: pin.baseFinalHashes, + generatedFinalHashes, + bundleBrotliBase64: bundle.toString("base64"), + }; + return { + basePatch: sections.join(""), + upgradePatch: `${JSON.stringify(upgradeJson, null, 2)}\n`, + }; +} + +async function diffScopedFile(oldFile, newFile, rel) { + let raw = ""; + try { + const result = await execFileAsync( + "git", + [ + "diff", + "--no-index", + "--no-color", + "--no-ext-diff", + "--unified=3", + "--diff-algorithm=myers", + "--", + oldFile, + newFile, + ], + { maxBuffer: MAX_BUFFER }, + ); + raw = result.stdout; + } catch (error) { + if (typeof error.stdout === "string" && error.stdout.length > 0) raw = error.stdout; + else throw error; + } + if (raw.length === 0) return ""; + const oldBlob = blobHash(await readFile(oldFile)); + const newBlob = blobHash(await readFile(newFile)); + return raw + .split("\n") + .map((line) => { + if (line.startsWith("diff --git ")) return `diff --git a/${rel} b/${rel}`; + if (line.startsWith("index ")) return `index ${oldBlob}..${newBlob} 100644`; + if (line.startsWith("--- ")) return `--- a/${rel}`; + if (line.startsWith("+++ ")) return `+++ b/${rel}`; + return line; + }) + .join("\n"); +} + +async function handleArtifacts({ basePatch, upgradePatch, packageRoot, checkOnly }) { + const baseDest = path.join(packageRoot, "patches", BASE_PATCH_NAME); + const upgradeDest = path.join(packageRoot, "patches", UPGRADE_PATCH_NAME); + if (checkOnly) { + const currentBase = existsSync(baseDest) ? await readFile(baseDest, "utf8") : ""; + const currentUpgrade = existsSync(upgradeDest) ? await readFile(upgradeDest, "utf8") : ""; + if (currentBase !== basePatch || currentUpgrade !== upgradePatch) { + throw err("check failed, generated patch drifts from the committed patch."); + } + return { status: "matched" }; + } + await mkdir(path.dirname(baseDest), { recursive: true }); + await writeFile(baseDest, basePatch, "utf8"); + await writeFile(upgradeDest, upgradePatch, "utf8"); + return { status: "written" }; +} + +function parseCli(argv) { + const options = { archive: undefined, check: false }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--check") options.check = true; + else if (arg === "--archive") { + const value = argv[index + 1]; + if (value === undefined || value.startsWith("--")) throw err("--archive requires a path."); + options.archive = value; + index += 1; + } else throw err(`unknown argument '${arg}'.`); + } + return options; +} + +const entry = process.argv[1]; +if (entry !== undefined && path.resolve(entry) === path.resolve(fileURLToPath(import.meta.url))) { + const result = await generateThreePatch(parseCli(process.argv.slice(2))); + process.stdout.write(`generate-three-patch: ${result.status}\n`); +} diff --git a/packages/core/three-patch/pin.json b/packages/core/three-patch/pin.json new file mode 100644 index 000000000..3627d1cd9 --- /dev/null +++ b/packages/core/three-patch/pin.json @@ -0,0 +1,197 @@ +{ + "version": "0.185.1", + "release": "r185", + "commit": "2431a09f46f34c560bc8e44b33be0e567723d5b9", + "packageJsonSha256": "1d5c438acdc1fe5a52fb8ede2e417afd91fec91d22275c40f7b739591b24e4df", + "packageLockSha256": "ad871796b84315cc243f63d6885e2fe397c0db3a40de3b2fcca66bd23afbf9da", + "archiveSha256": "904bfa6979764988420b09720e21bbdadb5eb4077812155789830360af9ead4c", + "frozenCompatPairs": [ + { + "path": "build/three.webgpu.js", + "oldHash": "148869ee7f18c62570ac1eafb7df5775e5a89581", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "5c5ef3c06885c724c1c2288295e7e68483ee1f6e", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "7aa62adb9e78ba8f0a67709d3141bf93c67419cc", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "bdffb9b4069f7beafa85913b155e92d3e83f12d0", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "efd0e16719cc7bbef1fbc18c241948e25eb85881", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "1245f0da9bb6a0f731b8c75a64490d7c91238700", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "1b3a6e96291af0077f7f247040e657cf4aa0cb33", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "3d47c1151d563f1e0ae2bb4b92edd057fe0df2b3", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "81c24b2e05e5047e1a02410a2056949d39b1a05c", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "b8a7b2fd0a2e95911e7e12fd5674592107f012de", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "src/nodes/accessors/Instance.js", + "oldHash": "ed00e75ba4fd7b49343cfada26eacdef8940a4f4", + "newHash": "18e2b7943393fd07b7989b1e3a5231a61ca24187" + }, + { + "path": "src/nodes/accessors/Skinning.js", + "oldHash": "aaeea633a8ece291be5faf868e312867ddad01b6", + "newHash": "76303e765bc29853cfd7f139dd3ec76545b70798" + }, + { + "path": "src/renderers/common/Renderer.js", + "oldHash": "89b33efb80ca1dcb9db931071dfda62ffdba3526", + "newHash": "b1293bfbe2ccf0583711813c625ddd5c5ba78620" + }, + { + "path": "src/renderers/webgpu/WebGPUBackend.js", + "oldHash": "69b8b67fdeec5be24de34d8382beda9020642f77", + "newHash": "5636b40e6708a490e926e5f590675834d23bf326" + }, + { + "path": "src/renderers/webgpu/utils/WebGPUAttributeUtils.js", + "oldHash": "83bbe189915c8450c4e14108e6a3f4ed98c4b016", + "newHash": "16bd72d0c0f1d693481efc98140a0f1b73905149" + }, + { + "path": "src/renderers/webgpu/utils/WebGPUTextureUtils.js", + "oldHash": "ff8ee2dbace2816105aa7152891c04f2333f33e8", + "newHash": "57c1eb62f1e0de063f22e08ec879e31d2a3240b6" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "9166d6f2dd1ac1505fa27b625f1976dff6b8ebc1", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "9e7c7f74be1191b761711bd70ee59cc033e586cb", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "src/nodes/accessors/TextureNode.js", + "oldHash": "f886ed21397ab39d6fdbc58e5ac57729f1871f8a", + "newHash": "1a138b1a055d0867e7686531930d4d557e1a20df" + }, + { + "path": "src/nodes/accessors/TextureSizeNode.js", + "oldHash": "d4668281657ef8b1acad761e0d86a0716909eee4", + "newHash": "43eccaa01bcb12ea6e02c94232d5ee76bcac0d01" + }, + { + "path": "src/nodes/display/PassNode.js", + "oldHash": "cc827a55cb82a6325296dcc5c55a4246f4f70aed", + "newHash": "3cdf66cfe0760066bc062bf7ce3f3a8000eb2b0d" + }, + { + "path": "src/renderers/common/Renderer.js", + "oldHash": "51590d5a23a93365e5cd9fb0360678750270d5bf", + "newHash": "b1293bfbe2ccf0583711813c625ddd5c5ba78620" + }, + { + "path": "src/renderers/webgpu/nodes/WGSLNodeBuilder.js", + "oldHash": "2acc832a628f43adac49614dad8e593ef058c398", + "newHash": "1cbc9b3e02c9832d69a8aadb3a6003392b443c8d" + }, + { + "path": "src/renderers/webgpu/utils/WebGPUBindingUtils.js", + "oldHash": "a5da7ca09f746c0b2859ef173df663ccdc851ebc", + "newHash": "c479d0a88794b01bca531e7cbfb780cf93fc4721" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "941ec68ef0b8174a633054043edb94d9d3d58f5e", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "57f69c17352b936a9eca118a9801805c95c401b6", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "build/three.webgpu.js", + "oldHash": "ef853fd6f7382a8c7ff66cda43471344aeb20770", + "newHash": "a308a892c59283b3b9a71477b3b02d64db40becd" + }, + { + "path": "build/three.webgpu.nodes.js", + "oldHash": "e7956e565579aa6ec550ca001336d5a0d76c652a", + "newHash": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb" + }, + { + "path": "src/renderers/common/Renderer.js", + "oldHash": "730ce0ccd0f56d70c4a7abe7c514c5077d544d1e", + "newHash": "b1293bfbe2ccf0583711813c625ddd5c5ba78620" + }, + { + "path": "src/renderers/webgpu/WebGPUBackend.js", + "oldHash": "927e3a1ad5a4665d77238ffbab4c57a603b86828", + "newHash": "5636b40e6708a490e926e5f590675834d23bf326" + }, + { + "path": "src/renderers/webgpu/utils/WebGPUTextureUtils.js", + "oldHash": "c46f6ab0e06d93f124c9fc62ece8859529461d5e", + "newHash": "57c1eb62f1e0de063f22e08ec879e31d2a3240b6" + } + ], + "baseFinalHashes": { + "build/three.cjs": "b0f917241f1f2cd25baa21dec20edda32176ef6a", + "build/three.core.js": "868a634b4294be5f32664912a50813d55d6dd6e7", + "build/three.webgpu.js": "a308a892c59283b3b9a71477b3b02d64db40becd", + "build/three.webgpu.nodes.js": "95669d28b54250a3857dcb7d1b1b0e30dd19e0cb", + "src/materials/nodes/manager/NodeMaterialObserver.js": "7e2cb9926878b43aa2b8b3a146e3427711be7210", + "src/nodes/accessors/Batch.js": "2cf4bb6039853d5c922e89041419250b243694ef", + "src/nodes/accessors/Instance.js": "18e2b7943393fd07b7989b1e3a5231a61ca24187", + "src/nodes/accessors/Skinning.js": "76303e765bc29853cfd7f139dd3ec76545b70798", + "src/nodes/accessors/TextureNode.js": "1a138b1a055d0867e7686531930d4d557e1a20df", + "src/nodes/accessors/TextureSizeNode.js": "43eccaa01bcb12ea6e02c94232d5ee76bcac0d01", + "src/nodes/accessors/VelocityNode.js": "c48002abbce20ee8b29908cf314d7b03afe0db03", + "src/nodes/core/NodeBuilder.js": "1990da8fe8f1f29a7b53030756dfd966d8de3cb6", + "src/nodes/display/PassNode.js": "3cdf66cfe0760066bc062bf7ce3f3a8000eb2b0d", + "src/nodes/lighting/PointShadowNode.js": "f9019a72890a30fded9f7ce76982722c2f7d6a3f", + "src/nodes/lighting/ShadowFilterNode.js": "fe8b50c8f76d0b6ce3d2fa1452751b2469a821e4", + "src/objects/BatchedMesh.js": "35cc0ffe83a69ea9fa5fca6dec2d820ca193f88d", + "src/renderers/common/Bindings.js": "a3cec4ed844ab88f870b46714c56c941e0cc7a8e", + "src/renderers/common/Geometries.js": "979da2ffc4d0dcda6fdd72747a7e7125e3f8a3a3", + "src/renderers/common/RenderContexts.js": "7319e25e280452b69c6f0c4c2f8c93c955ff9be3", + "src/renderers/common/RenderObject.js": "a3dcba2f1fb0df19f3ddb3039a97ae408e2938b3", + "src/renderers/common/RenderObjects.js": "72569d0339314d26eb7a35ff2a8462a803c852a6", + "src/renderers/common/Renderer.js": "b1293bfbe2ccf0583711813c625ddd5c5ba78620", + "src/renderers/common/Textures.js": "5b305173fa4a2814ec74563d2ae5cdc70e7505e1", + "src/renderers/common/nodes/NodeManager.js": "033128c396c2cad5c5d1d362f2d261e9adbd6bdc", + "src/renderers/webgpu/WebGPUBackend.js": "5636b40e6708a490e926e5f590675834d23bf326", + "src/renderers/webgpu/nodes/WGSLNodeBuilder.js": "1cbc9b3e02c9832d69a8aadb3a6003392b443c8d", + "src/renderers/webgpu/utils/WebGPUAttributeUtils.js": "16bd72d0c0f1d693481efc98140a0f1b73905149", + "src/renderers/webgpu/utils/WebGPUBindingUtils.js": "c479d0a88794b01bca531e7cbfb780cf93fc4721", + "src/renderers/webgpu/utils/WebGPUPipelineUtils.js": "3480fea1e86f1f237cb6ee2ea87b5826d4fc404d", + "src/renderers/webgpu/utils/WebGPUTextureUtils.js": "57c1eb62f1e0de063f22e08ec879e31d2a3240b6" + } +} diff --git a/packages/core/three-patch/source-delta.patch b/packages/core/three-patch/source-delta.patch new file mode 100644 index 000000000..5a9406dc0 --- /dev/null +++ b/packages/core/three-patch/source-delta.patch @@ -0,0 +1,2157 @@ +diff --git a/src/materials/nodes/manager/NodeMaterialObserver.js b/src/materials/nodes/manager/NodeMaterialObserver.js +index 1f4c2546c937c70f013fec3a0580b66ac501ebbb..7e2cb9926878b43aa2b8b3a146e3427711be7210 100644 +--- a/src/materials/nodes/manager/NodeMaterialObserver.js ++++ b/src/materials/nodes/manager/NodeMaterialObserver.js +@@ -133,6 +133,15 @@ class NodeMaterialObserver { + */ + this.renderId = 0; + ++ /** ++ * Whether the current render produces motion vectors. Objects keep refreshing ++ * fully while this is the case. ++ * ++ * @type {boolean} ++ * @default false ++ */ ++ this.renderingVelocity = false; ++ + } + + /** +@@ -232,6 +241,88 @@ class NodeMaterialObserver { + + } + ++ /** ++ * Returns `true` if the given render object is a static object whose per-object state - world ++ * matrix, material, geometry and the scene environment - did not change since the last ++ * refresh. A settled object is still drawn every frame and its FRAME and RENDER nodes still ++ * have to be updated; what it may skip is the work whose inputs it froze: its OBJECT node ++ * updates, its object uniform group upload and its per-draw attribute scan. ++ * ++ * @param {RenderObject} renderObject - The render object. ++ * @return {boolean} Whether the given render object is settled or not. ++ */ ++ isSettled( renderObject ) { ++ ++ const data = this.renderObjects.get( renderObject ); ++ ++ return data !== undefined && data.settledState === true; ++ ++ } ++ ++ /** ++ * Recomputes {@link NodeMaterialObserver#isSettled} for the given render object, once per draw, ++ * so every step of that draw reads the same answer. ++ * ++ * @param {RenderObject} renderObject - The render object. ++ * @return {boolean} Whether the given render object is settled or not. ++ */ ++ updateSettled( renderObject ) { ++ ++ const { object, material, geometry, scene } = renderObject; ++ const data = this.getRenderObjectData( renderObject ); ++ ++ if ( object.static !== true || this.renderingVelocity === true ) { ++ ++ data.settledState = false; ++ ++ return false; ++ ++ } ++ ++ let settled = data.settled; ++ ++ if ( settled === undefined ) { ++ ++ data.settled = settled = { ++ matrix: object.matrixWorld.clone(), ++ environmentRotation: scene.environmentRotation.clone(), ++ environmentIntensity: scene.environmentIntensity, ++ materialVersion: material.version, ++ geometryVersion: geometry.version ++ }; ++ ++ data.settledState = false; ++ ++ return false; ++ ++ } ++ ++ if ( settled.matrix.equals( object.matrixWorld ) !== true || ++ settled.environmentRotation.equals( scene.environmentRotation ) !== true || ++ settled.environmentIntensity !== scene.environmentIntensity || ++ settled.materialVersion !== material.version || ++ settled.geometryVersion !== geometry.version ) { ++ ++ // re-arm with the current state, so the object settles again once it stops changing ++ ++ settled.matrix.copy( object.matrixWorld ); ++ settled.environmentRotation.copy( scene.environmentRotation ); ++ settled.environmentIntensity = scene.environmentIntensity; ++ settled.materialVersion = material.version; ++ settled.geometryVersion = geometry.version; ++ ++ data.settledState = false; ++ ++ return false; ++ ++ } ++ ++ data.settledState = true; ++ ++ return true; ++ ++ } ++ + /** + * Returns an attribute data structure holding the attributes versions for + * monitoring. +@@ -298,7 +389,6 @@ class NodeMaterialObserver { + + data = { + _renderId: - 1, +- _equal: false, + + attributes: this.getAttributesData( geometry.attributes ), + indexId: geometry.index ? geometry.index.id : null, +@@ -327,7 +417,7 @@ class NodeMaterialObserver { + + if ( data === undefined ) { + +- data = { _renderId: - 1, _equal: false }; ++ data = { _renderId: - 1 }; + + for ( const property of this.refreshUniforms ) { + +@@ -411,7 +501,6 @@ class NodeMaterialObserver { + + value.copy( mtlValue ); + +- materialData._equal = false; + return false; + + } +@@ -423,7 +512,6 @@ class NodeMaterialObserver { + value.id = mtlValue.id; + value.version = mtlValue.version; + +- materialData._equal = false; + return false; + + } +@@ -432,7 +520,6 @@ class NodeMaterialObserver { + + materialData[ property ] = mtlValue; + +- materialData._equal = false; + return false; + + } +@@ -448,18 +535,12 @@ class NodeMaterialObserver { + renderObjectData.bufferWidth = width; + renderObjectData.bufferHeight = height; + +- materialData._equal = false; + return false; + + } + + } + +- materialData._equal = true; +- +- } else { +- +- if ( materialData._equal === false ) return false; + + } + +@@ -502,7 +583,6 @@ class NodeMaterialObserver { + // attribute was removed + delete storedAttributes[ name ]; + +- geometryData._equal = false; + return false; + + } +@@ -515,7 +595,6 @@ class NodeMaterialObserver { + storedAttributeData.id = id; + storedAttributeData.version = version; + +- geometryData._equal = false; + return false; + + } +@@ -526,7 +605,6 @@ class NodeMaterialObserver { + + geometryData.attributes = this.getAttributesData( attributes ); + +- geometryData._equal = false; + return false; + + } +@@ -544,7 +622,6 @@ class NodeMaterialObserver { + geometryData.indexId = currentIndexId; + geometryData.indexVersion = currentIndexVersion; + +- geometryData._equal = false; + return false; + + } +@@ -556,16 +633,10 @@ class NodeMaterialObserver { + geometryData.drawRange.start = geometry.drawRange.start; + geometryData.drawRange.count = geometry.drawRange.count; + +- geometryData._equal = false; + return false; + + } + +- geometryData._equal = true; +- +- } else { +- +- if ( geometryData._equal === false ) return false; + + } + +@@ -716,7 +787,14 @@ class NodeMaterialObserver { + */ + needsRefresh( renderObject, nodeFrame ) { + +- if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.needsVelocity( nodeFrame.renderer ) ) ++ this.renderingVelocity = this.needsVelocity( nodeFrame.renderer ); ++ ++ // Answered for the whole draw, so updateBefore, the geometry scan, the node updates and ++ // the object uniform upload of this object all skip or all refresh. ++ ++ this.updateSettled( renderObject ); ++ ++ if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.renderingVelocity ) + return true; + + const { renderId } = nodeFrame; +@@ -729,10 +807,14 @@ class NodeMaterialObserver { + + } + +- const isStatic = renderObject.object.static === true; + const isBundle = renderObject.bundle !== null && renderObject.bundle.static === true && this.getRenderObjectData( renderObject ).version === renderObject.bundle.version; + +- if ( isStatic || isBundle ) ++ // `object.static` is not the question - it is a switch, and a brand new object has it set ++ // before it has been drawn once, so answering from it skips the draw that creates this ++ // object's bind groups and leaves the pipeline reading a layout that was never built. A ++ // static object that moved, or whose material or geometry was edited, has to refresh like ++ // any other; only a settled one may skip, and nothing is settled before its first refresh. ++ if ( this.isSettled( renderObject ) === true || isBundle ) + return false; + + const lightsData = this.getLights( renderObject.lightsNode, renderId ); +diff --git a/src/nodes/accessors/Batch.js b/src/nodes/accessors/Batch.js +index ab62e98dadab2539d7e1d663fd42267bbc8ebcaa..2cf4bb6039853d5c922e89041419250b243694ef 100644 +--- a/src/nodes/accessors/Batch.js ++++ b/src/nodes/accessors/Batch.js +@@ -1,6 +1,6 @@ + + import { normalLocal } from './Normal.js'; +-import { positionLocal } from './Position.js'; ++import { positionLocal, positionPrevious } from './Position.js'; + import { vec3, mat3, mat4, int, ivec2, float, Fn } from '../tsl/TSLBase.js'; + import { textureLoad } from './TextureNode.js'; + import { textureSize } from './TextureSizeNode.js'; +@@ -64,6 +64,7 @@ export const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => { + const indirectId = getIndirectIndex( batchMesh._indirectTexture, int( batchingIdNode ) ); + + const matricesTexture = batchMesh._matricesTexture; ++ const previousMatricesTexture = batchMesh._previousMatricesTexture ?? matricesTexture; + + const size = int( textureSize( textureLoad( matricesTexture ), 0 ).x ).toConst(); + const j = float( indirectId ).mul( 4 ).toInt().toConst(); +@@ -90,6 +91,19 @@ export const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => { + const bm = mat3( batchingMatrix ); + + positionLocal.assign( batchingMatrix.mul( positionLocal ) ); ++ if ( builder.needsPreviousData() ) { ++ ++ const previousBatchingMatrix = mat4( ++ textureLoad( previousMatricesTexture, ivec2( x, y ) ), ++ textureLoad( previousMatricesTexture, ivec2( x.add( 1 ), y ) ), ++ textureLoad( previousMatricesTexture, ivec2( x.add( 2 ), y ) ), ++ textureLoad( previousMatricesTexture, ivec2( x.add( 3 ), y ) ) ++ ); ++ ++ positionPrevious.assign( previousBatchingMatrix.mul( positionPrevious ).xyz ); ++ ++ } ++ + + const transformedNormal = normalLocal.div( vec3( bm[ 0 ].dot( bm[ 0 ] ), bm[ 1 ].dot( bm[ 1 ] ), bm[ 2 ].dot( bm[ 2 ] ) ) ); + +@@ -104,5 +118,3 @@ export const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => { + } + + }, 'void' ); +- +- +diff --git a/src/nodes/accessors/Instance.js b/src/nodes/accessors/Instance.js +index b89834b9166c75fd946ade24db265bf8c367d244..18e2b7943393fd07b7989b1e3a5231a61ca24187 100644 +--- a/src/nodes/accessors/Instance.js ++++ b/src/nodes/accessors/Instance.js +@@ -1,6 +1,6 @@ + + import { vec3, mat4, Fn } from '../tsl/TSLBase.js'; +-import { OnFrameUpdate, OnObjectUpdate } from '../utils/EventNode.js'; ++import { OnBeforeFrameUpdate, OnBeforeObjectUpdate, OnObjectUpdate } from '../utils/EventNode.js'; + import { normalLocal, transformNormal } from './Normal.js'; + import { positionLocal, positionPrevious } from './Position.js'; + import { varyingProperty } from '../core/PropertyNode.js'; +@@ -40,7 +40,8 @@ function createInstanceMatrixNode( builder, instanceMatrix ) { + + const uniformBufferSize = matrixCount * 16 * 4; + +- if ( uniformBufferSize <= builder.getUniformBufferLimit() ) { ++ // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change ++ if ( false ) { + + instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', matrixCount ).element( instanceIndex ); + +@@ -134,7 +135,8 @@ export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder + + const uniformBufferSize = Math.max( matrices.count, 1 ) * 16 * 4; + +- if ( uniformBufferSize > builder.getUniformBufferLimit() ) { ++ // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change ++ if ( true ) { + + interleavedMatrix = _matrixBuffers.get( matrices ); + +@@ -175,7 +177,7 @@ export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder + // Synchronization of dynamic buffer updates per frame + if ( interleavedMatrix !== null || interleavedColor !== null ) { + +- OnFrameUpdate( () => { ++ OnBeforeFrameUpdate( () => { + + if ( interleavedMatrix !== null ) { + +@@ -216,11 +218,28 @@ export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder + + const instancedMesh = builder.object; + +- OnObjectUpdate( ( { object } ) => { ++ const updatePrevious = ( object, array ) => { ++ ++ const { previousInstanceMatrix } = _previousInstanceMatrices.get( object ); ++ previousInstanceMatrix.array.set( array ); ++ previousInstanceMatrix.needsUpdate = true; ++ const interleaved = _matrixBuffers.get( previousInstanceMatrix ); ++ if ( interleaved !== undefined ) interleaved.needsUpdate = true; ++ ++ }; ++ ++ // Scheduled history must reach the GPU in this draw, before the geometry uploads. ++ OnBeforeObjectUpdate( ( { object } ) => { + +- const previousInstanceData = _previousInstanceMatrices.get( object ); ++ const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ]; ++ if ( scheduledPrevious !== undefined ) updatePrevious( object, scheduledPrevious ); ++ ++ } ); ++ ++ // Without a scheduler, retain Three's draw-time snapshot for the next draw. ++ OnObjectUpdate( ( { object } ) => { + +- previousInstanceData.previousInstanceMatrix.array.set( matrices.array ); ++ if ( object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ] === undefined ) updatePrevious( object, matrices.array ); + + } ); + +diff --git a/src/nodes/accessors/Skinning.js b/src/nodes/accessors/Skinning.js +index 2356aed8656dceb1ea74b73ea2e739371a960484..76303e765bc29853cfd7f139dd3ec76545b70798 100644 +--- a/src/nodes/accessors/Skinning.js ++++ b/src/nodes/accessors/Skinning.js +@@ -117,7 +117,15 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve + + } + +- return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); ++ // The previous bones and world matrix must use the same frame's bind inverse. ++ const previousBindMatrixInverse = uniform( skinnedMesh.bindMatrixInverse, 'mat4' ).onObjectUpdate( ( { object } ) => { ++ ++ const mesh = object?.isSkinnedMesh === true ? object : skinnedMesh; ++ return mesh[ Symbol.for( 'threenative.velocity.previousBindMatrixInverse' ) ] ?? mesh.bindMatrixInverse; ++ ++ } ); ++ ++ return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, previousBindMatrixInverse, skinIndexNode, skinWeightNode ); + + } + +@@ -149,7 +157,8 @@ export const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => { + + if ( skeletonData !== undefined ) { + +- skeletonData.previousBoneMatrices.set( skeleton.boneMatrices ); ++ const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousBoneMatrices' ) ]; ++ skeletonData.previousBoneMatrices.set( scheduledPrevious ?? skeleton.boneMatrices ); + + } + +@@ -216,7 +225,8 @@ export const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = n + + if ( state !== undefined ) { + +- state.previousBoneMatrices.set( skeleton.boneMatrices ); ++ const scheduledPrevious = skinnedMesh[ Symbol.for( 'threenative.velocity.previousBoneMatrices' ) ]; ++ state.previousBoneMatrices.set( scheduledPrevious ?? skeleton.boneMatrices ); + + } + +diff --git a/src/nodes/accessors/TextureNode.js b/src/nodes/accessors/TextureNode.js +index f886ed21397ab39d6fdbc58e5ac57729f1871f8a..1a138b1a055d0867e7686531930d4d557e1a20df 100644 +--- a/src/nodes/accessors/TextureNode.js ++++ b/src/nodes/accessors/TextureNode.js +@@ -524,7 +524,7 @@ class TextureNode extends UniformNode { + + } else if ( this.sampler === false ) { + +- snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); ++ snippet = builder.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet, this.isRawTextureNode === true ); + + } else if ( levelSnippet ) { + +diff --git a/src/nodes/accessors/TextureSizeNode.js b/src/nodes/accessors/TextureSizeNode.js +index d4668281657ef8b1acad761e0d86a0716909eee4..43eccaa01bcb12ea6e02c94232d5ee76bcac0d01 100644 +--- a/src/nodes/accessors/TextureSizeNode.js ++++ b/src/nodes/accessors/TextureSizeNode.js +@@ -57,7 +57,8 @@ class TextureSizeNode extends Node { + const textureProperty = this.textureNode.build( builder, 'property' ); + const level = this.levelNode === null ? '0' : this.levelNode.build( builder, 'int' ); + +- return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }, ${ level } )`, this.getNodeType( builder ), output ); ++ const mip = this.textureNode.isRawTextureNode === true ? '' : `, ${ level }`; ++ return builder.format( `${ builder.getMethod( 'textureDimensions' ) }( ${ textureProperty }${ mip } )`, this.getNodeType( builder ), output ); + + } + +diff --git a/src/nodes/accessors/VelocityNode.js b/src/nodes/accessors/VelocityNode.js +index 373548b0b587e7da3ef0993bfe316af420e7f66c..c48002abbce20ee8b29908cf314d7b03afe0db03 100644 +--- a/src/nodes/accessors/VelocityNode.js ++++ b/src/nodes/accessors/VelocityNode.js +@@ -106,7 +106,8 @@ class VelocityNode extends TempNode { + + const previousModelMatrix = getPreviousMatrix( object ); + +- this.previousModelWorldMatrix.value.copy( previousModelMatrix ); ++ const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousWorldMatrix' ) ]; ++ this.previousModelWorldMatrix.value.copy( scheduledPrevious ?? previousModelMatrix ); + + // + +diff --git a/src/nodes/core/NodeBuilder.js b/src/nodes/core/NodeBuilder.js +index be00b81ef36e1af83b0066603e616f8f1f81a115..1990da8fe8f1f29a7b53030756dfd966d8de3cb6 100644 +--- a/src/nodes/core/NodeBuilder.js ++++ b/src/nodes/core/NodeBuilder.js +@@ -3450,7 +3450,7 @@ class NodeBuilder { + + const mrt = this.renderer.getMRT(); + +- return ( mrt && mrt.has( 'velocity' ) ) || getDataFromObject( this.object ).useVelocity === true; ++ return ( mrt && mrt.has( 'velocity' ) ) || getDataFromObject( this.object ).useVelocity === true || this.object.userData?.useVelocity === true; + + } + +diff --git a/src/nodes/display/PassNode.js b/src/nodes/display/PassNode.js +index cc827a55cb82a6325296dcc5c55a4246f4f70aed..3cdf66cfe0760066bc062bf7ce3f3a8000eb2b0d 100644 +--- a/src/nodes/display/PassNode.js ++++ b/src/nodes/display/PassNode.js +@@ -95,7 +95,7 @@ class PassMultipleTextureNode extends PassTextureNode { + * @param {string} textureName - The output texture name. + * @param {boolean} [previousTexture=false] - Whether previous frame data should be used or not. + */ +- constructor( passNode, textureName, previousTexture = false ) { ++ constructor( passNode, textureName, previousTexture = false, rawTexture = false ) { + + // null is passed to the super call since this class does not + // use an external texture for rendering pass data into. Instead +@@ -116,6 +116,8 @@ class PassMultipleTextureNode extends PassTextureNode { + * @type {boolean} + */ + this.previousTexture = previousTexture; ++ this.isRawTextureNode = rawTexture; ++ if ( rawTexture ) this.setSampler( false ); + + /** + * This flag can be used for type testing. +@@ -137,17 +139,33 @@ class PassMultipleTextureNode extends PassTextureNode { + + } + ++ getUniformHash( builder ) { ++ ++ const hash = super.getUniformHash( builder ); ++ return this.isRawTextureNode ? `${ this.passNode.uuid }:${ this.textureName }:raw4:${ hash }` : hash; ++ ++ } ++ + setup( builder ) { + + this.updateTexture(); + ++ if ( this.isRawTextureNode ) { ++ ++ if ( this.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); ++ if ( this.passNode.renderTarget.samples !== 4 ) throw new Error( 'Raw pass attachments require four-sample multisampling.' ); ++ if ( this.sampler || this.biasNode || this.compareNode || this.gradNode || this.gatherNode || this.depthNode ) throw new Error( 'Raw pass attachments support integer loads only.' ); ++ if ( builder.renderer.backend.isWebGPUBackend !== true ) throw new Error( 'Raw pass attachments require WebGPU.' ); ++ ++ } ++ + return super.setup( builder ); + + } + + clone() { + +- const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture ); ++ const newNode = new this.constructor( this.passNode, this.textureName, this.previousTexture, this.isRawTextureNode ); + newNode.uvNode = this.uvNode; + newNode.levelNode = this.levelNode; + newNode.biasNode = this.biasNode; +@@ -328,6 +346,8 @@ class PassNode extends TempNode { + * @type {Object} + */ + this._textureNodes = {}; ++ this._rawTextureNodes = {}; ++ this._rawTexturesDisposed = false; + + /** + * A dictionary holding the internal depth nodes. +@@ -634,6 +654,7 @@ class PassNode extends TempNode { + this._previousTextures[ name ] = texture; + + this._textureNodes[ name ].updateTexture(); ++ if ( this._rawTextureNodes[ name ] ) this._rawTextureNodes[ name ].updateTexture(); + this._previousTextureNodes[ name ].updateTexture(); + + } +@@ -646,6 +667,17 @@ class PassNode extends TempNode { + * @param {string} [name='output'] - The output name to get the texture node for. + * @return {TextureNode} The texture node. + */ ++ getRawTextureNode( name = 'output' ) { ++ ++ if ( this._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); ++ if ( this.renderTarget.samples !== 4 || this.renderTarget.depth !== 1 ) throw new Error( 'Raw pass attachments require four-sample 2D multisampling.' ); ++ let node = this._rawTextureNodes[ name ]; ++ if ( node === undefined ) node = this._rawTextureNodes[ name ] = new PassMultipleTextureNode( this, name, false, true ); ++ node.updateTexture(); ++ return node; ++ ++ } ++ + getTextureNode( name = 'output' ) { + + let textureNode = this._textureNodes[ name ]; +@@ -988,6 +1020,7 @@ class PassNode extends TempNode { + */ + dispose() { + ++ this._rawTexturesDisposed = true; + this.renderTarget.dispose(); + + } +diff --git a/src/nodes/lighting/PointShadowNode.js b/src/nodes/lighting/PointShadowNode.js +index 01d6909fea7fc5821c827e9596a3afb73cac9457..f9019a72890a30fded9f7ce76982722c2f7d6a3f 100644 +--- a/src/nodes/lighting/PointShadowNode.js ++++ b/src/nodes/lighting/PointShadowNode.js +@@ -2,6 +2,7 @@ import ShadowNode from './ShadowNode.js'; + import { uniform } from '../core/UniformNode.js'; + import { float, vec3, If, Fn } from '../tsl/TSLBase.js'; + import { reference } from '../accessors/ReferenceNode.js'; ++import { getShadowUniform } from './ShadowFilterNode.js'; + import { cubeTexture } from '../accessors/CubeTextureNode.js'; + import { renderGroup } from '../core/UniformGroupNode.js'; + import { Matrix4 } from '../../math/Matrix4.js'; +@@ -65,8 +66,8 @@ export const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, + */ + export const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => { + +- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); +- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); ++ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); ++ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); + + const texelSize = radius.div( mapSize.x ); + +@@ -102,9 +103,9 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo + const shadowPositionAbs = shadowPosition.abs().toConst(); + const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ); + +- const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ); +- const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ); +- const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup ); ++ const shadowCameraNear = getShadowUniform( shadow, 'cameraNear', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near ) ); ++ const shadowCameraFar = getShadowUniform( shadow, 'cameraFar', () => uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far ) ); ++ const bias = getShadowUniform( shadow, 'bias', () => reference( 'bias', 'float', shadow ).setGroup( renderGroup ) ); + + const result = float( 1.0 ).toVar(); + +diff --git a/src/nodes/lighting/ShadowFilterNode.js b/src/nodes/lighting/ShadowFilterNode.js +index 56dde4c38948adb5d2a476e96d0b4416dfd00534..fe8b50c8f76d0b6ce3d2fa1452751b2469a821e4 100644 +--- a/src/nodes/lighting/ShadowFilterNode.js ++++ b/src/nodes/lighting/ShadowFilterNode.js +@@ -11,6 +11,46 @@ import { NoBlending } from '../../constants.js'; + + const shadowMaterialLib = /*@__PURE__*/ new WeakMap(); + ++const _shadowUniforms = /*@__PURE__*/ new WeakMap(); ++ ++/** ++ * Returns the uniform `create` makes for this shadow and name, and makes it only once. ++ * ++ * A filter body runs once per node builder. A uniform that the body makes directly is a new node ++ * for each material, and a new render-group node gives each material its own copy of the shared ++ * render group: a buffer that each material compares and writes on each render call. One node per ++ * shadow lets every material that reads it share one render-group buffer. ++ * ++ * @private ++ * @param {LightShadow} shadow - The shadow that owns the uniform. ++ * @param {string} name - The uniform's name within that shadow. ++ * @param {Function} create - Makes the uniform the first time. ++ * @return {Node} The uniform node. ++ */ ++export const getShadowUniform = ( shadow, name, create ) => { ++ ++ let uniforms = _shadowUniforms.get( shadow ); ++ ++ if ( uniforms === undefined ) { ++ ++ uniforms = new Map(); ++ _shadowUniforms.set( shadow, uniforms ); ++ ++ } ++ ++ let node = uniforms.get( name ); ++ ++ if ( node === undefined ) { ++ ++ node = create(); ++ uniforms.set( name, node ); ++ ++ } ++ ++ return node; ++ ++}; ++ + /** + * A shadow filtering function performing basic filtering. This is in fact an unfiltered version of the shadow map + * with a binary `[0,1]` result. +@@ -65,8 +105,8 @@ export const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, + + }; + +- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); +- const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup ); ++ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); ++ const radius = getShadowUniform( shadow, 'radius', () => reference( 'radius', 'float', shadow ).setGroup( renderGroup ) ); + + const texelSize = vec2( 1 ).div( mapSize ); + const radiusScaled = radius.mul( texelSize.x ); +@@ -97,7 +137,7 @@ export const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, + */ + export const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => { + +- const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ); ++ const mapSize = getShadowUniform( shadow, 'mapSize', () => reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup ) ); + + const texelSize = vec2( 1 ).div( mapSize ); + +diff --git a/src/objects/BatchedMesh.js b/src/objects/BatchedMesh.js +index 2ef7cb257438826059eb2408b3756c4891f06a10..35cc0ffe83a69ea9fa5fca6dec2d820ca193f88d 100644 +--- a/src/objects/BatchedMesh.js ++++ b/src/objects/BatchedMesh.js +@@ -181,6 +181,8 @@ function copyArrayContents( src, target ) { + */ + class BatchedMesh extends Mesh { + ++ static threeNativeBatchedVelocityPatch = true; ++ + /** + * Constructs a new batched mesh. + * +diff --git a/src/renderers/common/Bindings.js b/src/renderers/common/Bindings.js +index 5f3d213a016b37db3b2e5d022da3dd0234d86f6d..a3cec4ed844ab88f870b46714c56c941e0cc7a8e 100644 +--- a/src/renderers/common/Bindings.js ++++ b/src/renderers/common/Bindings.js +@@ -145,7 +145,7 @@ class Bindings extends DataMap { + */ + updateForRender( renderObject ) { + +- this._updateBindings( this.getForRender( renderObject ) ); ++ this._updateBindings( this.getForRender( renderObject ), this.nodes.isSettled( renderObject ) ); + + } + +@@ -206,10 +206,14 @@ class Bindings extends DataMap { + + } else if ( binding.isSampledTexture ) { + ++ // Async node compilation can yield while a viewport node changes targets. ++ // Refresh its reference before creating the first GPU binding, not after. ++ binding.update(); + this.textures.updateTexture( binding.texture ); + + } else if ( binding.isSampler ) { + ++ binding.update(); + this.textures.updateSampler( binding ); + + } else if ( binding.isStorageBuffer ) { +@@ -292,12 +296,13 @@ class Bindings extends DataMap { + * Updates the given array of bindings. + * + * @param {Array} bindings - The bind groups. ++ * @param {boolean} [settled] - Whether the render object is a settled static object. + */ +- _updateBindings( bindings ) { ++ _updateBindings( bindings, settled ) { + + for ( const bindGroup of bindings ) { + +- this._update( bindGroup, bindings ); ++ this._update( bindGroup, bindings, settled ); + + } + +@@ -308,11 +313,27 @@ class Bindings extends DataMap { + * + * @param {BindGroup} bindGroup - The bind group to update. + * @param {Array} bindings - The bind groups. ++ * @param {boolean} [settled] - Whether the render object is a settled static object. + */ +- _update( bindGroup, bindings ) { ++ _update( bindGroup, bindings, settled ) { + + const { backend } = this; + ++ // A group a node added after this object's first draw has no buffers and no layout yet, and ++ // the pipeline reads that layout whatever this draw decides to do with it - so it is built ++ // here, on every draw. A group that did not exist cannot have been written either, so it is ++ // written now: `settled` is what tells the loop below to write it, not to skip it. ++ if ( this.get( bindGroup ).bindGroup === undefined ) { ++ ++ this._createBindings( [ bindGroup ] ); ++ ++ settled = false; ++ ++ } ++ ++ // a settled object keeps the values it uploaded the last time it refreshed, so an object ++ // group it has already written is the one thing in here it may skip ++ + let needsBindingsUpdate = false; + let cacheBindings = true; + let cacheIndex = 0; +@@ -322,7 +343,7 @@ class Bindings extends DataMap { + + for ( const binding of bindGroup.bindings ) { + +- const updatedGroup = this.nodes.updateGroup( binding ); ++ const updatedGroup = this.nodes.updateGroup( binding, settled ); + + // every uniforms group is a uniform buffer. So if no update is required, + // we move one with the next binding. Otherwise the next if block will update the group. +diff --git a/src/renderers/common/Geometries.js b/src/renderers/common/Geometries.js +index eb332c415882927e2fc1ac9b0cc0ed903f70432c..979da2ffc4d0dcda6fdd72747a7e7125e3f8a3a3 100644 +--- a/src/renderers/common/Geometries.js ++++ b/src/renderers/common/Geometries.js +@@ -139,6 +139,14 @@ class Geometries extends DataMap { + */ + this._geometryDisposeListeners = new Map(); + ++ /** ++ * This Weak Map holds the attribute versions a settled render object was last drawn with, ++ * so a write into one of them is what brings the object back out of the settled path. ++ * ++ * @type {WeakMap>} ++ */ ++ this._settledAttributes = new WeakMap(); ++ + } + + /** +@@ -164,10 +172,69 @@ class Geometries extends DataMap { + + if ( this.has( renderObject ) === false ) this.initGeometry( renderObject ); + ++ // A settled static object has no attribute that could have changed since the last refresh, ++ // unless a writer bumped one. An InstancedMesh's instance buffer is a vertex attribute of ++ // this draw, and a BufferGeometry carries no version of its own to announce the write, so ++ // the attributes' own versions are the signal - and the only one there is. ++ ++ const monitor = renderObject.getMonitor(); ++ ++ if ( monitor !== undefined && monitor.isSettled( renderObject ) === true && this.attributesChanged( renderObject ) === false ) return; ++ + this.updateAttributes( renderObject ); + + } + ++ /** ++ * Returns `true` if an attribute of the given render object - or its index - was written to ++ * since the last time its versions were read, and records the versions it holds now. ++ * ++ * @param {RenderObject} renderObject - The render object. ++ * @return {boolean} Whether any of the render object's versions moved. ++ */ ++ attributesChanged( renderObject ) { ++ ++ const attributes = renderObject.getAttributes(); ++ const index = this.getIndex( renderObject ); ++ const settled = this._settledAttributes.get( renderObject ); ++ ++ if ( settled !== undefined && settled.size === attributes.length + ( index === null ? 0 : 1 ) ) { ++ ++ let changed = false; ++ ++ for ( const attribute of attributes ) { ++ ++ if ( settled.get( attribute.isInterleavedBufferAttribute ? attribute.data : attribute ) !== attribute.version ) { ++ ++ changed = true; ++ break; ++ ++ } ++ ++ } ++ ++ if ( changed === false && ( index === null || settled.get( index ) === index.version ) ) return false; ++ ++ } ++ ++ const versions = new Map(); ++ ++ for ( const attribute of attributes ) { ++ ++ const data = attribute.isInterleavedBufferAttribute ? attribute.data : attribute; ++ ++ versions.set( data, data.version ); ++ ++ } ++ ++ if ( index !== null ) versions.set( index, index.version ); ++ ++ this._settledAttributes.set( renderObject, versions ); ++ ++ return true; ++ ++ } ++ + /** + * Initializes the geometry of the given render object. + * +diff --git a/src/renderers/common/RenderContexts.js b/src/renderers/common/RenderContexts.js +index 64ecdeeeabcd2b28000fc503e826d1cbf8d3c118..7319e25e280452b69c6f0c4c2f8c93c955ff9be3 100644 +--- a/src/renderers/common/RenderContexts.js ++++ b/src/renderers/common/RenderContexts.js +@@ -77,7 +77,8 @@ class RenderContexts { + + } + +- if ( renderTarget !== null ) renderState.sampleCount = renderTarget.samples === 0 ? 1 : renderTarget.samples; ++ const sampleCount = renderTarget !== null ? renderTarget.samples : this.renderer.currentSamples; ++ renderState.sampleCount = sampleCount === 0 ? 1 : sampleCount; + + renderState.clearDepthValue = this.renderer.getClearDepth(); + renderState.clearStencilValue = this.renderer.getClearStencil(); +diff --git a/src/renderers/common/RenderObject.js b/src/renderers/common/RenderObject.js +index c900fa4c9654f8cf3512e96de52a2c1c83e189a6..a3dcba2f1fb0df19f3ddb3039a97ae408e2938b3 100644 +--- a/src/renderers/common/RenderObject.js ++++ b/src/renderers/common/RenderObject.js +@@ -299,6 +299,18 @@ class RenderObject { + */ + this._sourceMaterial = renderer._currentSourceMaterial; + ++ /** ++ * The version of `_sourceMaterial` this render object was last validated against. ++ * ++ * An override material is shared by every object in the pass and is used as a per-object ++ * scratchpad, so its own `version` moves for reasons that have nothing to do with this ++ * render object. The source material's version is the signal that does. ++ * ++ * @type {number} ++ * @default 0 ++ */ ++ this.sourceVersion = ( this._sourceMaterial === null || this._sourceMaterial === undefined ) ? 0 : this._sourceMaterial.version; ++ + /** + * An event listener which is defined by `RenderObjects`. It performs + * clean up tasks when `dispose()` on this render object. +@@ -486,7 +498,7 @@ class RenderObject { + */ + getChainArray() { + +- return [ this.object, this.material, this.context, this.lightsNode ]; ++ return [ this.object, this.material, this.context, this.lightsNode, this._sourceMaterial ?? this.material ]; + + } + +@@ -936,6 +948,7 @@ class RenderObject { + } + + cacheKey = hash( cacheKey, this.renderer.contextNode.id, this.renderer.contextNode.version ); ++ cacheKey = hash( cacheKey, this.context.sampleCount ); + + return cacheKey; + +diff --git a/src/renderers/common/RenderObjects.js b/src/renderers/common/RenderObjects.js +index 676cc2bd4169427ed420f2467b1e6684bd2aeebc..72569d0339314d26eb7a35ff2a8462a803c852a6 100644 +--- a/src/renderers/common/RenderObjects.js ++++ b/src/renderers/common/RenderObjects.js +@@ -97,6 +97,8 @@ class RenderObjects { + _chainKeys[ 1 ] = material; + _chainKeys[ 2 ] = renderContext; + _chainKeys[ 3 ] = lightsNode; ++ // Material groups share the shadow override, but must retain separate GPU bindings. ++ _chainKeys[ 4 ] = this.renderer._currentSourceMaterial ?? material; + + // + +@@ -124,9 +126,18 @@ class RenderObjects { + + } + +- if ( renderObject.version !== material.version || renderObject.needsUpdate ) { ++ // When the renderer is drawing through an override material, that material is shared by ++ // the whole pass and mutated per object, so its `version` alone answers the wrong ++ // question. The source material this object actually carries answers the right one. + +- if ( renderObject.initialCacheKey !== renderObject.getCacheKey() ) { ++ const sourceMaterial = this.renderer._currentSourceMaterial; ++ const sourceTracked = sourceMaterial !== null && sourceMaterial !== undefined; ++ const sourceReplaced = sourceTracked && renderObject._sourceMaterial !== sourceMaterial; ++ const sourceChanged = sourceTracked && ( sourceReplaced || renderObject.sourceVersion !== sourceMaterial.version ); ++ ++ if ( renderObject.version !== material.version || sourceChanged || renderObject.needsUpdate ) { ++ ++ if ( sourceReplaced || renderObject.initialCacheKey !== renderObject.getCacheKey() ) { + + renderObject.dispose(); + +@@ -136,6 +147,8 @@ class RenderObjects { + + renderObject.version = material.version; + ++ if ( sourceTracked ) renderObject.sourceVersion = sourceMaterial.version; ++ + } + + } +@@ -148,6 +161,7 @@ class RenderObjects { + _chainKeys[ 1 ] = null; + _chainKeys[ 2 ] = null; + _chainKeys[ 3 ] = null; ++ _chainKeys[ 4 ] = null; + + // + +diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js +index 8dfc2ae055ad7b9d4c00e91f7d52a39e747caebe..b1293bfbe2ccf0583711813c625ddd5c5ba78620 100644 +--- a/src/renderers/common/Renderer.js ++++ b/src/renderers/common/Renderer.js +@@ -1937,6 +1937,18 @@ class Renderer { + + } + ++ /** ++ * Releases an exclusively owned attribute through the backend and memory accounting. ++ * Wait for asynchronous readers before releasing storage shared by compute dispatches. ++ * @param {BufferAttribute} attribute - The owned attribute to release. ++ * @return {?Object} The removed attribute data, or null when absent/uninitialized. ++ */ ++ deleteAttribute( attribute ) { ++ ++ return this._attributes === null ? null : this._attributes.delete( attribute ); ++ ++ } ++ + /** + * Can be used to transfer buffer data from a storage buffer attribute + * from the GPU to the CPU in context of compute shaders. +@@ -2930,6 +2942,14 @@ class Renderer { + + } + ++ // Explicit cold compressed-texture gate; ordinary initTexture keeps its synchronous path. ++ async prepareTextureAsync( texture, options = {} ) { ++ ++ if ( this._initialized === false ) throw new Error( 'Renderer must be initialized before texture preparation.' ); ++ await this._textures.updateTextureAsync( texture, options ); ++ ++ } ++ + /** + * Initializes the given render target. + * +@@ -3550,6 +3570,7 @@ class Renderer { + let materialDisplacementMap; + let materialDisplacementScale; + let materialDisplacementBias; ++ let materialAlphaTest; + + const previousSourceMaterial = this._currentSourceMaterial; + +@@ -3575,6 +3596,7 @@ class Renderer { + materialDisplacementMap = overrideMaterial.displacementMap; + materialDisplacementScale = overrideMaterial.displacementScale; + materialDisplacementBias = overrideMaterial.displacementBias; ++ materialAlphaTest = overrideMaterial.alphaTest; + + if ( material.positionNode && material.positionNode.isNode ) { + +@@ -3582,7 +3604,15 @@ class Renderer { + + } + +- overrideMaterial.alphaTest = material.alphaTest; ++ // `Material.alphaTest`'s setter bumps `version` whenever alpha-test-ness flips, and this ++ // override material is shared by every caster in the pass. A scene that mixes alpha-tested ++ // casters with opaque ones therefore moves that shared version several times per frame, ++ // and every OTHER render object drawn with it then fails the version gate in ++ // `RenderObjects.get` and recomputes a full material cache key that always matches. ++ // Writing the backing field leaves the `alphaTest` getter — and so the cache key — still ++ // reporting this caster's value, while each render object tracks the staleness signal ++ // that belongs to it: its own source material's version. ++ overrideMaterial._alphaTest = material.alphaTest; + overrideMaterial.alphaMap = material.alphaMap; + overrideMaterial.displacementMap = material.displacementMap; + overrideMaterial.displacementScale = material.displacementScale; +@@ -3644,6 +3674,7 @@ class Renderer { + scene.overrideMaterial.displacementMap = materialDisplacementMap; + scene.overrideMaterial.displacementScale = materialDisplacementScale; + scene.overrideMaterial.displacementBias = materialDisplacementBias; ++ scene.overrideMaterial._alphaTest = materialAlphaTest; + + } + +@@ -3705,18 +3736,17 @@ class Renderer { + + // + +- const needsRefresh = this._nodes.needsRefresh( renderObject ); +- +- if ( needsRefresh ) { +- +- this._nodes.updateBefore( renderObject ); +- +- this._geometries.updateForRender( renderObject ); +- +- this._nodes.updateForRender( renderObject ); +- this._bindings.updateForRender( renderObject ); ++ this._nodes.needsRefresh( renderObject ); + +- } ++ // Every step of a draw runs, settled or not. What a settled static object skips is decided ++ // inside each of them - its OBJECT node updates, its object uniform group write and its ++ // attribute scan - because each of those answers for itself, and a step left out here ++ // cannot: a group a node added after this object's first draw still has to be created, and ++ // the pipeline reads that group's layout whatever this draw decided to do with it. ++ this._nodes.updateBefore( renderObject ); ++ this._geometries.updateForRender( renderObject ); ++ this._nodes.updateForRender( renderObject ); ++ this._bindings.updateForRender( renderObject ); + + this._pipelines.updateForRender( renderObject ); + +@@ -3726,7 +3756,7 @@ class Renderer { + + this.backend.draw( renderObject, this.info ); + +- if ( needsRefresh ) this._nodes.updateAfter( renderObject ); ++ this._nodes.updateAfter( renderObject ); + + } + +diff --git a/src/renderers/common/Textures.js b/src/renderers/common/Textures.js +index f42b90eac69ca042c39e7999773d6b8303fc3dd0..5b305173fa4a2814ec74563d2ae5cdc70e7505e1 100644 +--- a/src/renderers/common/Textures.js ++++ b/src/renderers/common/Textures.js +@@ -54,6 +54,10 @@ class Textures extends DataMap { + * @type {Set} + */ + this._htmlTextures = new Set(); ++ this._pendingUploads = new Map(); ++ this._uploadTail = Promise.resolve(); ++ this._uploadSlice = { started: null }; ++ this._uploadDisposed = false; + + } + +@@ -203,7 +207,16 @@ class Textures extends DataMap { + */ + updateTexture( texture, options = {} ) { + ++ if ( this._pendingUploads.has( texture ) ) throw new Error( 'Compressed texture preparation is pending.' ); ++ if ( this.has( texture ) && this.get( texture ).uploadError ) throw this.get( texture ).uploadError; ++ return this._updateTexture( texture, options ); ++ ++ } ++ ++ _updateTexture( texture, options = {}, upload = null ) { ++ + const textureData = this.get( texture ); ++ let uploadPromise; + if ( textureData.initialized === true && textureData.version === texture.version ) return; + + const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture; +@@ -215,6 +228,21 @@ class Textures extends DataMap { + + backend.destroyTexture( texture ); + ++ // delete cached bind groups so they don't point to the destroyed texture ++ ++ if ( textureData.bindGroups ) { ++ ++ for ( const bindGroup of textureData.bindGroups ) { ++ ++ const bindingsData = this.backend.get( bindGroup ); ++ ++ bindingsData.groups = undefined; ++ bindingsData.versions = undefined; ++ ++ } ++ ++ } ++ + } + + // +@@ -352,7 +380,16 @@ class Textures extends DataMap { + + } + +- if ( texture.source.dataReady === true ) backend.updateTexture( texture, options ); ++ if ( texture.source.dataReady === true ) { ++ ++ if ( upload ) { ++ ++ this._initializeTexture( texture, textureData ); ++ uploadPromise = upload( texture, options ); ++ ++ } else backend.updateTexture( texture, options ); ++ ++ } + + const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false; + +@@ -362,7 +399,7 @@ class Textures extends DataMap { + + } + +- if ( texture.onUpdate ) texture.onUpdate( texture ); ++ if ( ! upload && texture.onUpdate ) texture.onUpdate( texture ); + + } + +@@ -379,6 +416,17 @@ class Textures extends DataMap { + + } + ++ this._initializeTexture( texture, textureData ); ++ if ( upload ) return uploadPromise; ++ ++ // ++ ++ textureData.version = texture.version; ++ ++ } ++ ++ _initializeTexture( texture, textureData ) { ++ + // dispose handler + + if ( textureData.initialized !== true ) { +@@ -411,9 +459,134 @@ class Textures extends DataMap { + + } + +- // + +- textureData.version = texture.version; ++ } ++ ++ // A single preparation lane, keyed by Texture identity; Source clones own distinct GPU data. ++ updateTextureAsync( texture, options = {} ) { ++ ++ if ( this._uploadDisposed ) return Promise.reject( new Error( 'Texture manager disposed.' ) ); ++ if ( this._uploadDeviceLost ) return Promise.reject( this._uploadDeviceLost ); ++ if ( ! this._uploadLossSubscribed && this.backend.device?.lost ) { ++ ++ this._uploadLossSubscribed = true; ++ const lost = error => { ++ ++ this._uploadDeviceLost = error instanceof Error ? error : new Error( 'Device lost during texture preparation.' ); ++ for ( const job of this._pendingUploads.values() ) job.controller.abort( this._uploadDeviceLost ); ++ ++ }; ++ this.backend.device.lost.then( lost, lost ); ++ ++ } ++ if ( options.signal?.aborted ) return Promise.reject( options.signal.reason ); ++ const pending = this._pendingUploads.get( texture ); ++ if ( pending ) { ++ ++ if ( pending.version !== texture.version ) return Promise.reject( new Error( 'Texture changed during preparation.' ) ); ++ return this._waitForUpload( pending, options.signal ); ++ ++ } ++ const data = this.get( texture ); ++ if ( data.uploadError ) return Promise.reject( data.uploadError ); ++ if ( ! ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) || texture.isCubeTexture || texture.version <= 0 || texture.source.dataReady !== true || texture.mipmaps.length === 0 || typeof this.backend.updateTextureAsync !== 'function' ) return Promise.reject( new Error( 'Bounded texture preparation requires ready compressed mip data.' ) ); ++ if ( data.initialized === true && data.version === texture.version ) { ++ ++ if ( data.isDefaultTexture !== true && this.backend.has( texture ) && this.backend.get( texture ).texture && this.backend.get( texture ).version === texture.version ) return Promise.resolve(); ++ return Promise.reject( new Error( 'Compressed texture content has not completed an upload.' ) ); ++ ++ } ++ if ( data.version !== undefined && data.isDefaultTexture !== true ) return Promise.reject( new Error( 'Bounded texture preparation requires a cold texture.' ) ); ++ const controller = new AbortController(); ++ const source = texture.source; ++ const mipmaps = texture.mipmaps; ++ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); ++ const image = texture.image; ++ const size = { width: image?.width, height: image?.height, depth: image?.depth }; ++ const layers = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; ++ const device = this.backend.device; ++ const job = { texture, data, version: texture.version, controller, users: 0, promise: null }; ++ const checkQueued = () => { ++ ++ if ( controller.signal.aborted ) throw controller.signal.reason; ++ if ( ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || texture.source !== source || source.dataReady !== true || texture.image !== image || image?.complete === false || image?.width !== size.width || image?.height !== size.height || image?.depth !== size.depth || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) || this.backend.device !== device ) throw new Error( 'Compressed texture changed or invalidated before preparation.' ); ++ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== layers.length || layers.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed before preparation.' ); ++ ++ }; ++ let backendData, gpuTexture; ++ const onQueuedDispose = () => controller.abort( new Error( 'Texture disposed during preparation.' ) ); ++ texture.addEventListener( 'dispose', onQueuedDispose ); ++ this._pendingUploads.set( texture, job ); ++ const cancelled = new Promise( ( resolve, reject ) => controller.signal.addEventListener( 'abort', () => reject( controller.signal.reason ), { once: true } ) ); ++ const work = this._uploadTail.then( () => { ++ ++ checkQueued(); ++ const result = this._updateTexture( texture, {}, ( value, uploadOptions ) => this.backend.updateTextureAsync( value, { ...uploadOptions, budgetMs: options.budgetMs, maxBytesPerWrite: options.maxBytesPerWrite, signal: controller.signal, uploadSlice: this._uploadSlice } ) ); ++ if ( ! result || typeof result.then !== 'function' || ! this.backend.has( texture ) ) throw new Error( 'Compressed texture upload did not start.' ); ++ backendData = this.backend.get( texture ); ++ gpuTexture = backendData.texture; ++ return result; ++ ++ } ); ++ job.promise = Promise.race( [ work, cancelled ] ).then( () => { ++ ++ if ( controller.signal.aborted || ! this.has( texture ) || this.get( texture ) !== data || texture.version !== job.version || source.dataReady !== true || this.backend.device !== device || ! this.backend.has( texture ) || this.backend.get( texture ) !== backendData || backendData.texture !== gpuTexture || backendData.version !== job.version ) throw new Error( 'Texture preparation invalidated.' ); ++ data.version = job.version; ++ delete data.uploadError; ++ if ( texture.onUpdate ) texture.onUpdate( texture ); ++ ++ } ).catch( error => { ++ ++ if ( this.has( texture ) && this.get( texture ) === data ) data.uploadError = error; ++ throw error; ++ ++ } ).finally( () => { ++ ++ texture.removeEventListener( 'dispose', onQueuedDispose ); ++ if ( this._pendingUploads.get( texture ) === job ) this._pendingUploads.delete( texture ); ++ ++ } ); ++ // Caller cancellation is prompt; the backend lane stays owned until validation drains. ++ this._uploadTail = Promise.allSettled( [ work, job.promise ] ).then( () => {} ); ++ return this._waitForUpload( job, options.signal ); ++ ++ } ++ ++ _waitForUpload( job, signal ) { ++ ++ job.users ++; ++ return new Promise( ( resolve, reject ) => { ++ ++ let settled = false; ++ const finish = ( error ) => { ++ ++ if ( settled ) return; ++ settled = true; ++ signal?.removeEventListener( 'abort', abort ); ++ job.users --; ++ if ( error ) reject( error ); else resolve(); ++ if ( job.users === 0 && ! job.controller.signal.aborted ) job.controller.abort( new Error( 'Texture preparation has no remaining caller.' ) ); ++ ++ }; ++ const abort = () => finish( signal.reason || new Error( 'Texture preparation cancelled.' ) ); ++ signal?.addEventListener( 'abort', abort, { once: true } ); ++ job.promise.then( () => finish(), finish ); ++ ++ } ); ++ ++ } ++ ++ dispose() { ++ ++ this._uploadDisposed = true; ++ for ( const job of this._pendingUploads.values() ) { ++ ++ job.controller.abort( new Error( 'Texture manager disposed.' ) ); ++ if ( this.has( job.texture ) && this.get( job.texture ).initialized === true ) this._destroyTexture( job.texture ); ++ ++ } ++ this._pendingUploads.clear(); ++ super.dispose(); + + } + +@@ -591,6 +764,8 @@ class Textures extends DataMap { + */ + _destroyTexture( texture ) { + ++ this._pendingUploads.get( texture )?.controller.abort( new Error( 'Texture disposed during preparation.' ) ); ++ + if ( this.has( texture ) === true ) { + + const textureData = this.get( texture ); +diff --git a/src/renderers/common/nodes/NodeManager.js b/src/renderers/common/nodes/NodeManager.js +index f6d4f0b54e848b172c2c7e138a26e42b98c01978..033128c396c2cad5c5d1d362f2d261e9adbd6bdc 100644 +--- a/src/renderers/common/nodes/NodeManager.js ++++ b/src/renderers/common/nodes/NodeManager.js +@@ -109,15 +109,17 @@ class NodeManager extends DataMap { + * Returns `true` if the given node uniforms group must be updated or not. + * + * @param {NodeUniformsGroup} nodeUniformsGroup - The node uniforms group. ++ * @param {boolean} [settled] - Whether the render object is a settled static object. + * @return {boolean} Whether the node uniforms group requires an update or not. + */ +- updateGroup( nodeUniformsGroup ) { ++ updateGroup( nodeUniformsGroup, settled ) { + + const groupNode = nodeUniformsGroup.groupNode; + +- // groups that are updated per object always require an update so no further checks are needed ++ // groups that are updated per object always require an update so no further checks are needed. ++ // A settled static object keeps the values it uploaded the last time it was refreshed. + +- if ( groupNode.updateType === NodeUpdateType.OBJECT ) return true; ++ if ( groupNode.updateType === NodeUpdateType.OBJECT ) return settled !== true; + + // check for update + +@@ -921,6 +923,21 @@ class NodeManager extends DataMap { + + } + ++ /** ++ * Returns `true` if the per-object updates of the given render object can be skipped ++ * because the object is static and nothing it depends on changed since its last refresh. ++ * ++ * @param {RenderObject} renderObject - The render object. ++ * @return {boolean} Whether the render object is settled or not. ++ */ ++ isSettled( renderObject ) { ++ ++ const monitor = renderObject.getMonitor(); ++ ++ return monitor !== undefined ? monitor.isSettled( renderObject ) : false; ++ ++ } ++ + /** + * Triggers the call of `updateBefore()` methods + * for all nodes of the given render object. +@@ -930,9 +947,14 @@ class NodeManager extends DataMap { + updateBefore( renderObject ) { + + const nodeBuilder = renderObject.getNodeBuilderState(); ++ const settled = this.isSettled( renderObject ); + + for ( const node of nodeBuilder.updateBeforeNodes ) { + ++ // a settled static object has nothing to update per object ++ ++ if ( settled === true && node.getUpdateBeforeType() === NodeUpdateType.OBJECT ) continue; ++ + // update frame state for each node + + this.getNodeFrameForRender( renderObject ).updateBeforeNode( node ); +@@ -950,10 +972,11 @@ class NodeManager extends DataMap { + updateAfter( renderObject ) { + + const nodeBuilder = renderObject.getNodeBuilderState(); ++ const settled = this.isSettled( renderObject ); + + for ( const node of nodeBuilder.updateAfterNodes ) { + +- // update frame state for each node ++ if ( settled === true && node.getUpdateAfterType() === NodeUpdateType.OBJECT ) continue; + + this.getNodeFrameForRender( renderObject ).updateAfterNode( node ); + +@@ -990,9 +1013,14 @@ class NodeManager extends DataMap { + + const nodeFrame = this.getNodeFrameForRender( renderObject ); + const nodeBuilder = renderObject.getNodeBuilderState(); ++ const settled = this.isSettled( renderObject ); + + for ( const node of nodeBuilder.updateNodes ) { + ++ // a settled static object keeps the per-object values it uploaded last time ++ ++ if ( settled === true && node.getUpdateType() === NodeUpdateType.OBJECT ) continue; ++ + nodeFrame.updateNode( node ); + + } +diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js +index 06e203693ed9b168a2bddf773cd8ce8dc23196e4..5636b40e6708a490e926e5f590675834d23bf326 100644 +--- a/src/renderers/webgpu/WebGPUBackend.js ++++ b/src/renderers/webgpu/WebGPUBackend.js +@@ -102,6 +102,27 @@ class WebGPUBackend extends Backend { + */ + this.compatibilityMode = null; + ++ /** ++ * A reference to the WebGPU instance that creates adapters. ++ * ++ * @type {?GPU} ++ * @default null ++ */ ++ this.gpu = null; ++ ++ /** ++ * A reference to the adapter this device was requested from. ++ * ++ * The adapter owns the wire instance every operation in flight completes on. Chromium ++ * collects an adapter the page stops referencing and the instance goes with it, so every ++ * operation still running completes with a shutdown the page reads as ++ * 'A valid external Instance reference no longer exists.' and presentation stops for. ++ * ++ * @type {?GPUAdapter} ++ * @default null ++ */ ++ this.adapter = null; ++ + /** + * A reference to the device. + * +@@ -214,7 +235,11 @@ class WebGPUBackend extends Backend { + xrCompatible: renderer.xr.enabled + }; + +- const adapter = ( typeof navigator !== 'undefined' ) ? await navigator.gpu.requestAdapter( adapterOptions ) : null; ++ this.gpu = ( typeof navigator !== 'undefined' ) ? navigator.gpu : null; ++ ++ const adapter = this.gpu !== null ? await this.gpu.requestAdapter( adapterOptions ) : null; ++ ++ this.adapter = adapter; + + if ( adapter === null ) { + +@@ -441,7 +466,9 @@ class WebGPUBackend extends Backend { + + let descriptor = canvasData.descriptor; + +- if ( descriptor === undefined || canvasData.samples !== samples ) { ++ if ( descriptor === undefined || canvasData.samples !== samples || ++ canvasData.depth !== renderer.depth || canvasData.stencil !== renderer.stencil || ++ canvasData.reversedDepthBuffer !== renderer.reversedDepthBuffer ) { + + descriptor = new GPURenderPassDescriptor(); + descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); +@@ -468,6 +495,9 @@ class WebGPUBackend extends Backend { + + canvasData.descriptor = descriptor; + canvasData.samples = samples; ++ canvasData.depth = renderer.depth; ++ canvasData.stencil = renderer.stencil; ++ canvasData.reversedDepthBuffer = renderer.reversedDepthBuffer; + + } + +@@ -1749,13 +1779,12 @@ class WebGPUBackend extends Backend { + for ( let i = 0, l = bindings.length; i < l; i ++ ) { + + const bindGroup = bindings[ i ]; ++ const bindingsData = this.get( bindGroup ); + +- if ( currentBindingGroups[ i ] !== bindGroup.id ) { +- +- const bindingsData = this.get( bindGroup ); ++ if ( currentBindingGroups[ i ] !== bindingsData.group ) { + + passEncoderGPU.setBindGroup( i, bindingsData.group ); +- currentBindingGroups[ i ] = bindGroup.id; ++ currentBindingGroups[ i ] = bindingsData.group; + + } + +@@ -2054,6 +2083,7 @@ class WebGPUBackend extends Backend { + const utils = this.utils; + + const sampleCount = utils.getSampleCountRenderContext( renderObject.context ); ++ const bindGroupLayoutKeys = renderObject.getBindings().map( bindGroup => this.get( bindGroup ).layoutKey ).join( '|' ); + const colorSpace = utils.getCurrentColorSpace( renderObject.context ); + const colorFormat = utils.getCurrentColorFormat( renderObject.context ); + const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context ); +@@ -2075,7 +2105,8 @@ class WebGPUBackend extends Backend { + data.colorFormat !== colorFormat || data.depthStencilFormat !== depthStencilFormat || + data.primitiveTopology !== primitiveTopology || + data.frontFaceCW !== frontFaceCW || +- data.clippingContextCacheKey !== renderObject.clippingContextCacheKey ++ data.clippingContextCacheKey !== renderObject.clippingContextCacheKey || ++ data.bindGroupLayoutKeys !== bindGroupLayoutKeys + ) { + + data.material = material; data.materialVersion = material.version; +@@ -2095,6 +2126,7 @@ class WebGPUBackend extends Backend { + data.primitiveTopology = primitiveTopology; + data.frontFaceCW = frontFaceCW; + data.clippingContextCacheKey = renderObject.clippingContextCacheKey; ++ data.bindGroupLayoutKeys = bindGroupLayoutKeys; + + needsUpdate = true; + +@@ -2116,6 +2148,7 @@ class WebGPUBackend extends Backend { + + const utils = this.utils; + const renderContext = renderObject.context; ++ const bindGroupLayoutKeys = renderObject.getBindings().map( bindGroup => this.get( bindGroup ).layoutKey ).join( '|' ); + + // meshes with negative scale have a different frontFace render pipeline + // descriptor value so the following must be honored in the cache key +@@ -2134,6 +2167,7 @@ class WebGPUBackend extends Backend { + material.side, + frontFaceCW, + utils.getSampleCountRenderContext( renderContext ), ++ bindGroupLayoutKeys, + utils.getCurrentColorSpace( renderContext ), utils.getCurrentColorFormat( renderContext ), utils.getCurrentDepthStencilFormat( renderContext ), + utils.getPrimitiveTopology( object, material ), + renderObject.getGeometryCacheKey(), +@@ -2204,6 +2238,12 @@ class WebGPUBackend extends Backend { + + } + ++ updateTextureAsync( texture, options ) { ++ ++ return this.textureUtils.updateTextureAsync( texture, options ); ++ ++ } ++ + /** + * Generates mipmaps for the given texture. + * +diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +index be07536301cd47ef2544e4a54f6abe17407f52af..1cbc9b3e02c9832d69a8aadb3a6003392b443c8d 100644 +--- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js ++++ b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +@@ -723,7 +723,7 @@ class WGSLNodeBuilder extends NodeBuilder { + * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. + * @return {string} The WGSL snippet. + */ +- generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { ++ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet, rawTexture = false ) { + + if ( levelSnippet === null ) levelSnippet = '0u'; + +@@ -741,7 +741,14 @@ class WGSLNodeBuilder extends NodeBuilder { + + } else { + +- snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`; ++ // naga v25 (wgpu-native v25.0.2.2) rejects a u32 sample index on a ++ // multisampled textureLoad; i32 is accepted by every backend. Mip-level ++ // operands (including the array form above) stay u32. ++ const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null; ++ const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1; ++ const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 ); ++ const indexType = ( rawTexture || backendMultisampled || targetMultisampled ) ? 'i32' : 'u32'; ++ snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`; + + if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { + +@@ -1282,7 +1289,7 @@ class WGSLNodeBuilder extends NodeBuilder { + + // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) + // Also textureGather always need sampler. +- const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; ++ const needsSampler = node.isRawTextureNode !== true && ( node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null ); + + if ( needsSampler ) { + +@@ -1323,7 +1330,8 @@ class WGSLNodeBuilder extends NodeBuilder { + + uniformGPU = buffer; + +- uniformNode.name = name ? name : 'NodeBuffer_' + uniformNode.id; ++ // The base builder supplies a unique name within this program. A process-wide ++ // allocation id changes otherwise identical shader source for every buffer. + + } else { + +@@ -1711,7 +1719,8 @@ ${ flowData.code } + + for ( const { name, property, type } of builtins.values() ) { + +- snippets.push( `@builtin( ${name} ) ${property} : ${type}` ); ++ const invariant = shaderStage === 'vertex' && name === 'position' && this.context.invariantPosition === true ? '@invariant ' : ''; ++ snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` ); + + } + +@@ -1967,6 +1976,8 @@ ${ flowData.code } + getVaryings( shaderStage ) { + + const snippets = []; ++ const policy = this.context.sampleInterpolation; ++ const sampleInterpolation = typeof policy === 'function' ? policy( this.material ) === true : policy === true; + + if ( shaderStage === 'vertex' ) { + +@@ -1989,7 +2000,12 @@ ${ flowData.code } + + let attributesSnippet = `@location( ${ varyingIndex ++ } )`; + +- if ( varying.interpolationType ) { ++ if ( sampleInterpolation && ! /^(int|uint|ivec|uvec)/.test( varying.type ) && varying.interpolationType !== 'flat' ) { ++ ++ if ( varying.interpolationSampling !== null && varying.interpolationSampling !== 'center' && varying.interpolationSampling !== 'sample' ) throw new Error( 'Sample interpolation policy cannot replace explicit centroid sampling.' ); ++ attributesSnippet += ` @interpolate( ${ varying.interpolationType || 'perspective' }, sample )`; ++ ++ } else if ( varying.interpolationType ) { + + const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )'; + +@@ -2066,9 +2082,12 @@ ${ flowData.code } + const textureNode = uniform.node; + const texture = textureNode.value; + +- // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) +- // Also textureGather always need sampler. +- const needsSampler = texture.isCubeTexture === true || ( this.isUnfilterable( texture ) === false && textureNode.isStorageTextureNode !== true ) || textureNode.gatherNode !== null; ++ // The bind group layout freezes sampler inclusion once, in getUniformFromNode ++ // (nodeData.uniformGPU). Re-deriving it here from live texture filter state can ++ // disagree with that layout and shift every later binding index. ++ const nodeData = this.getDataFromNode( textureNode, shaderStage, this.globalCache ); ++ const uniformGPU = nodeData.uniformGPU; ++ const needsSampler = Array.isArray( uniformGPU ) && uniformGPU[ 0 ].isSampler === true && uniformGPU[ 0 ].isSampledTexture !== true; + + if ( needsSampler ) { + +@@ -2090,7 +2109,7 @@ ${ flowData.code } + + const { primarySamples } = backend.utils.getTextureSampleData( texture ); + +- if ( primarySamples > 1 ) { ++ if ( primarySamples > 1 || textureNode.isRawTextureNode === true ) { + + multisampled = '_multisampled'; + +diff --git a/src/renderers/webgpu/utils/WebGPUAttributeUtils.js b/src/renderers/webgpu/utils/WebGPUAttributeUtils.js +index f683d74a3d10cdffee8a39382d6f0c635657a3cb..16bd72d0c0f1d693481efc98140a0f1b73905149 100644 +--- a/src/renderers/webgpu/utils/WebGPUAttributeUtils.js ++++ b/src/renderers/webgpu/utils/WebGPUAttributeUtils.js +@@ -3,6 +3,7 @@ import { submit } from './WebGPUUtils.js'; + import GPUBufferDescriptor from '../descriptors/GPUBufferDescriptor.js'; + import GPUCommandEncoderDescriptor from '../descriptors/GPUCommandEncoderDescriptor.js'; + ++import { DynamicDrawUsage } from '../../../constants.js'; + import { Float16BufferAttribute } from '../../../core/BufferAttribute.js'; + import { isTypedArray, error } from '../../../utils.js'; + +@@ -174,6 +175,15 @@ class WebGPUAttributeUtils { + buffer.unmap(); + + bufferData.buffer = buffer; ++ bufferData.version = bufferAttribute.version; ++ bufferData.uploadCall = backend.renderer.info.calls; ++ ++ } else if ( bufferData.version < bufferAttribute.version || ++ ( bufferAttribute.usage === DynamicDrawUsage && bufferData.uploadCall !== backend.renderer.info.calls ) ) { ++ ++ // A rebuilt shader can create a fresh attribute wrapper for this existing buffer. ++ // Synchronize its dirty bytes before Attributes records the wrapper as current. ++ this.updateAttribute( attribute ); + + } + +@@ -194,6 +204,11 @@ class WebGPUAttributeUtils { + const bufferData = backend.get( bufferAttribute ); + const buffer = backend.get( bufferAttribute ).buffer; + ++ // A fresh wrapper may already have uploaded this shared buffer in the same render or compute call. ++ // Do not repeat that write after its partial ranges have been consumed. ++ if ( bufferData.version === bufferAttribute.version && ++ ( bufferAttribute.usage !== DynamicDrawUsage || bufferData.uploadCall === backend.renderer.info.calls ) ) return; ++ + let array = bufferAttribute.array; + + const itemSize = bufferData._itemSize; +@@ -275,6 +290,9 @@ class WebGPUAttributeUtils { + + } + ++ bufferData.version = bufferAttribute.version; ++ bufferData.uploadCall = backend.renderer.info.calls; ++ + } + + /** +@@ -361,9 +379,15 @@ class WebGPUAttributeUtils { + destroyAttribute( attribute ) { + + const backend = this.backend; +- const data = backend.get( this._getBufferAttribute( attribute ) ); ++ const bufferAttribute = this._getBufferAttribute( attribute ); ++ const data = backend.get( bufferAttribute ); ++ ++ // An interleaved attribute's GPU buffer lives on its InterleavedBuffer, and several attributes ++ // share it: destroy it once and forget it there too, as the WebGL backend does, or the next ++ // createAttribute() finds the destroyed buffer and submits it (threenative patch). ++ if ( data.buffer !== undefined ) data.buffer.destroy(); + +- data.buffer.destroy(); ++ if ( bufferAttribute !== attribute ) backend.delete( bufferAttribute ); + + backend.delete( attribute ); + +diff --git a/src/renderers/webgpu/utils/WebGPUBindingUtils.js b/src/renderers/webgpu/utils/WebGPUBindingUtils.js +index da4bd473bd8df021159108bac235d211ff2a011f..c479d0a88794b01bca531e7cbfb780cf93fc4721 100644 +--- a/src/renderers/webgpu/utils/WebGPUBindingUtils.js ++++ b/src/renderers/webgpu/utils/WebGPUBindingUtils.js +@@ -97,18 +97,16 @@ class WebGPUBindingUtils { + + const bindingsData = backend.get( bindGroup ); + +- // check if the the bind group already has a layout ++ const entries = this._createLayoutEntries( bindGroup ); ++ const bindGroupLayoutKey = hashString( JSON.stringify( entries ) ); + + if ( bindingsData.layout ) { + +- return bindingsData.layout.layoutGPU; +- +- } ++ if ( bindingsData.layoutKey === bindGroupLayoutKey ) return bindingsData.layout.layoutGPU; + +- // if not, assing one ++ this.deleteBindGroupData( bindGroup ); + +- const entries = this._createLayoutEntries( bindGroup ); +- const bindGroupLayoutKey = hashString( JSON.stringify( entries ) ); ++ } + + // try to find an existing layout in the cache + +@@ -329,7 +327,20 @@ class WebGPUBindingUtils { + + let resourceGPU; + +- if ( textureData.externalTexture !== undefined ) { ++ if ( binding.textureNode?.isRawTextureNode === true ) { ++ ++ if ( binding.textureNode.passNode._rawTexturesDisposed ) throw new Error( 'Raw pass attachment owner is disposed.' ); ++ const raw = binding.texture.isDepthTexture ? textureData.texture : textureData.msaaTexture; ++ if ( binding.texture.renderTarget?.samples !== 4 || raw === undefined ) throw new Error( 'Raw multisample attachment is unavailable.' ); ++ if ( textureData.rawTextureViewSource !== raw ) { ++ ++ textureData.rawTextureView = raw.createView( { dimension: '2d', aspect: binding.texture.isDepthTexture ? 'depth-only' : 'all', baseMipLevel: 0, mipLevelCount: 1 } ); ++ textureData.rawTextureViewSource = raw; ++ ++ } ++ resourceGPU = textureData.rawTextureView; ++ ++ } else if ( textureData.externalTexture !== undefined ) { + + resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } ); + +@@ -500,7 +511,7 @@ class WebGPUBindingUtils { + + const { primarySamples } = backend.utils.getTextureSampleData( binding.texture ); + +- if ( primarySamples > 1 ) { ++ if ( primarySamples > 1 || binding.textureNode?.isRawTextureNode === true ) { + + texture.multisampled = true; + +@@ -538,7 +549,7 @@ class WebGPUBindingUtils { + + } else if ( type === FloatType ) { + +- if ( this.backend.hasFeature( 'float32-filterable' ) ) { ++ if ( texture.multisampled !== true && this.backend.hasFeature( 'float32-filterable' ) ) { + + texture.sampleType = GPUTextureSampleType.Float; + +@@ -630,6 +641,9 @@ class WebGPUBindingUtils { + + } + ++ bindingsData.groups = []; ++ bindingsData.versions = []; ++ + } + + /** +diff --git a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js +index 764267c51628af3b8340888a1fa62ee9f00f435c..3480fea1e86f1f237cb6ee2ea87b5826d4fc404d 100644 +--- a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js ++++ b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js +@@ -299,6 +299,15 @@ class WebGPUPipelineUtils { + + _renderPipelineDescriptor.reset(); + ++ // Close the scope before yielding so unrelated render commands cannot enter it. ++ // Handle rejection immediately even if the pipeline promise is still pending. ++ const errorScopePromise = device.popErrorScope().catch( ( err ) => { ++ ++ asyncError ??= err; ++ return null; ++ ++ } ); ++ + if ( pipelinePromise !== null ) { + + try { +@@ -313,7 +322,7 @@ class WebGPUPipelineUtils { + + } + +- const errorScope = await device.popErrorScope(); ++ const errorScope = await errorScopePromise; + + if ( errorScope !== null || asyncError !== null ) { + +diff --git a/src/renderers/webgpu/utils/WebGPUTextureUtils.js b/src/renderers/webgpu/utils/WebGPUTextureUtils.js +index ff8ee2dbace2816105aa7152891c04f2333f33e8..57c1eb62f1e0de063f22e08ec879e31d2a3240b6 100644 +--- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js ++++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js +@@ -41,6 +41,7 @@ import { + Compatibility + } from '../../../constants.js'; + import { CubeTexture } from '../../../textures/CubeTexture.js'; ++import { DepthTexture } from '../../../textures/DepthTexture.js'; + import { Texture } from '../../../textures/Texture.js'; + import { warn, error } from '../../../utils.js'; + +@@ -143,6 +144,9 @@ class WebGPUTextureUtils { + */ + this._samplerCache = new Map(); + ++ // Own default-canvas depth variants until resize, explicit disposal or teardown. ++ this._depthBuffers = new Map(); ++ + } + + /** +@@ -359,7 +363,12 @@ class WebGPUTextureUtils { + + } + +- if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat ) { ++ // WebGPU has no 3D render attachment, so wgpu-native rejects a 3D texture that asks for ++ // one: a compute volume, or a `.cube` grade table. Only a rendered-into 3D texture keeps it. ++ const notAnAttachment3D = dimension === GPUTextureDimension.ThreeD && ++ texture.isRenderTargetTexture !== true && options.renderTarget === undefined && options.needsMipmaps !== true; ++ ++ if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat && ! notAnAttachment3D ) { + + usage |= GPUTextureUsage.RENDER_ATTACHMENT; + +@@ -432,6 +441,22 @@ class WebGPUTextureUtils { + destroyTexture( texture, isDefaultTexture = false ) { + + const backend = this.backend; ++ ++ const depthBuffers = this._depthBuffers.get( texture ); ++ ++ if ( depthBuffers !== undefined ) { ++ ++ this._depthBuffers.delete( texture ); ++ depthBuffers.canvasTarget.removeEventListener( 'resize', depthBuffers.onDispose ); ++ depthBuffers.canvasTarget.removeEventListener( 'dispose', depthBuffers.onDispose ); ++ texture.removeEventListener( 'dispose', depthBuffers.onDispose ); ++ for ( const buffer of depthBuffers.textures.values() ) this.destroyTexture( buffer ); ++ backend.get( depthBuffers.canvasTarget ).descriptor = undefined; ++ backend.delete( texture ); ++ return; ++ ++ } ++ + const textureData = backend.get( texture ); + + if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); +@@ -472,7 +497,7 @@ class WebGPUTextureUtils { + const colorTexture = canvasTarget.colorTexture; + const colorTextureData = backend.get( colorTexture ); + +- if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { ++ if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { + + return colorTextureData.texture; + +@@ -497,8 +522,8 @@ class WebGPUTextureUtils { + + // + +- colorTexture.source.width = width; +- colorTexture.source.height = height; ++ colorTexture.image.width = width; ++ colorTexture.image.height = height; + colorTexture.samples = samples; + + colorTextureData.texture = colorBuffer; +@@ -521,64 +546,59 @@ class WebGPUTextureUtils { + const canvasTarget = backend.renderer.getCanvasTarget(); + const { width, height } = backend.getDrawingBufferSize(); + const samples = backend.renderer.currentSamples; +- ++ const sampleCount = backend.utils.getSampleCount( samples ); + const depthTexture = canvasTarget.depthTexture; ++ const format = stencil ? DepthStencilFormat : DepthFormat; ++ const type = backend.renderer.reversedDepthBuffer === true ? FloatType : ( stencil ? UnsignedInt248Type : UnsignedIntType ); + +- if ( depthTexture.width === width && +- depthTexture.height === height && +- depthTexture.samples === samples && +- depthTexture.depth === depth && +- depthTexture.stencil === stencil ) { +- +- return backend.get( depthTexture ).texture; +- +- } +- +- // +- +- const depthTextureGPU = backend.get( depthTexture ).texture; ++ let cache = this._depthBuffers.get( depthTexture ); + +- let format, type; ++ if ( cache !== undefined && ( cache.width !== width || cache.height !== height || cache.format !== format || cache.type !== type ) ) { + +- if ( stencil ) { ++ this.destroyTexture( depthTexture ); ++ cache = undefined; + +- format = DepthStencilFormat; +- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type; ++ } + +- } else if ( depth ) { ++ if ( cache === undefined ) { + +- format = DepthFormat; +- type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType; ++ cache = { canvasTarget, width, height, format, type, textures: new Map(), onDispose: () => this.destroyTexture( depthTexture ) }; ++ this._depthBuffers.set( depthTexture, cache ); ++ canvasTarget.addEventListener( 'resize', cache.onDispose ); ++ canvasTarget.addEventListener( 'dispose', cache.onDispose ); ++ depthTexture.addEventListener( 'dispose', cache.onDispose ); + + } + +- if ( depthTextureGPU !== undefined ) { +- +- if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) { ++ // Output and direct overlay passes alternate effective samples 4 and 1. ++ // Retain at most those two buffers for this canvas size and depth format. ++ let buffer = cache.textures.get( sampleCount ); + +- return depthTextureGPU; ++ if ( buffer === undefined ) { + +- } +- +- this.destroyTexture( depthTexture ); ++ buffer = new DepthTexture( width, height, type ); ++ buffer.name = 'depthBuffer'; ++ buffer.format = format; ++ buffer.samples = sampleCount; ++ this.createTexture( buffer, { width, height } ); ++ cache.textures.set( sampleCount, buffer ); + + } + +- // recreate +- ++ // Preserve the canonical texture identity and its currently selected GPU data. + depthTexture.name = 'depthBuffer'; + depthTexture.format = format; + depthTexture.type = type; + depthTexture.image.width = width; + depthTexture.image.height = height; + depthTexture.samples = samples; ++ Object.assign( backend.get( depthTexture ), backend.get( buffer ) ); + +- this.createTexture( depthTexture, { width, height } ); +- +- return backend.get( depthTexture ).texture; ++ return backend.get( buffer ).texture; + + } + ++ + /** + * Uploads the updated texture data to the GPU. + * +@@ -733,6 +753,143 @@ class WebGPUTextureUtils { + + } + ++ // Opt-in preparation for cold compressed textures. The caller owns the visibility gate. ++ async updateTextureAsync( texture, options = {} ) { ++ ++ const { budgetMs = 2, maxBytesPerWrite = 65536, signal } = options; ++ if ( ! Number.isFinite( budgetMs ) || budgetMs <= 0 || ! Number.isSafeInteger( maxBytesPerWrite ) ) throw new Error( 'Invalid compressed upload budget.' ); ++ const backend = this.backend; ++ const device = backend.device; ++ if ( ! backend.has( texture ) ) throw new Error( 'Compressed texture allocation invalidated.' ); ++ const textureData = backend.get( texture ); ++ const gpuTexture = textureData.texture; ++ const descriptor = textureData.textureDescriptorGPU; ++ const block = descriptor && this._getBlockData( descriptor.format ); ++ if ( ! block || ! gpuTexture || maxBytesPerWrite < block.byteLength ) throw new Error( 'Compressed texture allocation or block budget is unsupported.' ); ++ const version = texture.version; ++ const source = texture.source; ++ const mipmaps = texture.mipmaps; ++ const captured = mipmaps.map( mip => ( { mip, data: mip.data, width: mip.width, height: mip.height } ) ); ++ const updates = texture.isCompressedArrayTexture ? [ ...texture.layerUpdates ] : []; ++ const layers = updates.length > 0 ? updates : Array.from( { length: descriptor.size.depthOrArrayLayers }, ( _, i ) => i ); ++ const check = () => { ++ ++ if ( signal?.aborted ) throw signal.reason || new Error( 'Compressed texture upload cancelled.' ); ++ if ( backend.device !== device || ! backend.has( texture ) || backend.get( texture ) !== textureData || textureData.texture !== gpuTexture || textureData.textureDescriptorGPU !== descriptor ) throw new Error( 'Compressed texture allocation invalidated.' ); ++ if ( source.dataReady !== true || texture.version !== version || texture.source !== source || texture.mipmaps !== mipmaps || mipmaps.length !== captured.length || captured.some( ( c, i ) => mipmaps[ i ] !== c.mip || c.mip.data !== c.data || c.mip.width !== c.width || c.mip.height !== c.height ) ) throw new Error( 'Compressed texture changed during preparation.' ); ++ if ( texture.isCompressedArrayTexture && ( texture.layerUpdates.size !== updates.length || updates.some( layer => ! texture.layerUpdates.has( layer ) ) ) ) throw new Error( 'Compressed texture layers changed during preparation.' ); ++ ++ }; ++ function* tiles() { ++ ++ for ( let mipLevel = 0; mipLevel < captured.length; mipLevel ++ ) { ++ ++ const mip = captured[ mipLevel ]; ++ const columns = Math.ceil( mip.width / block.width ); ++ const rows = Math.ceil( mip.height / block.height ); ++ const stride = columns * block.byteLength; ++ const imageBytes = stride * rows; ++ const bytes = new Uint8Array( mip.data.buffer, mip.data.byteOffset, mip.data.byteLength ); ++ const tileColumns = Math.min( columns, Math.floor( maxBytesPerWrite / block.byteLength ) ); ++ const tileRows = stride <= maxBytesPerWrite ? Math.max( 1, Math.floor( maxBytesPerWrite / stride ) ) : 1; ++ for ( const layer of layers ) { ++ ++ if ( ! Number.isInteger( layer ) || layer < 0 || layer >= descriptor.size.depthOrArrayLayers || ( layer + 1 ) * imageBytes > bytes.byteLength ) throw new Error( 'Invalid compressed texture layer data.' ); ++ for ( let y = 0; y < rows; y += tileRows ) { ++ ++ for ( let x = 0; x < columns; x += tileColumns ) { ++ ++ const width = Math.min( tileColumns, columns - x ); ++ const height = Math.min( tileRows, rows - y ); ++ const offset = layer * imageBytes + y * stride + x * block.byteLength; ++ const length = ( height - 1 ) * stride + width * block.byteLength; ++ yield { destination: { texture: gpuTexture, mipLevel, origin: { x: x * block.width, y: y * block.height, z: layer } }, data: bytes.subarray( offset, offset + length ), layout: { offset: 0, bytesPerRow: stride, rowsPerImage: rows }, extent: { width: width * block.width, height: height * block.height, depthOrArrayLayers: 1 } }; ++ ++ } ++ ++ } ++ ++ } ++ ++ } ++ ++ } ++ const slice = options.uploadSlice || { started: null }; ++ const yieldHost = async () => { ++ ++ await new Promise( resolve => setTimeout( resolve, 0 ) ); ++ slice.started = performance.now(); ++ check(); ++ ++ }; ++ const iterator = tiles(); ++ const validations = []; ++ const failures = []; ++ let next = iterator.next(); ++ let uploadFailed = false; ++ let uploadError; ++ try { ++ ++ while ( ! next.done && failures.length === 0 ) { ++ ++ check(); ++ if ( slice.started === null ) slice.started = performance.now(); ++ if ( performance.now() - slice.started >= budgetMs ) await yieldHost(); ++ if ( failures.length > 0 ) break; ++ const scoped = typeof device.pushErrorScope === 'function' && typeof device.popErrorScope === 'function'; ++ if ( scoped ) { device.pushErrorScope( 'out-of-memory' ); device.pushErrorScope( 'validation' ); } ++ try { ++ ++ do { ++ ++ check(); ++ const tile = next.value; ++ device.queue.writeTexture( tile.destination, tile.data, tile.layout, tile.extent ); ++ next = iterator.next(); ++ ++ } while ( ! next.done && performance.now() - slice.started < budgetMs ); ++ ++ } finally { ++ ++ // Pop synchronously: a loading-screen render must not enter our error scopes. ++ if ( scoped ) validations.push( Promise.allSettled( [ device.popErrorScope(), device.popErrorScope() ] ).then( results => { ++ ++ for ( const result of results ) { ++ ++ if ( result.status === 'rejected' ) failures.push( result.reason ); ++ else if ( result.value !== null ) failures.push( new Error( result.value.message ) ); ++ ++ } ++ ++ } ) ); ++ ++ } ++ // Bound validation latency without changing the host slice or byte budgets. ++ if ( validations.length === 4 ) await validations.shift(); ++ check(); ++ if ( ! next.done && failures.length === 0 ) await yieldHost(); ++ ++ } ++ ++ } catch ( error ) { ++ ++ uploadFailed = true; ++ uploadError = error; ++ ++ } finally { ++ ++ // Observe every issued scope, including after cancellation or a write failure. ++ await Promise.all( validations ); ++ ++ } ++ if ( uploadFailed ) throw uploadError; ++ if ( failures.length > 0 ) throw new AggregateError( failures, 'Compressed texture upload failed: ' + failures.map( error => error?.message || String( error ) ).join( '; ' ) ); ++ check(); ++ textureData.version = version; ++ if ( updates.length > 0 ) texture.clearLayerUpdates(); ++ ++ } ++ + /** + * Returns texture data as a typed array. + * +@@ -806,6 +963,8 @@ class WebGPUTextureUtils { + */ + dispose() { + ++ for ( const texture of this._depthBuffers.keys() ) this.destroyTexture( texture ); ++ + this._samplerCache.clear(); + + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2a01277b..e4aa41818 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,7 +117,7 @@ patchedDependencies: hash: e5decbc72f46c0747e26e2ca910e92936f157872011d75fbd32d3d3bcf0d3c1a path: patches/@tailwindcss__node@4.3.3.patch three@0.185.1: - hash: f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f + hash: ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170 path: packages/core/patches/three@0.185.1.patch tsup@8.5.1: hash: bdcba0cb0e0023b6ee572aae9dd40456c0c3bb0edd9c2abd290caed6ebef0770 @@ -201,7 +201,7 @@ importers: version: 19.2.0(react@19.2.0) three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@tailwindcss/vite': specifier: 'catalog:' @@ -248,7 +248,7 @@ importers: dependencies: three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: vite: specifier: 'catalog:' @@ -267,7 +267,7 @@ importers: version: link:../../packages/physics three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -292,7 +292,7 @@ importers: version: link:../../packages/core three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@threenative/playtest': specifier: workspace:* @@ -326,7 +326,7 @@ importers: version: link:../../packages/playtest three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -357,7 +357,7 @@ importers: version: link:../../packages/core three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -385,7 +385,7 @@ importers: version: link:../../packages/physics three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@threenative/playtest': specifier: workspace:* @@ -413,7 +413,7 @@ importers: version: link:../../packages/playtest three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -432,7 +432,7 @@ importers: version: link:../../packages/physics three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -451,7 +451,7 @@ importers: dependencies: three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -476,7 +476,7 @@ importers: version: 0.33.0(react@19.2.0) three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@tailwindcss/vite': specifier: 'catalog:' @@ -525,7 +525,7 @@ importers: version: link:../../packages/physics three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/react': specifier: 'catalog:' @@ -559,7 +559,7 @@ importers: version: link:../../packages/core three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -581,11 +581,11 @@ importers: version: 0.2.0(@types/react@19.2.0)(react@19.2.0) three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@threenative/playtest': specifier: 0.2.0 - version: 0.2.0(playwright@1.62.1)(three@0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f)) + version: 0.2.0(playwright@1.62.1)(three@0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170)) '@types/three': specifier: 0.185.3 version: 0.185.3 @@ -613,7 +613,7 @@ importers: version: link:../../packages/core three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -641,7 +641,7 @@ importers: version: link:../../packages/playtest three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -666,7 +666,7 @@ importers: version: link:../../packages/playtest three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -688,7 +688,7 @@ importers: version: link:../../packages/core three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -710,7 +710,7 @@ importers: version: link:../../packages/core three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -732,7 +732,7 @@ importers: version: link:../../packages/core three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -760,7 +760,7 @@ importers: version: link:../../packages/playtest three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -785,7 +785,7 @@ importers: version: link:../../packages/playtest three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@threenative/runtime-native': specifier: workspace:* @@ -813,7 +813,7 @@ importers: version: link:../../packages/procedural-animals three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@threenative/assets': specifier: workspace:* @@ -841,7 +841,7 @@ importers: version: link:../../packages/playtest three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@threenative/assets': specifier: workspace:* @@ -875,7 +875,7 @@ importers: version: link:../../packages/core three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/three': specifier: 0.185.3 @@ -900,7 +900,7 @@ importers: version: link:../../packages/physics three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@threenative/terrain': specifier: workspace:* @@ -931,7 +931,7 @@ importers: version: link:../../packages/core three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@threenative/playtest': specifier: workspace:* @@ -990,10 +990,10 @@ importers: version: 0.35.4(@types/node@20.19.43) three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) three-mesh-bvh: specifier: 'catalog:' - version: 0.9.14(three@0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f)) + version: 0.9.14(three@0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170)) wasm-media-encoders: specifier: 0.7.0 version: 0.7.0 @@ -1051,10 +1051,10 @@ importers: dependencies: three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) three-mesh-bvh: specifier: 'catalog:' - version: 0.9.14(three@0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f)) + version: 0.9.14(three@0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170)) threenative-asset-mcp: specifier: 0.9.5 version: 0.9.5(@types/node@20.19.43) @@ -1130,7 +1130,7 @@ importers: version: 1.1.1 three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@threenative/assets': specifier: workspace:* @@ -1203,7 +1203,7 @@ importers: version: 0.3.22 three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) tsup: specifier: 8.5.1 version: 8.5.1(patch_hash=bdcba0cb0e0023b6ee572aae9dd40456c0c3bb0edd9c2abd290caed6ebef0770)(@typescript/typescript6@6.0.2)(jiti@2.7.0)(postcss@8.5.25)(tsx@4.23.5) @@ -1234,7 +1234,7 @@ importers: version: 0.3.22 three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) tsup: specifier: 8.5.1 version: 8.5.1(patch_hash=bdcba0cb0e0023b6ee572aae9dd40456c0c3bb0edd9c2abd290caed6ebef0770)(@typescript/typescript6@6.0.2)(jiti@2.7.0)(postcss@8.5.25)(tsx@4.23.5) @@ -1265,7 +1265,7 @@ importers: version: 0.3.22 three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) tsup: specifier: 8.5.1 version: 8.5.1(patch_hash=bdcba0cb0e0023b6ee572aae9dd40456c0c3bb0edd9c2abd290caed6ebef0770)(@typescript/typescript6@6.0.2)(jiti@2.7.0)(postcss@8.5.25)(tsx@4.23.5) @@ -1277,10 +1277,10 @@ importers: dependencies: procedural-animals: specifier: 'catalog:' - version: https://codeload.github.com/majidmanzarpour/threejs-procedural-animals/tar.gz/c95ae49346aa8e140a924376cec6cf0073d99512(three@0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f)) + version: https://codeload.github.com/majidmanzarpour/threejs-procedural-animals/tar.gz/c95ae49346aa8e140a924376cec6cf0073d99512(three@0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170)) three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@threenative/assets': specifier: workspace:* @@ -1308,7 +1308,7 @@ importers: dependencies: three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/node': specifier: 20.19.43 @@ -1355,7 +1355,7 @@ importers: version: 0.3.22 three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) vitest: specifier: 'catalog:' version: 4.1.10(@types/node@20.19.43)(@vitest/coverage-v8@4.1.10)(vite@8.2.0(patch_hash=c99095f3002ba7f0997f348d6c1bcf5952587ed5dee18c058269c0d634d146c1)(@types/node@20.19.43)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.23.5)) @@ -1382,7 +1382,7 @@ importers: version: 0.3.22 three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) tsup: specifier: 'catalog:' version: 8.5.1(patch_hash=bdcba0cb0e0023b6ee572aae9dd40456c0c3bb0edd9c2abd290caed6ebef0770)(@typescript/typescript6@6.0.2)(jiti@2.7.0)(postcss@8.5.25)(tsx@4.23.5) @@ -1400,7 +1400,7 @@ importers: version: 0.8.3 three: specifier: 'catalog:' - version: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + version: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) devDependencies: '@types/node': specifier: 20.19.43 @@ -4755,8 +4755,8 @@ snapshots: '@threenative/core@0.2.0(@types/react@19.2.0)(react@19.2.0)': dependencies: - three: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) - three-mesh-bvh: 0.9.14(three@0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f)) + three: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) + three-mesh-bvh: 0.9.14(three@0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170)) zustand: 5.0.14(@types/react@19.2.0)(react@19.2.0) transitivePeerDependencies: - '@types/react' @@ -4764,19 +4764,19 @@ snapshots: - react - use-sync-external-store - '@threenative/playtest@0.2.0(playwright@1.62.1)(three@0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f))': + '@threenative/playtest@0.2.0(playwright@1.62.1)(three@0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170))': dependencies: pngjs: 7.0.0 optionalDependencies: playwright: 1.62.1 - three: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + three: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) - '@threenative/playtest@0.3.2(playwright@1.62.0)(three@0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f))': + '@threenative/playtest@0.3.2(playwright@1.62.0)(three@0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170))': dependencies: pngjs: 7.0.0 optionalDependencies: playwright: 1.62.0 - three: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + three: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) '@threenative/runtime-native@0.2.0': dependencies: @@ -5690,9 +5690,9 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - procedural-animals@https://codeload.github.com/majidmanzarpour/threejs-procedural-animals/tar.gz/c95ae49346aa8e140a924376cec6cf0073d99512(three@0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f)): + procedural-animals@https://codeload.github.com/majidmanzarpour/threejs-procedural-animals/tar.gz/c95ae49346aa8e140a924376cec6cf0073d99512(three@0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170)): dependencies: - three: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + three: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) proj4@2.22.0(geotiff@3.0.5): dependencies: @@ -5969,11 +5969,11 @@ snapshots: dependencies: any-promise: 1.3.0 - three-mesh-bvh@0.9.14(three@0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f)): + three-mesh-bvh@0.9.14(three@0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170)): dependencies: - three: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + three: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) - three@0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f): {} + three@0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170): {} threenative-asset-mcp@0.9.5(@types/node@20.19.43): dependencies: @@ -5981,11 +5981,11 @@ snapshots: '@gltf-transform/extensions': 4.4.2 '@gltf-transform/functions': 4.4.2(@types/node@20.19.43) '@modelcontextprotocol/server': 2.0.0 - '@threenative/playtest': 0.3.2(playwright@1.62.0)(three@0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f)) + '@threenative/playtest': 0.3.2(playwright@1.62.0)(three@0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170)) '@zip.js/zip.js': 2.8.34 playwright: 1.62.0 sharp: 0.35.4(@types/node@20.19.43) - three: 0.185.1(patch_hash=f3c6b1fca2a168855ac231df8aea1b0ffd76a4a8bf228355a029ece9618b227f) + three: 0.185.1(patch_hash=ac74287de3af98a67b81a5854eb81e4766ee3dcd45d86e345c849a3a891ee170) zod: 4.4.3 transitivePeerDependencies: - '@types/node' From f2bf1eb9ffe49af87d3f3de86118d13bdb371b08 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 15:44:15 -0700 Subject: [PATCH 027/210] test(scaffold): refresh verified shared artifact fingerprints --- ...generated-and-the-upload-lane-leaves-it.md | 2 ++ .../__tests__/scaffold.spec.ts | 28 ++++++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md b/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md index e31688b29..4fd299855 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md @@ -82,3 +82,5 @@ Core audit of `2d2124792` (2026-10-10): again after installation. Generated unified-diff payloads retain upstream whitespace. 2026-10-10 EPIC integration: parent reviewed the generator, bounded upgrade reader, frozen predecessor fixture and CI wiring. The lockfile delta is exactly 56 substitutions of the patch hash, with no other changes. Parent frozen install passed with the merged lock unchanged; the three focused patch specs passed 42 tests in 7.50 s, including all 31 legacy predecessors and all 30 previously shipped finals. A fresh default `pnpm --filter @threenative/core patch:generate --check` downloaded and built the pinned upstream and reported `matched`. CI structure/needs contracts passed 213 tests in 8.68 s. Actual CI, hand-edit rejection, upload migration, runtime/visual proof and wrapper cost remain open. + +Scaffold drift closure: the first parent pre-push gate passed 331 tests but rejected the stale thirteen-tree fingerprints. Restoring only the previous shared Three patch and capability manifest/reference bytes recovered every old fingerprint in the actual `createProject` test. The observed current fingerprints were then recorded, and the unchanged full-tree assertion passed for all thirteen templates (1 selected test, 67 unselected; 1.63 s). No template source or hash exclusion changed. diff --git a/packages/create-threenative/__tests__/scaffold.spec.ts b/packages/create-threenative/__tests__/scaffold.spec.ts index 54a838983..fc184de06 100644 --- a/packages/create-threenative/__tests__/scaffold.spec.ts +++ b/packages/create-threenative/__tests__/scaffold.spec.ts @@ -188,6 +188,8 @@ const BUG_REPORT_SKILL_PATHS = [ // Three patch. Restoring the previous patch recovers every prior fingerprint. // Recomputed from clean committed c22e5ba8d for the independently reviewed bounded-upload // package patch embedded by every scaffold. No template, capability or appearance change. +// PRD-591/593: measured all thirteen actual trees after readiness manifest and generated patch changes. +// Restoring only the previous shared patch/manifest/reference bytes recovers every prior hash. const PRD_201_PARENT_SCAFFOLD_HASHES: Readonly> = { // Measured by the actual thirteen-tree createProject equality test on the Oct 6 // af6dfc8 + ba72eed publication merge, after capability and canonical patch generation. @@ -231,20 +233,20 @@ const PRD_201_PARENT_SCAFFOLD_HASHES: Readonly> = { // strata branch: measured through createProject on the merged tree. // Recomputed 2026-10-10 after integrating develop 1a5314a1: measured through createProject on // the merged tree, including its latest template changes. - "action-rpg": "72539cd4803384cbf8d1d13dcc80116740fd241e2122a19e7b9efd5b6023a145", - minimal: "286787918a1d8aa9e6a7d1ea7cddfeb6016b8582edf4947099771d7a4d5d77c2", - platformer: "fb3acdd77377eb38039aaa6fa634b0e301bb0b42b579260d90948256513d2fce", - puzzle: "d2aa9144346b81cad693db90c38e223c269baaaca00ac2015eb3a22d0c6b0373", - racing: "c0692610b232e12a659eeb591afc7dc03cff265cc278c773f1731b331bcd3382", - rain: "a5485e777dbf56e578641f078cb426821e1f64952dd8ec7ffb1674b1ee8586d0", - rts: "9e8f064a139ee84c0a30079e97f3ff910724b6a9a7bfe5e61d2a915091318dbe", - runner: "3c425e56f0ae0193fd4af6587ebf97e9b94102c3d70d644fc1692d8ce375051a", + "action-rpg": "f564218c84e1bdaa92cc80199191a8e9393653067a3bf80d3611d8b00b41e8ab", + minimal: "0ebcac7e199458b5387b1114f6ef4563430b0d1bbd1070b6bba24798fca59cb2", + platformer: "2d7df1fe72acccbc8dd7b3eed26cf1d293dbd58fdbb0fd1ee1ea4c8829c93d8e", + puzzle: "f3175c1791fa5780849523ee9e4ee898242da1e9a50a43e23ccdc6002de81459", + racing: "2095ecaede919f5a51790c5c762e6cb7259a0683ab7ddca66fac28faeedb8fe1", + rain: "f4b44083ccc11fbff18356d09473abaa1e0c86d681cb1b1d24d37a3ded4082dd", + rts: "884aa0c01a4350ad1cf8524337100e0f6d298ae1b0a67893d6cc1335607b8f2e", + runner: "143f9798fd42af8a5b0710ad028e8c805624e1275e71ab26a6ecfda9e6888405", // Initial finite-height readiness plus its scene-owned lifecycle helper and mirrored docs. - sailing: "8255df7db8cd434c3b832ae7f9dc71f11145839ebbd0012f876fea766362d824", - shooter: "f29ccbc565b468c0c476078f39103469bc59cd074c4511e90a62cac4b52680cb", - snow: "7a49725bf82aaa6373851f4d326d71f0a0981562a0a011312e462696acea5bf9", - starter: "b2a367f2f73cd700bd5e1d447fa810ba823ce4a71f3b54e07d8bdf5e1c45726e", - "tower-defense": "fbb8c4f10c0ee48f244a53bc82920c6089440bc1d73a6ad33a1a65e383e7ccc6", + sailing: "42139f7a5f0e1314651942d8ff4c08648958735654626fbd1dd2bae744004783", + shooter: "6333597c38d5dede454bb9668e4348d01dded6a4db6e69dc1f06d739cfd91053", + snow: "f37a026eb96cae8a987a678230c3a0a652aa778af0921460eaddb18d8ae0b710", + starter: "c8a3ef3bc29fa4b20e746961f85c6140fad2f978d89b455452bdf08134bc1d8e", + "tower-defense": "a5959f1223ca0ee3fe9ed3c446438e2d13167a8398a1304994914ae8c190b916", }; const GENERATED_SCAFFOLD_METADATA = From 4c3c69a71abc5851a50707820ff80f01a80e37f1 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 15:47:01 -0700 Subject: [PATCH 028/210] docs(strata): record actual generated patch drift rejection --- ...he-three-patch-is-generated-and-the-upload-lane-leaves-it.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md b/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md index 4fd299855..6e0e46558 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-593-the-three-patch-is-generated-and-the-upload-lane-leaves-it.md @@ -84,3 +84,5 @@ Core audit of `2d2124792` (2026-10-10): 2026-10-10 EPIC integration: parent reviewed the generator, bounded upgrade reader, frozen predecessor fixture and CI wiring. The lockfile delta is exactly 56 substitutions of the patch hash, with no other changes. Parent frozen install passed with the merged lock unchanged; the three focused patch specs passed 42 tests in 7.50 s, including all 31 legacy predecessors and all 30 previously shipped finals. A fresh default `pnpm --filter @threenative/core patch:generate --check` downloaded and built the pinned upstream and reported `matched`. CI structure/needs contracts passed 213 tests in 8.68 s. Actual CI, hand-edit rejection, upload migration, runtime/visual proof and wrapper cost remain open. Scaffold drift closure: the first parent pre-push gate passed 331 tests but rejected the stale thirteen-tree fingerprints. Restoring only the previous shared Three patch and capability manifest/reference bytes recovered every old fingerprint in the actual `createProject` test. The observed current fingerprints were then recorded, and the unchanged full-tree assertion passed for all thirteen templates (1 selected test, 67 unselected; 1.63 s). No template source or hash exclusion changed. + +2026-10-10 hand-edit rejection proof: parent copied the committed pin, source delta and patch artifacts to an ignored isolated directory, appended a hand-edited line to its base patch, and ran the real generator with `check: true`. A fresh pinned upstream download/build rejected the altered artifact with `check failed, generated patch drifts from the committed patch`. The checkout patch remained byte-identical. This closes the local negative case; AC-2 remains open for actual CI execution. From e12a5d578b554e24bebf75b79a4bba427d579f1e Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 16:48:08 -0700 Subject: [PATCH 029/210] test(native): prove WorldCells spawn readiness lifecycle --- ...-stops-reimplementing-engine-mechanisms.md | 41 ++++- .../scenes/shared/terrain-inline-jobs.js | 165 ++++++++++++++++++ 2 files changed, 200 insertions(+), 6 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md b/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md index 2fd078078..7c9554bb9 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md @@ -68,15 +68,44 @@ both material paths. Disposal clears the timer; unsettled test polling fails closed. Earlier core typecheck/build (publint) passed; the parent core typecheck after the bound correction also passed (exit 0). - Open, not ticked: caller migration (`examples/strata-terrain-preview/src/render/loading.ts`, - `game.ts`, five kit fixtures), `pnpm --filter strata-terrain-preview test:terrain:web`, and - native proof. This migration must keep the current pre-props failure capture, cancellation and - all five fixtures plus the `propStreaming` warmed/prewarmed aggregation; remove none now. - - Native proof open: the linked checkout has no host build, but the primary checkout already - has `packages/runtime-native/build/tn-linux-wgpu/mystral`. The bounded native assertion task - stopped before editing on a provider quota diagnostic. No native proof or new C++ build is claimed. + `game.ts`, five kit fixtures), `pnpm --filter strata-terrain-preview test:terrain:web`, and the + caller's full native-matrix proof. This migration must keep the current pre-props failure + capture, cancellation and all five fixtures plus the `propStreaming` warmed/prewarmed + aggregation; remove none now. + - Native availability corrected: the linked checkout has no `third_party/` or `build/` of its own, + and the primary owning checkout already ships + `packages/runtime-native/build/tn-linux-wgpu/mystral` (also `tn-linux-jsprofile` and + `tn-linux-contracts-physics`). That existing binary proves the gate on the desktop lane. It is + not a new C++ build and not a whole-source or full native-matrix qualification. + - Gate component landed in the existing `world-terrain-inline-hash` scene as + `assertSpawnReadiness()`. It drives the real `WorldCells` over one decoded empty-run cell (no + fetch, no GLTF, no browser global), and asserts a region outside the ring that pends, the + admitting `update` that settles it for its snapshot, one-shot success, disposal rejection and a + finite `25 ms` deadline, before the frame renders. proof: selected row + `--target --only-tests world-terrain-inline-hash`. + - Parent review found the caller surface leaked: `readinessWorld` made a `MeshBasicMaterial` + per load and `WorldCells.dispose` never released a caller surface. Fixed by returning + `{ world, surface }`, disposing the surface in all three callers' `finally` and on a rejected + `load`; same geometry hash and render tree. + - Node: `packages/runtime-native`, `pnpm exec vitest run --config vitest.config.ts + tests/terrain-inline-jobs.test.mjs` — 3/3 passed, pinned hash unchanged. + - Web: pass, 3.0 s, hardware adapter `nvidia/turing`, no page errors, capture non-uniform; + fresh report `packages/runtime-native/artifacts/strata591-cleanup-web/report.json`. + - Desktop: pass with the primary binary through `THREENATIVE_RUNTIME_BINARY`, 15.5 s, + `pixelMismatchRatio` 0, `perceptualDeltaE` 0, native exit 0, runtime SHA-256 + `b756382651baf8347434bdde63cae1b789d126f24f65d2c58ee11e16811ce23e` (unchanged); fresh report + `packages/runtime-native/artifacts/strata591-cleanup-desktop/report.json`. The report keeps + only the last 4000 bytes of native stdout, so a direct run of the same bundle printed + `TN_WORLD_SPAWN_READINESS pending=resolve dispose=reject timeout=25ms` beside + `TN_TERRAIN_INLINE_HASH hash=d138e335 ... expected=d138e335` + (`artifacts/strata591-cleanup-desktop/direct-native.log`). + - Still open under this box: the caller migration, its web playtest and the full native-matrix + claim. No caller migration is attempted here. - [ ] Scatter off the `enter` path. proof: web playtest long-task assertion #### Phase 3: ground and look check **Status:** NOT STARTED - [ ] Engine splat plumbing, measured bands. proof: web playtest - [ ] Visual judge passes. proof: judge verdict on the PR + +Parent review of the native gate found and corrected caller-material ownership: all three fixtures dispose their surfaces, including rejected loads. After the correction, parent inspected the fresh selected web/desktop reports and direct native log: both selected rows pass, no GPU validation errors, desktop image metrics are zero, and both readiness/hash markers appear. Parent reran the focused Node fingerprint spec (3/3 passed, 7.18 s), scoped Biome passed, and documentation links passed. Caller migration and full runtime/visual acceptance remain open. diff --git a/packages/runtime-native/conformance/scenes/shared/terrain-inline-jobs.js b/packages/runtime-native/conformance/scenes/shared/terrain-inline-jobs.js index e683b19dc..f06dd0390 100644 --- a/packages/runtime-native/conformance/scenes/shared/terrain-inline-jobs.js +++ b/packages/runtime-native/conformance/scenes/shared/terrain-inline-jobs.js @@ -1,4 +1,5 @@ import { MeshStandardMaterial } from "three"; +import { WorldCells } from "../../../../core/src/world-cells.ts"; import { TerrainTiles } from "../../../../core/src/world-tiles.ts"; import { assertCondition, startVisualScene, THREE } from "./scene-support.js"; @@ -154,6 +155,168 @@ export function settleInlineTerrainHash() { } } +/** + * One empty-run cell: no asset and no chunk, so residency alone answers the spawn region. + * + * The decoded manifest and placements mean the real `WorldCells` needs no fetch, no GLTF and no + * browser global, which is the only reason this row can assert the gate on the native host too. + */ +const READINESS_MANIFEST = { + version: 1, + extent: { minX: 0, minZ: 0, sizeX: 64, sizeZ: 64 }, + cellSize: 64, + terrain: { heightmap: "unused.u16", columns: 5, rows: 5, spacing: 16, heightMin: 0, heightMax: 1 }, + placements: "unused.bin", + assets: {}, + cells: [{ x: 0, z: 0, runs: [] }], +}; + +/** A follow point far outside the one-cell ring, so the cell stays unadmitted until the walk. */ +const AWAY = 9_000_000; + +/** + * Each `WorldCells.load` this row makes: caller surface, no terrain, no network. + * + * The surface is the caller's, not the world's: `WorldCells.dispose` releases only what it built, + * so this returns the surface beside the world and every caller disposes it. A rejected load hands + * back no world to own the surface, so it disposes the surface here. + */ +async function readinessWorld(follow) { + const surface = new THREE.MeshBasicMaterial(); + try { + const world = await WorldCells.load({ + url: "world.json", + surface, + follow, + ring: 0, + terrain: false, + data: { manifest: READINESS_MANIFEST, placements: new ArrayBuffer(0) }, + budgets: { bytes: 64, instances: 1, residentCells: 1 }, + }); + return { world, surface }; + } catch (error) { + surface.dispose(); + throw error; + } +} + +/** Await a rejection whose message names the expected fragment, and fail closed on a resolve. */ +async function expectRejection(promise, fragment) { + try { + await promise; + } catch (error) { + const message = String(error instanceof Error ? error.message : error); + assertCondition( + message.includes(fragment), + `terrain-inline-jobs: unexpected spawn rejection '${message}'`, + ); + return; + } + throw new Error(`terrain-inline-jobs: expected a spawn rejection naming '${fragment}'`); +} + +/** + * Lifecycle assertions for `WorldCells.whenSpawnReady` on this host. + * + * The core spec pins the gate in Node with a mocked loader. This drives the real class: a region + * outside the ring pends, the update that admits it settles the waiter for its snapshot, a settled + * region answers the next call once, disposal rejects a waiting world, and an unadmitted region + * meets its finite deadline. The marker prints only after every assertion above it holds. + */ +export async function assertSpawnReadiness() { + // A pending region: not ready at arm time, resolved by the update that admits it. + const follow = { position: { x: AWAY, y: 0, z: AWAY } }; + const { world, surface } = await readinessWorld(follow); + try { + const spawn = { x: 32, z: 32 }; + const before = world.readinessAt(spawn); + assertCondition( + before.ready === false && before.requiredCells === 1 && before.loadedCells === 0, + "terrain-inline-jobs: an unadmitted spawn region must not be ready", + ); + const pending = world.whenSpawnReady(spawn, 0, 5000); + let settled; + let failure; + pending.then( + (readiness) => { + settled = readiness; + }, + (error) => { + failure = error; + }, + ); + // Mutated after the call: the waiter must keep the coordinates it armed with. + spawn.x = AWAY; + world.update(); + await Promise.resolve(); + assertCondition( + settled === undefined && failure === undefined, + "terrain-inline-jobs: an unadmitted region must keep its waiter pending", + ); + // Move the follow point into the ring, then drive updates and microtasks until it settles. + follow.position.x = 32; + follow.position.z = 32; + for (let frame = 0; frame < 64 && settled === undefined && failure === undefined; frame += 1) { + world.update(); + await Promise.resolve(); + } + assertCondition( + failure === undefined, + `terrain-inline-jobs: whenSpawnReady rejected: ${String(failure)}`, + ); + assertCondition( + settled !== undefined && + settled.ready === true && + settled.loadedCells === 1 && + settled.requiredCells === 1 && + settled.failures === 0, + "terrain-inline-jobs: the admitted spawn region did not settle ready", + ); + // One-shot: the settled region answers the next call immediately, with no further update. + const again = await world.whenSpawnReady({ x: 32, z: 32 }, 0, 5000); + assertCondition( + again.ready === true && again.loadedCells === 1, + "terrain-inline-jobs: a ready region must answer immediately", + ); + } finally { + world.dispose(); + surface.dispose(); + } + + // Disposal rejects a waiting world instead of leaving a game awaiting its gate. + const doomedFollow = { position: { x: AWAY, y: 0, z: AWAY } }; + const { world: doomed, surface: doomedSurface } = await readinessWorld(doomedFollow); + try { + const pending = doomed.whenSpawnReady({ x: 32, z: 32 }, 0, 5000); + doomed.update(); + await Promise.resolve(); + doomed.dispose(); + await expectRejection(pending, "WorldCells spawn cancelled"); + } finally { + doomed.dispose(); + doomedSurface.dispose(); + } + + // An unadmitted region meets its finite deadline, and the region stays unready. + const timedFollow = { position: { x: AWAY, y: 0, z: AWAY } }; + const { world: timed, surface: timedSurface } = await readinessWorld(timedFollow); + try { + await expectRejection( + timed.whenSpawnReady({ x: 32, z: 32 }, 0, 25), + "WorldCells spawn exceeded 25 ms", + ); + assertCondition( + timed.readinessAt({ x: 32, z: 32 }).ready === false, + "terrain-inline-jobs: a timed-out region must stay unready", + ); + } finally { + timed.dispose(); + timedSurface.dispose(); + } + + console.info("TN_WORLD_SPAWN_READINESS pending=resolve dispose=reject timeout=25ms"); +} + export async function startScene(canvas, dimensions) { const settled = settleInlineTerrainHash(); assertCondition( @@ -165,6 +328,8 @@ export async function startScene(canvas, dimensions) { `TN_TERRAIN_INLINE_HASH hash=${settled.hash} blocks=${settled.blocks} ` + `bridges=${settled.bridges} expected=${EXPECTED_HASH}`, ); + // The spawn gate the streamed terrain games repeat, proven on the real class before the frame. + await assertSpawnReadiness(); return startVisualScene( canvas, dimensions, From 3b32a45c238bc1d2b33a3b06be59791fcb25f375 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 23:09:39 -0700 Subject: [PATCH 030/210] refactor(strata): join engine spawn readiness in preview callers --- ...-stops-reimplementing-engine-mechanisms.md | 20 +- .../scripts/fixtures/kit-game/Alpine.ts | 73 ++-- .../scripts/fixtures/kit-game/Coastal.ts | 74 ++-- .../scripts/fixtures/kit-game/Desert.ts | 73 ++-- .../scripts/fixtures/kit-game/Forest.ts | 73 ++-- .../scripts/fixtures/kit-game/Tundra.ts | 73 ++-- examples/strata-terrain-preview/src/game.ts | 94 +++-- .../src/render/loading.ts | 66 --- .../terrain/__tests__/prop-streaming.spec.ts | 51 +-- .../terrain/__tests__/strata-loading.spec.ts | 388 ++++++++++++++---- 10 files changed, 633 insertions(+), 352 deletions(-) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md b/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md index 7c9554bb9..a5890a60c 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md @@ -67,11 +67,17 @@ both material paths. Node timer overflow, then passed with the supported `(0, 2147483647]` ms bound. Disposal clears the timer; unsettled test polling fails closed. Earlier core typecheck/build (publint) passed; the parent core typecheck after the bound correction also passed (exit 0). - - Open, not ticked: caller migration (`examples/strata-terrain-preview/src/render/loading.ts`, - `game.ts`, five kit fixtures), `pnpm --filter strata-terrain-preview test:terrain:web`, and the - caller's full native-matrix proof. This migration must keep the current pre-props failure - capture, cancellation and all five fixtures plus the `propStreaming` warmed/prewarmed - aggregation; remove none now. + - Caller migration implemented, runtime acceptance still open: the preview and five kit fixtures + join `WorldCells.whenSpawnReady` with every stream's `prewarmed` promise. One entry deadline + covers preparation and admission; cancellation, late settlement and pre-props failure capture + remain guarded. The old `createSpawnReadiness` helper is deleted; loading UI and bounded failure + receipts remain. Timeout failures update UI state immediately. Empty kit failures receive a + nonempty kit label and cannot be overwritten by late success or another failure. + Parent reproduced the empty-error defect in four kits, corrected Forest's VM setup, then ran + the actual-source loading tests: 31/31 passed (1.08 s); the focused outstanding-world-load test + also passed (other nine tests skipped). Terrain typecheck and direct preview `tsc --noEmit` + passed. Scoped Biome and `git diff --check` passed. The web playtest, all-kit runtime captures + and caller full native-matrix proof remain unverified; this box stays open. - Native availability corrected: the linked checkout has no `third_party/` or `build/` of its own, and the primary owning checkout already ships `packages/runtime-native/build/tn-linux-wgpu/mystral` (also `tn-linux-jsprofile` and @@ -99,8 +105,8 @@ both material paths. `TN_WORLD_SPAWN_READINESS pending=resolve dispose=reject timeout=25ms` beside `TN_TERRAIN_INLINE_HASH hash=d138e335 ... expected=d138e335` (`artifacts/strata591-cleanup-desktop/direct-native.log`). - - Still open under this box: the caller migration, its web playtest and the full native-matrix - claim. No caller migration is attempted here. + - Still open under this box: caller runtime verification, its web playtest and the full + native-matrix claim. The selected native row proves the engine gate component only. - [ ] Scatter off the `enter` path. proof: web playtest long-task assertion #### Phase 3: ground and look check diff --git a/examples/strata-terrain-preview/scripts/fixtures/kit-game/Alpine.ts b/examples/strata-terrain-preview/scripts/fixtures/kit-game/Alpine.ts index 248fa4c1b..0e38a2765 100644 --- a/examples/strata-terrain-preview/scripts/fixtures/kit-game/Alpine.ts +++ b/examples/strata-terrain-preview/scripts/fixtures/kit-game/Alpine.ts @@ -6,7 +6,7 @@ import { type ICtx, type IFrameBudgetWindow, Scene } from "@threenative/core"; import { CharacterBody3D, CollisionShape3D, type IPhysicsContext } from "@threenative/physics"; import { Object3D, type PerspectiveCamera, Vector3 } from "three"; -import { createLoadingScreen, createSpawnReadiness } from "../render/loading.js"; +import { createLoadingScreen } from "../render/loading.js"; import { type GameState, initialState } from "../state.js"; import { alpineDaylight } from "../terrain/alpine/sky.js"; import { COLLIDERS, type IAlpineWorld, addAlpine } from "../terrain/alpine/world.js"; @@ -308,18 +308,36 @@ export class Alpine extends Scene { #alpine: IAlpineWorld | undefined; #stand: IStand | undefined; #released = false; - #admission: ReturnType | undefined; + #ready = false; + #error = ""; + #settle: ((error?: Error) => void) | undefined; + #timer: ReturnType | undefined; #loading: ReturnType | undefined; /** The kit's world streams while the scene loads, so `ready` waits for the cells the spawn needs. */ override async load(ctx: Ctx): Promise { for (const view of VIEW_NAMES) viewWindows.set(view, { frameP95: [], gpuP50: [], gpuP95: [] }); - const admission = createSpawnReadiness("Alpine spawn"); - this.#admission = admission; - void admission.promise.catch((error: unknown) => { + const deadlineAt = performance.now() + 120_000; + let settled = false; + const spawn = new Promise((done, fail) => { + this.#settle = (error) => { + if (settled) return; + settled = true; + if (this.#timer !== undefined) clearTimeout(this.#timer); + if (error === undefined) { + if (!this.#released && !this.#error) this.#ready = true; + done(); + } else fail(error); + }; + }); + this.#timer = setTimeout( + () => this.#fail(ctx, "Alpine spawn admission exceeded 120000 ms"), + 120_000, + ); + void spawn.catch((error: unknown) => { this.#fail(ctx, error instanceof Error ? error.message : String(error)); }); - if (ctx.startup.phase !== "ready") ctx.startup.hold("alpine-spawn", admission.promise, 120_000); + if (ctx.startup.phase !== "ready") ctx.startup.hold("alpine-spawn", spawn, 120_000); let alpine: IAlpineWorld | undefined; // A holder rather than a `let`: the progress getter reads it, and the spawn is set once it is known. const spawnView: { at?: Vector3 } = {}; @@ -336,8 +354,7 @@ export class Alpine extends Scene { (ctx.startup.progress + (required > 0 ? loaded / required : 0)) / 2, ); }, - whenReady: () => - Promise.all([ctx.startup.whenReady(), admission.promise]).then(() => undefined), + whenReady: () => Promise.all([ctx.startup.whenReady(), spawn]).then(() => undefined), }, }); try { @@ -360,6 +377,19 @@ export class Alpine extends Scene { } this.#stand = stand; spawnView.at = stand.views.ground.at; + // Arm once, after the world is attached: the engine owns readiness, its prewarm owns the draw. + const remaining = deadlineAt - performance.now(); + void ( + remaining > 0 + ? Promise.all([ + alpine.world.whenSpawnReady(stand.views.ground.at, 60, remaining), + alpine.world.prewarmed, + ]) + : Promise.reject(new Error("Alpine spawn admission exceeded 120000 ms")) + ).then( + () => this.#settle?.(), + (error: unknown) => this.#fail(ctx, error instanceof Error ? error.message : String(error)), + ); const player = new Object3D(); player.position.set(stand.spawn.x, stand.groundY + HALF_HEIGHT + RADIUS + 2.5, stand.spawn.z); this.#player = player; @@ -402,29 +432,24 @@ export class Alpine extends Scene { spawnTerrainRequired: region.requiredTerrainTiles, }); if (this.#released) return; - this.#admission?.observe( - region.ready && alpine.world.stats().pendingPrewarm === 0, - region.failures, - ); ctx.state.set({ - worldReady: this.#admission?.ready ? 1 : 0, - loadingError: this.#admission?.error ?? "", + worldReady: this.#ready ? 1 : 0, + loadingError: this.#error, }); }); } #fail(ctx: Ctx, message: string): void { - if (this.#released || ctx.state.getState().loadingError) return; - this.#admission?.fail(message); - ctx.state.set({ - loadingError: this.#admission?.error ?? message, - worldReady: this.#admission?.ready ? 1 : 0, - }); + if (this.#released || this.#error || this.#ready) return; + const error = message || "Alpine spawn failed"; + this.#error = error; + this.#settle?.(new Error(error)); + ctx.state.set({ loadingError: error, worldReady: 0 }); } override exit(): void { this.#released = true; - this.#admission?.cancel(); + this.#settle?.(new Error("Alpine spawn cancelled")); this.#loading?.finish(); this.#body?.dispose(); if (this.#alpine) disposeAlpine(this.#alpine); @@ -435,11 +460,7 @@ export class Alpine extends Scene { const player = this.#player; const stand = this.#stand; if (body === undefined || player === undefined || stand === undefined) return; - if ( - !this.#admission?.ready || - ctx.startup.phase !== "ready" || - ctx.state.getState().loadingError - ) { + if (!this.#ready || ctx.startup.phase !== "ready" || ctx.state.getState().loadingError) { body.velocity.x = 0; body.velocity.z = 0; body.moveAndSlide(dt); diff --git a/examples/strata-terrain-preview/scripts/fixtures/kit-game/Coastal.ts b/examples/strata-terrain-preview/scripts/fixtures/kit-game/Coastal.ts index e86463627..b122e2130 100644 --- a/examples/strata-terrain-preview/scripts/fixtures/kit-game/Coastal.ts +++ b/examples/strata-terrain-preview/scripts/fixtures/kit-game/Coastal.ts @@ -7,7 +7,7 @@ import { type ICtx, type IFrameBudgetWindow, Scene } from "@threenative/core"; import { CharacterBody3D, CollisionShape3D, type IPhysicsContext } from "@threenative/physics"; import { Object3D, type PerspectiveCamera, Vector3 } from "three"; -import { createLoadingScreen, createSpawnReadiness } from "../render/loading.js"; +import { createLoadingScreen } from "../render/loading.js"; import { type GameState, initialState } from "../state.js"; import { coastalDaylight } from "../terrain/coastal/sky.js"; import { COLLIDERS, type ICoastalWorld, addCoastal } from "../terrain/coastal/world.js"; @@ -338,19 +338,36 @@ export class Coastal extends Scene { #coastal: ICoastalWorld | undefined; #stand: IStand | undefined; #released = false; - #admission: ReturnType | undefined; + #ready = false; + #error = ""; + #settle: ((error?: Error) => void) | undefined; + #timer: ReturnType | undefined; #loading: ReturnType | undefined; /** The kit's world streams while the scene loads, so `ready` waits for the cells the spawn needs. */ override async load(ctx: Ctx): Promise { for (const view of VIEW_NAMES) viewWindows.set(view, { frameP95: [], gpuP50: [], gpuP95: [] }); - const admission = createSpawnReadiness("Coastal spawn"); - this.#admission = admission; - void admission.promise.catch((error: unknown) => { + const deadlineAt = performance.now() + 120_000; + let settled = false; + const spawn = new Promise((done, fail) => { + this.#settle = (error) => { + if (settled) return; + settled = true; + if (this.#timer !== undefined) clearTimeout(this.#timer); + if (error === undefined) { + if (!this.#released && !this.#error) this.#ready = true; + done(); + } else fail(error); + }; + }); + this.#timer = setTimeout( + () => this.#fail(ctx, "Coastal spawn admission exceeded 120000 ms"), + 120_000, + ); + void spawn.catch((error: unknown) => { this.#fail(ctx, error instanceof Error ? error.message : String(error)); }); - if (ctx.startup.phase !== "ready") - ctx.startup.hold("coastal-spawn", admission.promise, 120_000); + if (ctx.startup.phase !== "ready") ctx.startup.hold("coastal-spawn", spawn, 120_000); let coastal: ICoastalWorld | undefined; // A holder rather than a `let`: the progress getter reads it, and the spawn is set once it is known. const spawnView: { at?: Vector3 } = {}; @@ -367,8 +384,7 @@ export class Coastal extends Scene { (ctx.startup.progress + (required > 0 ? loaded / required : 0)) / 2, ); }, - whenReady: () => - Promise.all([ctx.startup.whenReady(), admission.promise]).then(() => undefined), + whenReady: () => Promise.all([ctx.startup.whenReady(), spawn]).then(() => undefined), }, }); try { @@ -391,6 +407,19 @@ export class Coastal extends Scene { } this.#stand = stand; spawnView.at = stand.views.ground.at; + // Arm once, after the world is attached: the engine owns readiness, its prewarm owns the draw. + const remaining = deadlineAt - performance.now(); + void ( + remaining > 0 + ? Promise.all([ + coastal.world.whenSpawnReady(stand.views.ground.at, 60, remaining), + coastal.world.prewarmed, + ]) + : Promise.reject(new Error("Coastal spawn admission exceeded 120000 ms")) + ).then( + () => this.#settle?.(), + (error: unknown) => this.#fail(ctx, error instanceof Error ? error.message : String(error)), + ); const player = new Object3D(); player.position.set(stand.spawn.x, stand.groundY + HALF_HEIGHT + RADIUS + 2.5, stand.spawn.z); this.#player = player; @@ -439,29 +468,24 @@ export class Coastal extends Scene { spawnTerrainRequired: region.requiredTerrainTiles, }); if (this.#released) return; - this.#admission?.observe( - region.ready && coastal.world.stats().pendingPrewarm === 0, - region.failures, - ); ctx.state.set({ - worldReady: this.#admission?.ready ? 1 : 0, - loadingError: this.#admission?.error ?? "", + worldReady: this.#ready ? 1 : 0, + loadingError: this.#error, }); }); } #fail(ctx: Ctx, message: string): void { - if (this.#released || ctx.state.getState().loadingError) return; - this.#admission?.fail(message); - ctx.state.set({ - loadingError: this.#admission?.error ?? message, - worldReady: this.#admission?.ready ? 1 : 0, - }); + if (this.#released || this.#error || this.#ready) return; + const error = message || "Coastal spawn failed"; + this.#error = error; + this.#settle?.(new Error(error)); + ctx.state.set({ loadingError: error, worldReady: 0 }); } override exit(): void { this.#released = true; - this.#admission?.cancel(); + this.#settle?.(new Error("Coastal spawn cancelled")); this.#loading?.finish(); this.#body?.dispose(); if (this.#coastal) disposeCoastal(this.#coastal); @@ -472,11 +496,7 @@ export class Coastal extends Scene { const player = this.#player; const stand = this.#stand; if (body === undefined || player === undefined || stand === undefined) return; - if ( - !this.#admission?.ready || - ctx.startup.phase !== "ready" || - ctx.state.getState().loadingError - ) { + if (!this.#ready || ctx.startup.phase !== "ready" || ctx.state.getState().loadingError) { body.velocity.x = 0; body.velocity.z = 0; body.moveAndSlide(dt); diff --git a/examples/strata-terrain-preview/scripts/fixtures/kit-game/Desert.ts b/examples/strata-terrain-preview/scripts/fixtures/kit-game/Desert.ts index 1fcfd8a90..3ef839aba 100644 --- a/examples/strata-terrain-preview/scripts/fixtures/kit-game/Desert.ts +++ b/examples/strata-terrain-preview/scripts/fixtures/kit-game/Desert.ts @@ -6,7 +6,7 @@ import { type ICtx, type IFrameBudgetWindow, Scene } from "@threenative/core"; import { CharacterBody3D, CollisionShape3D, type IPhysicsContext } from "@threenative/physics"; import { Object3D, type PerspectiveCamera, Vector3 } from "three"; -import { createLoadingScreen, createSpawnReadiness } from "../render/loading.js"; +import { createLoadingScreen } from "../render/loading.js"; import { type GameState, initialState } from "../state.js"; import { desertDaylight } from "../terrain/desert/sky.js"; import { COLLIDERS, type IDesertWorld, addDesert } from "../terrain/desert/world.js"; @@ -308,18 +308,36 @@ export class Desert extends Scene { #desert: IDesertWorld | undefined; #stand: IStand | undefined; #released = false; - #admission: ReturnType | undefined; + #ready = false; + #error = ""; + #settle: ((error?: Error) => void) | undefined; + #timer: ReturnType | undefined; #loading: ReturnType | undefined; /** The kit's world streams while the scene loads, so `ready` waits for the cells the spawn needs. */ override async load(ctx: Ctx): Promise { for (const view of VIEW_NAMES) viewWindows.set(view, { frameP95: [], gpuP50: [], gpuP95: [] }); - const admission = createSpawnReadiness("Desert spawn"); - this.#admission = admission; - void admission.promise.catch((error: unknown) => { + const deadlineAt = performance.now() + 120_000; + let settled = false; + const spawn = new Promise((done, fail) => { + this.#settle = (error) => { + if (settled) return; + settled = true; + if (this.#timer !== undefined) clearTimeout(this.#timer); + if (error === undefined) { + if (!this.#released && !this.#error) this.#ready = true; + done(); + } else fail(error); + }; + }); + this.#timer = setTimeout( + () => this.#fail(ctx, "Desert spawn admission exceeded 120000 ms"), + 120_000, + ); + void spawn.catch((error: unknown) => { this.#fail(ctx, error instanceof Error ? error.message : String(error)); }); - if (ctx.startup.phase !== "ready") ctx.startup.hold("desert-spawn", admission.promise, 120_000); + if (ctx.startup.phase !== "ready") ctx.startup.hold("desert-spawn", spawn, 120_000); let desert: IDesertWorld | undefined; // A holder rather than a `let`: the progress getter reads it, and the spawn is set once it is known. const spawnView: { at?: Vector3 } = {}; @@ -336,8 +354,7 @@ export class Desert extends Scene { (ctx.startup.progress + (required > 0 ? loaded / required : 0)) / 2, ); }, - whenReady: () => - Promise.all([ctx.startup.whenReady(), admission.promise]).then(() => undefined), + whenReady: () => Promise.all([ctx.startup.whenReady(), spawn]).then(() => undefined), }, }); try { @@ -360,6 +377,19 @@ export class Desert extends Scene { } this.#stand = stand; spawnView.at = stand.views.ground.at; + // Arm once, after the world is attached: the engine owns readiness, its prewarm owns the draw. + const remaining = deadlineAt - performance.now(); + void ( + remaining > 0 + ? Promise.all([ + desert.world.whenSpawnReady(stand.views.ground.at, 60, remaining), + desert.world.prewarmed, + ]) + : Promise.reject(new Error("Desert spawn admission exceeded 120000 ms")) + ).then( + () => this.#settle?.(), + (error: unknown) => this.#fail(ctx, error instanceof Error ? error.message : String(error)), + ); const player = new Object3D(); player.position.set(stand.spawn.x, stand.groundY + HALF_HEIGHT + RADIUS + 2.5, stand.spawn.z); this.#player = player; @@ -402,29 +432,24 @@ export class Desert extends Scene { spawnTerrainRequired: region.requiredTerrainTiles, }); if (this.#released) return; - this.#admission?.observe( - region.ready && desert.world.stats().pendingPrewarm === 0, - region.failures, - ); ctx.state.set({ - worldReady: this.#admission?.ready ? 1 : 0, - loadingError: this.#admission?.error ?? "", + worldReady: this.#ready ? 1 : 0, + loadingError: this.#error, }); }); } #fail(ctx: Ctx, message: string): void { - if (this.#released || ctx.state.getState().loadingError) return; - this.#admission?.fail(message); - ctx.state.set({ - loadingError: this.#admission?.error ?? message, - worldReady: this.#admission?.ready ? 1 : 0, - }); + if (this.#released || this.#error || this.#ready) return; + const error = message || "Desert spawn failed"; + this.#error = error; + this.#settle?.(new Error(error)); + ctx.state.set({ loadingError: error, worldReady: 0 }); } override exit(): void { this.#released = true; - this.#admission?.cancel(); + this.#settle?.(new Error("Desert spawn cancelled")); this.#loading?.finish(); this.#body?.dispose(); if (this.#desert) disposeDesert(this.#desert); @@ -435,11 +460,7 @@ export class Desert extends Scene { const player = this.#player; const stand = this.#stand; if (body === undefined || player === undefined || stand === undefined) return; - if ( - !this.#admission?.ready || - ctx.startup.phase !== "ready" || - ctx.state.getState().loadingError - ) { + if (!this.#ready || ctx.startup.phase !== "ready" || ctx.state.getState().loadingError) { body.velocity.x = 0; body.velocity.z = 0; body.moveAndSlide(dt); diff --git a/examples/strata-terrain-preview/scripts/fixtures/kit-game/Forest.ts b/examples/strata-terrain-preview/scripts/fixtures/kit-game/Forest.ts index 7984fb2d6..237d954f4 100644 --- a/examples/strata-terrain-preview/scripts/fixtures/kit-game/Forest.ts +++ b/examples/strata-terrain-preview/scripts/fixtures/kit-game/Forest.ts @@ -5,7 +5,7 @@ import { type ICtx, type IFrameBudgetWindow, Scene } from "@threenative/core"; import { CharacterBody3D, CollisionShape3D, type IPhysicsContext } from "@threenative/physics"; import { type InstancedMesh, Object3D, type PerspectiveCamera, Vector3 } from "three"; -import { createLoadingScreen, createSpawnReadiness } from "../render/loading.js"; +import { createLoadingScreen } from "../render/loading.js"; import { type GameState, initialState } from "../state.js"; import { forestDaylight } from "../terrain/forest/sky.js"; import { stand } from "../terrain/forest/stand.js"; @@ -92,7 +92,10 @@ export class Forest extends Scene { #settled = false; #forest: Awaited> | undefined; #released = false; - #admission: ReturnType | undefined; + #ready = false; + #error = ""; + #settle: ((error?: Error) => void) | undefined; + #timer: ReturnType | undefined; #loading: ReturnType | undefined; /** The kit's world streams while the scene loads, so `ready` waits for real streamed cells. */ @@ -105,12 +108,27 @@ export class Forest extends Scene { this.#player = player; ctx.camera.position.copy(VIEWS.ground.at); ctx.camera.lookAt(VIEWS.ground.look); - const admission = createSpawnReadiness("Forest spawn"); - this.#admission = admission; - void admission.promise.catch((error: unknown) => { + const deadlineAt = performance.now() + 120_000; + let settled = false; + const spawn = new Promise((done, fail) => { + this.#settle = (error) => { + if (settled) return; + settled = true; + if (this.#timer !== undefined) clearTimeout(this.#timer); + if (error === undefined) { + if (!this.#released && !this.#error) this.#ready = true; + done(); + } else fail(error); + }; + }); + this.#timer = setTimeout( + () => this.#fail(ctx, "Forest spawn admission exceeded 120000 ms"), + 120_000, + ); + void spawn.catch((error: unknown) => { this.#fail(ctx, error instanceof Error ? error.message : String(error)); }); - if (ctx.startup.phase !== "ready") ctx.startup.hold("forest-spawn", admission.promise, 120_000); + if (ctx.startup.phase !== "ready") ctx.startup.hold("forest-spawn", spawn, 120_000); let forest: Awaited> | undefined; this.#loading = createLoadingScreen({ ...ctx, @@ -124,8 +142,7 @@ export class Forest extends Scene { (ctx.startup.progress + (required > 0 ? loaded / required : 0)) / 2, ); }, - whenReady: () => - Promise.all([ctx.startup.whenReady(), admission.promise]).then(() => undefined), + whenReady: () => Promise.all([ctx.startup.whenReady(), spawn]).then(() => undefined), }, }); try { @@ -142,6 +159,19 @@ export class Forest extends Scene { } this.#forest = forest; this.#player = player; + // Arm once, after the world is attached: the engine owns readiness, its prewarm owns the draw. + const remaining = deadlineAt - performance.now(); + void ( + remaining > 0 + ? Promise.all([ + forest.world.whenSpawnReady(VIEWS.ground.at, 60, remaining), + forest.world.prewarmed, + ]) + : Promise.reject(new Error("Forest spawn admission exceeded 120000 ms")) + ).then( + () => this.#settle?.(), + (error: unknown) => this.#fail(ctx, error instanceof Error ? error.message : String(error)), + ); ctx.state.set({ propColliders: forest.colliders.active, waterLakes: forest.water.lakes, @@ -180,29 +210,24 @@ export class Forest extends Scene { spawnTerrainRequired: region.requiredTerrainTiles, }); if (this.#released) return; - this.#admission?.observe( - region.ready && this.#forest?.world.stats().pendingPrewarm === 0, - region.failures, - ); ctx.state.set({ - worldReady: this.#admission?.ready ? 1 : 0, - loadingError: this.#admission?.error ?? "", + worldReady: this.#ready ? 1 : 0, + loadingError: this.#error, }); }); } #fail(ctx: Ctx, message: string): void { - if (this.#released || ctx.state.getState().loadingError) return; - this.#admission?.fail(message); - ctx.state.set({ - loadingError: this.#admission?.error ?? message, - worldReady: this.#admission?.ready ? 1 : 0, - }); + if (this.#released || this.#error || this.#ready) return; + const error = message || "Forest spawn failed"; + this.#error = error; + this.#settle?.(new Error(error)); + ctx.state.set({ loadingError: error, worldReady: 0 }); } override exit(): void { this.#released = true; - this.#admission?.cancel(); + this.#settle?.(new Error("Forest spawn cancelled")); this.#loading?.finish(); this.#body?.dispose(); this.#forest?.world.dispose(); @@ -214,11 +239,7 @@ export class Forest extends Scene { const body = this.#body; const player = this.#player; if (body === undefined || player === undefined) return; - if ( - !this.#admission?.ready || - ctx.startup.phase !== "ready" || - ctx.state.getState().loadingError - ) { + if (!this.#ready || ctx.startup.phase !== "ready" || ctx.state.getState().loadingError) { body.velocity.x = 0; body.velocity.z = 0; body.moveAndSlide(dt); diff --git a/examples/strata-terrain-preview/scripts/fixtures/kit-game/Tundra.ts b/examples/strata-terrain-preview/scripts/fixtures/kit-game/Tundra.ts index 81db17cb4..c17e72c7d 100644 --- a/examples/strata-terrain-preview/scripts/fixtures/kit-game/Tundra.ts +++ b/examples/strata-terrain-preview/scripts/fixtures/kit-game/Tundra.ts @@ -7,7 +7,7 @@ import { type ICtx, type IFrameBudgetWindow, Scene } from "@threenative/core"; import { CharacterBody3D, CollisionShape3D, type IPhysicsContext } from "@threenative/physics"; import { Object3D, type PerspectiveCamera, Vector3 } from "three"; -import { createLoadingScreen, createSpawnReadiness } from "../render/loading.js"; +import { createLoadingScreen } from "../render/loading.js"; import { type GameState, initialState } from "../state.js"; import { tundraDaylight } from "../terrain/tundra/sky.js"; import { @@ -337,18 +337,36 @@ export class Tundra extends Scene { #tundra: ITundraWorld | undefined; #stand: IStand | undefined; #released = false; - #admission: ReturnType | undefined; + #ready = false; + #error = ""; + #settle: ((error?: Error) => void) | undefined; + #timer: ReturnType | undefined; #loading: ReturnType | undefined; /** The kit's world streams while the scene loads, so `ready` waits for the cells the spawn needs. */ override async load(ctx: Ctx): Promise { for (const view of VIEW_NAMES) viewWindows.set(view, { frameP95: [], gpuP50: [], gpuP95: [] }); - const admission = createSpawnReadiness("Tundra spawn"); - this.#admission = admission; - void admission.promise.catch((error: unknown) => { + const deadlineAt = performance.now() + 120_000; + let settled = false; + const spawn = new Promise((done, fail) => { + this.#settle = (error) => { + if (settled) return; + settled = true; + if (this.#timer !== undefined) clearTimeout(this.#timer); + if (error === undefined) { + if (!this.#released && !this.#error) this.#ready = true; + done(); + } else fail(error); + }; + }); + this.#timer = setTimeout( + () => this.#fail(ctx, "Tundra spawn admission exceeded 120000 ms"), + 120_000, + ); + void spawn.catch((error: unknown) => { this.#fail(ctx, error instanceof Error ? error.message : String(error)); }); - if (ctx.startup.phase !== "ready") ctx.startup.hold("tundra-spawn", admission.promise, 120_000); + if (ctx.startup.phase !== "ready") ctx.startup.hold("tundra-spawn", spawn, 120_000); let tundra: ITundraWorld | undefined; // A holder rather than a `let`: the progress getter reads it, and the spawn is set once it is known. const spawnView: { at?: Vector3 } = {}; @@ -365,8 +383,7 @@ export class Tundra extends Scene { (ctx.startup.progress + (required > 0 ? loaded / required : 0)) / 2, ); }, - whenReady: () => - Promise.all([ctx.startup.whenReady(), admission.promise]).then(() => undefined), + whenReady: () => Promise.all([ctx.startup.whenReady(), spawn]).then(() => undefined), }, }); try { @@ -389,6 +406,19 @@ export class Tundra extends Scene { } this.#stand = stand; spawnView.at = stand.views.ground.at; + // Arm once, after the world is attached: the engine owns readiness, its prewarm owns the draw. + const remaining = deadlineAt - performance.now(); + void ( + remaining > 0 + ? Promise.all([ + tundra.world.whenSpawnReady(stand.views.ground.at, 60, remaining), + tundra.world.prewarmed, + ]) + : Promise.reject(new Error("Tundra spawn admission exceeded 120000 ms")) + ).then( + () => this.#settle?.(), + (error: unknown) => this.#fail(ctx, error instanceof Error ? error.message : String(error)), + ); const player = new Object3D(); player.position.set(stand.spawn.x, stand.groundY + HALF_HEIGHT + RADIUS + 2.5, stand.spawn.z); this.#player = player; @@ -436,29 +466,24 @@ export class Tundra extends Scene { spawnTerrainRequired: region.requiredTerrainTiles, }); if (this.#released) return; - this.#admission?.observe( - region.ready && tundra.world.stats().pendingPrewarm === 0, - region.failures, - ); ctx.state.set({ - worldReady: this.#admission?.ready ? 1 : 0, - loadingError: this.#admission?.error ?? "", + worldReady: this.#ready ? 1 : 0, + loadingError: this.#error, }); }); } #fail(ctx: Ctx, message: string): void { - if (this.#released || ctx.state.getState().loadingError) return; - this.#admission?.fail(message); - ctx.state.set({ - loadingError: this.#admission?.error ?? message, - worldReady: this.#admission?.ready ? 1 : 0, - }); + if (this.#released || this.#error || this.#ready) return; + const error = message || "Tundra spawn failed"; + this.#error = error; + this.#settle?.(new Error(error)); + ctx.state.set({ loadingError: error, worldReady: 0 }); } override exit(): void { this.#released = true; - this.#admission?.cancel(); + this.#settle?.(new Error("Tundra spawn cancelled")); this.#loading?.finish(); this.#body?.dispose(); if (this.#tundra) disposeTundra(this.#tundra); @@ -469,11 +494,7 @@ export class Tundra extends Scene { const player = this.#player; const stand = this.#stand; if (body === undefined || player === undefined || stand === undefined) return; - if ( - !this.#admission?.ready || - ctx.startup.phase !== "ready" || - ctx.state.getState().loadingError - ) { + if (!this.#ready || ctx.startup.phase !== "ready" || ctx.state.getState().loadingError) { body.velocity.x = 0; body.velocity.z = 0; body.moveAndSlide(dt); diff --git a/examples/strata-terrain-preview/src/game.ts b/examples/strata-terrain-preview/src/game.ts index 9c1ec7ba1..7c11abb3f 100644 --- a/examples/strata-terrain-preview/src/game.ts +++ b/examples/strata-terrain-preview/src/game.ts @@ -17,11 +17,7 @@ import { Vector3, } from "three"; import { BIOMES, type WorldName } from "./render/biomes.js"; -import { - createLoadingScreen, - createSpawnReadiness, - spawnReadinessSnapshot, -} from "./render/loading.js"; +import { createLoadingScreen, spawnReadinessSnapshot } from "./render/loading.js"; import { createOcean, createWaterMesh } from "./render/ocean.js"; import { type ICanopyComparison, loadPack, loadSkyLight } from "./render/pack.js"; import { loadPreparedProps } from "./render/prepared.js"; @@ -309,7 +305,50 @@ function terrainScene(world: WorldName): new () => Scene void = () => undefined; + let rejectSpawn: (reason: Error) => void = () => undefined; + const spawnPromise = new Promise((done, failed) => { + resolveSpawn = done; + rejectSpawn = failed; + }); + function settleSpawn(reason?: Error): void { + if (spawnSettled || capturingSpawnFailure) return; + spawnSettled = true; + clearTimeout(spawnTimer); + if (reason === undefined) { + spawnReady = true; + resolveSpawn(); + } else { + rejectSpawn(reason); + } + } + function failSpawn(reason: unknown): void { + if (spawnSettled || capturingSpawnFailure) return; + const message = + (reason instanceof Error ? reason.message : String(reason)) || "Strata spawn failed"; + capturingSpawnFailure = true; + try { + captureSpawnFailure(message); + } catch { + // A diagnostic must never replace the admission failure or leave its promise pending. + } + capturingSpawnFailure = false; + spawnError = message; + settleSpawn(new Error(message)); + ctx.state.set({ worldReady: spawnReady, loadingError: spawnError }); + } + const spawnTimer: ReturnType = setTimeout( + () => failSpawn("Strata spawn admission exceeded 120000 ms"), + 120_000, + ); function captureSpawnFailure(reason: string): void { console.error( `TN_STRATA_SPAWN_FAILURE ${JSON.stringify( @@ -319,7 +358,7 @@ function terrainScene(world: WorldName): new () => Scene Scene { - admission.fail(reason); - ctx.state.set({ worldReady: admission.ready, loadingError: admission.error }); - }; if (ctx.startup.phase !== "ready") ctx.startup.hold( "strata-spawn", - // Core polls this once per rendered frame, so the gate settles under a playtest's - // deterministic clock, which advances no fixed step while startup is held. - () => { - const region = props?.readinessAt(spawn); - admission.observe(region?.ready === true, region?.failures ?? 0); - return admission.ready || admission.error !== ""; - }, + spawnPromise, 120_000, () => completedPropsWork + @@ -361,7 +390,7 @@ function terrainScene(world: WorldName): new () => Scene - Promise.all([ctx.startup.whenReady(), admission.promise]).then(() => undefined), + Promise.all([ctx.startup.whenReady(), spawnPromise]).then(() => undefined), }, }); ctx.afterPhysics(() => loading.update()); @@ -612,7 +641,7 @@ function terrainScene(world: WorldName): new () => Scene { released = true; textureUploads.abort(new Error("Strata scene released during texture preparation.")); - admission.cancel(); + settleSpawn(new Error("Strata spawn cancelled")); loading.finish(); surfacesDispose?.(); flat.dispose(); @@ -770,6 +799,21 @@ function terrainScene(world: WorldName): new () => Scene stream.whenSpawnReady(spawn, 60, remaining)), + ...props.worlds.map((stream) => stream.prewarmed), + ]).then( + () => settleSpawn(), + (reason) => failSpawn(reason), + ); + } const alpineCrags = world === "alpine" @@ -942,7 +986,10 @@ function terrainScene(world: WorldName): new () => Scene { propsSettled = true; - if (!released) failSpawn(error); + if (!released) { + failSpawn(error); + ctx.state.set({ worldReady: spawnReady, loadingError: spawnError }); + } }, ); } @@ -962,11 +1009,10 @@ function terrainScene(world: WorldName): new () => Scene void, -) { - if (!label.trim() || !Number.isFinite(budgetMs) || budgetMs <= 0) - throw new Error("Spawn readiness requires a label and positive budget."); - let ready = false; - let error = ""; - let cancelled = false; - let capturingFailure = false; - let resolve: () => void = () => undefined; - let reject: (reason: Error) => void = () => undefined; - const promise = new Promise((done, fail) => { - resolve = done; - reject = fail; - }); - function fail(reason: unknown): void { - if (ready || error || cancelled || capturingFailure) return; - const message = - (reason instanceof Error ? reason.message : String(reason)) || `${label} failed`; - capturingFailure = true; - try { - beforeFailure?.(message); - } catch { - // A diagnostic must never replace the admission failure or leave its promise pending. - } - capturingFailure = false; - error = message; - clearTimeout(timer); - reject(new Error(error)); - } - const timer = setTimeout(() => fail(`${label} admission exceeded ${budgetMs} ms`), budgetMs); - return { - promise, - fail, - get ready() { - return ready; - }, - get error() { - return error; - }, - observe(covered: boolean, failures = 0): void { - if (ready || error || cancelled || capturingFailure) return; - if (failures > 0) { - fail(`${label} assets failed to load`); - return; - } - if (covered) { - ready = true; - clearTimeout(timer); - resolve(); - } - }, - cancel(): void { - if (capturingFailure) return; - clearTimeout(timer); - if (!ready && !error && !cancelled) { - cancelled = true; - reject(new Error(`${label} cancelled`)); - } - }, - }; -} - /* BEGIN THREENATIVE LOADING APPEARANCE */ /** Edit these source constants for the starter's loading look. */ export const loading = { diff --git a/packages/terrain/__tests__/prop-streaming.spec.ts b/packages/terrain/__tests__/prop-streaming.spec.ts index e96bb294b..13d158de0 100644 --- a/packages/terrain/__tests__/prop-streaming.spec.ts +++ b/packages/terrain/__tests__/prop-streaming.spec.ts @@ -11,10 +11,7 @@ import { Vector3, } from "three"; import { expect, it, vi } from "vitest"; -import { - createSpawnReadiness, - spawnReadinessSnapshot, -} from "../../../examples/strata-terrain-preview/src/render/loading.js"; +import { spawnReadinessSnapshot } from "../../../examples/strata-terrain-preview/src/render/loading.js"; import { type IPropPreparationProgress, createStreamedProps, @@ -77,35 +74,23 @@ it("reports an outstanding world load before the factory returns or attaches it" expect(progress?.phase).toBe("world-load"); expect(progress?.bucket).toBe("horizon:true"); expect(progress?.worlds).toHaveLength(0); - vi.useFakeTimers(); - let receipt: ReturnType | undefined; - const spawn = createSpawnReadiness("Controlled spawn", 100, (reason) => { - receipt = spawnReadinessSnapshot({ - reason, - atMs: performance.now(), - world: "forest", - sceneUuid: "controlled-scene", - released: false, - gate: spawn, - stage: "streaming", - propsSettled: false, - preparation: progress, - assets: assets.progress, - startup: { phase: "collapsing", progress: 0.95, compileSettled: true, timeline: {} }, - }); + const receipt = spawnReadinessSnapshot({ + reason: "world load still outstanding before the factory returns or attaches", + atMs: performance.now(), + world: "forest", + sceneUuid: "controlled-scene", + released: false, + gate: { ready: false, error: "" }, + stage: "streaming", + propsSettled: false, + preparation: progress, + assets: assets.progress, + startup: { phase: "collapsing", progress: 0.95, compileSettled: true, timeline: {} }, }); - const rejection = expect(spawn.promise).rejects.toThrow("admission exceeded 100 ms"); - try { - await vi.advanceTimersByTimeAsync(100); - await rejection; - expect(receipt?.gate).toEqual({ ready: false, error: "" }); - expect(receipt?.preparation.phase).toBe("world-load"); - expect(receipt?.preparation.worldCount).toBe(0); - expect(receipt?.released).toBe(false); - } finally { - spawn.cancel(); - vi.useRealTimers(); - } + expect(receipt.gate).toEqual({ ready: false, error: "" }); + expect(receipt.preparation.phase).toBe("world-load"); + expect(receipt.preparation.worldCount).toBe(0); + expect(receipt.released).toBe(false); release(); streamed = await work; expect(progress?.phase).toBe("complete"); @@ -113,7 +98,7 @@ it("reports an outstanding world load before the factory returns or attaches it" expect(streamed?.worlds[0]?.parent).toBeNull(); expect(streamed?.worlds[0]?.released).toBe(false); expect(progress?.prewarmedWorlds).toBe(0); - expect(receipt?.preparation.worldCount).toBe(0); + expect(receipt.preparation.worldCount).toBe(0); } finally { release(); streamed ??= await work; diff --git a/packages/terrain/__tests__/strata-loading.spec.ts b/packages/terrain/__tests__/strata-loading.spec.ts index d80b37831..2b039c501 100644 --- a/packages/terrain/__tests__/strata-loading.spec.ts +++ b/packages/terrain/__tests__/strata-loading.spec.ts @@ -4,18 +4,112 @@ import { type CanvasTexture, type Mesh, type MeshBasicMaterial, + Object3D, OrthographicCamera, PerspectiveCamera, Scene, + Vector3, } from "three"; import ts from "typescript"; import { expect, it, vi } from "vitest"; import { createLoadingScreen, - createSpawnReadiness, spawnReadinessSnapshot, } from "../../../examples/strata-terrain-preview/src/render/loading.js"; +const GAME_URL = new URL("../../../examples/strata-terrain-preview/src/game.ts", import.meta.url); +const GATE_NAMES = [ + "deadlineAt", + "spawnReady", + "spawnError", + "spawnSettled", + "capturingSpawnFailure", + "resolveSpawn", + "rejectSpawn", + "spawnPromise", + "settleSpawn", + "failSpawn", + "spawnTimer", +] as const; + +/** Pull named declarations straight out of the production `enter()` so the tests drive real code. */ +function gameDeclarations(names: readonly string[]): string { + const source = ts.createSourceFile( + "game.ts", + readFileSync(GAME_URL, "utf8"), + ts.ScriptTarget.Latest, + true, + ); + const wanted = new Set(names); + const found = new Map(); + function visit(node: ts.Node): void { + if (ts.isVariableDeclaration(node)) { + const name = node.name.getText(source); + if (wanted.has(name)) found.set(name, node.parent.parent.getText(source)); + } else if (ts.isFunctionDeclaration(node) && node.name !== undefined) { + const name = node.name.getText(source); + if (wanted.has(name)) found.set(name, node.getText(source)); + } + ts.forEachChild(node, visit); + } + visit(source); + return names + .map((name) => { + const text = found.get(name); + if (text === undefined) throw new Error(`Missing production declaration ${name}`); + return text; + }) + .join("\n"); +} + +interface ISpawnGate { + readonly spawn: Promise; + readonly state: { worldReady: boolean; loadingError: string }; + error(): string; + fail(reason: unknown): void; + ready(): boolean; + settle(error?: Error): void; +} + +/** Run the production spawn gate in a fresh context; `capture` receives each failure receipt. */ +function spawnGate(capture: (reason: string) => void = () => undefined): ISpawnGate { + vi.useFakeTimers(); + const state = { worldReady: false, loadingError: "" }; + const scope = { + setTimeout, + clearTimeout, + performance, + Error, + captureSpawnFailure: capture, + spawnState: state, + ctx: { + state: { + set: (value: { worldReady: boolean; loadingError: string }) => Object.assign(state, value), + }, + }, + exports: {} as { Gate?: new () => ISpawnGate }, + }; + runInNewContext( + ts.transpileModule( + `export class Gate { + constructor() { + ${gameDeclarations(GATE_NAMES)} + this.settle = settleSpawn; + this.fail = failSpawn; + this.spawn = spawnPromise; + this.ready = () => spawnReady; + this.error = () => spawnError; + this.state = spawnState; + } + }`, + { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }, + ).outputText, + scope, + ); + if (scope.exports.Gate === undefined) throw new Error("Production spawn gate did not load"); + return new scope.exports.Gate(); +} + function host(ready: Promise) { return { camera: new PerspectiveCamera(), @@ -35,27 +129,15 @@ function host(ready: Promise) { it.each(["attached", "cancelled", "released"] as const)( "defers hidden world draws until prepared streams attach: %s", async (outcome) => { - const source = ts.createSourceFile( - "game.ts", - readFileSync( - new URL("../../../examples/strata-terrain-preview/src/game.ts", import.meta.url), - "utf8", - ), - ts.ScriptTarget.Latest, - true, - ); - let declaration = ""; - function visit(node: ts.Node): void { - if (ts.isVariableDeclaration(node) && node.name.getText(source) === "buildProps") - declaration = node.getText(source); - ts.forEachChild(node, visit); - } - visit(source); - if (!declaration) throw new Error("Missing production Strata preparation controller"); + const declarations = gameDeclarations(["buildProps"]); const controllerHost = host(new Promise(() => undefined)); controllerHost.canvasLayer.opaque = true; controllerHost.canvasLayer.renderWorldDuringStartup = false; - const world = new Scene(); + const world = { + prewarmed: Promise.resolve(), + update: () => undefined, + whenSpawnReady: () => Promise.resolve({}), + }; const attachments: boolean[] = []; let entered: () => void = () => undefined; const preparing = new Promise((resolve) => { @@ -69,6 +151,10 @@ it.each(["attached", "cancelled", "released"] as const)( const parts = new Map(); const scope = { exports: {} as { Controller?: new () => { run(): Promise } }, + performance, + deadlineAt: performance.now() + 120_000, + settleSpawn: () => undefined, + failSpawn: () => undefined, ctx: { ...controllerHost, assets: undefined, @@ -90,6 +176,7 @@ it.each(["attached", "cancelled", "released"] as const)( propParts: parts, world: "forest", data: { size: 512 }, + spawn: { x: 0, y: 0, z: 0 }, biome: {}, fallbackSaplingHeight: undefined, scatter: { placements: [], counts: {} }, @@ -135,7 +222,7 @@ it.each(["attached", "cancelled", "released"] as const)( }; runInNewContext( ts.transpileModule( - `export class Controller { #surfaces; async run() { const ${declaration}; await buildProps(); } }`, + `export class Controller { #surfaces; async run() { ${declarations}; await buildProps(); } }`, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }, ).outputText, scope, @@ -143,7 +230,12 @@ it.each(["attached", "cancelled", "released"] as const)( if (!scope.exports.Controller) throw new Error("Production preparation controller did not load"); const work = new scope.exports.Controller().run(); - await preparing; + await Promise.race([ + preparing, + work.then(() => { + throw Error("preparation did not enter"); + }), + ]); try { expect(scope.completedPropsWork).toBe(55); expect(controllerHost.canvasLayer.keepWorldRendering).toBe(false); @@ -359,108 +451,222 @@ it("releases its layer only after the combined spawn and startup promise resolve }); it("latches playable coverage so later spawn eviction never disables controls", async () => { - const spawn = createSpawnReadiness("Strata spawn"); - expect(spawn.ready).toBe(false); - spawn.observe(false); - expect(spawn.ready).toBe(false); - spawn.observe(true); - await spawn.promise; - spawn.observe(false, 1); - expect(spawn.ready).toBe(true); - expect(spawn.error).toBe(""); - spawn.cancel(); + const gate = spawnGate(); + let settled = false; + void gate.spawn.then( + () => { + settled = true; + }, + () => { + settled = true; + }, + ); + expect(gate.ready()).toBe(false); + await Promise.resolve(); + expect(settled).toBe(false); + gate.settle(); + await gate.spawn; + gate.fail("late failure"); + expect(gate.ready()).toBe(true); + expect(gate.error()).toBe(""); + gate.settle(new Error("Strata spawn cancelled")); + vi.useRealTimers(); }); it("rejects a timed out spawn and never treats later coverage as playable", async () => { - vi.useFakeTimers(); - const spawn = createSpawnReadiness("Forest spawn", 100); - const rejection = expect(spawn.promise).rejects.toThrow("Forest spawn admission exceeded 100 ms"); - await vi.advanceTimersByTimeAsync(100); + const gate = spawnGate(); + const rejection = expect(gate.spawn).rejects.toThrow("Strata spawn admission exceeded 120000 ms"); + await vi.advanceTimersByTimeAsync(120_000); await rejection; - spawn.observe(true); - expect(spawn.ready).toBe(false); - expect(spawn.error).toContain("exceeded"); - spawn.cancel(); + gate.settle(); + expect(gate.ready()).toBe(false); + expect(gate.error()).toContain("exceeded"); vi.useRealTimers(); }); it("captures the pending gate before timeout latches or rejects, once per failure", async () => { - vi.useFakeTimers(); const events: string[] = []; - const spawn = createSpawnReadiness("Forest spawn", 100, (reason) => { - events.push(`capture:${spawn.ready}:${spawn.error}:${reason}`); + const gate = spawnGate((reason) => { + events.push(`capture:${gate.ready()}:${gate.error()}:${reason}`); }); - const rejection = spawn.promise.catch((error: Error) => events.push(`rejected:${error.message}`)); - try { - await vi.advanceTimersByTimeAsync(100); - await rejection; - spawn.observe(true); - spawn.fail("late failure"); - spawn.cancel(); - expect(events).toEqual([ - "capture:false::Forest spawn admission exceeded 100 ms", - "rejected:Forest spawn admission exceeded 100 ms", - ]); - expect(spawn.ready).toBe(false); - expect(vi.getTimerCount()).toBe(0); - } finally { - spawn.cancel(); - vi.useRealTimers(); - } + const rejection = gate.spawn.catch((error: Error) => events.push(`rejected:${error.message}`)); + await vi.advanceTimersByTimeAsync(120_000); + await rejection; + gate.settle(); + gate.fail("late failure"); + gate.settle(new Error("Strata spawn cancelled")); + expect(events).toEqual([ + "capture:false::Strata spawn admission exceeded 120000 ms", + "rejected:Strata spawn admission exceeded 120000 ms", + ]); + expect(gate.ready()).toBe(false); + expect(vi.getTimerCount()).toBe(0); + vi.useRealTimers(); }); it("keeps the original admission failure when its diagnostic capture throws", async () => { - vi.useFakeTimers(); const capture = vi.fn(() => { throw new Error("diagnostic observer failed"); }); - const spawn = createSpawnReadiness("Forest spawn", 100, capture); - const rejection = expect(spawn.promise).rejects.toThrow("Forest spawn assets failed to load"); - try { - spawn.observe(false, 1); - await rejection; - expect(capture).toHaveBeenCalledOnce(); - expect(spawn.error).toBe("Forest spawn assets failed to load"); - expect(vi.getTimerCount()).toBe(0); - } finally { - spawn.cancel(); - vi.useRealTimers(); - } + const gate = spawnGate(capture); + const rejection = expect(gate.spawn).rejects.toThrow("Strata spawn assets failed to load"); + gate.fail(new Error("Strata spawn assets failed to load")); + await rejection; + expect(capture).toHaveBeenCalledOnce(); + expect(gate.error()).toBe("Strata spawn assets failed to load"); + expect(vi.getTimerCount()).toBe(0); + vi.useRealTimers(); }); it.each(["observe", "cancel"])( "keeps capture reentry through %s from changing failure settlement", async (action) => { - const spawn = createSpawnReadiness("Controlled spawn", 100, () => { - if (action === "observe") spawn.observe(true); - else spawn.cancel(); + const gate = spawnGate(() => { + if (action === "observe") gate.settle(); + else gate.settle(new Error("Strata spawn cancelled")); }); - const rejection = expect(spawn.promise).rejects.toThrow("original admission failure"); - spawn.fail("original admission failure"); + const rejection = expect(gate.spawn).rejects.toThrow("original admission failure"); + gate.fail("original admission failure"); await rejection; - expect(spawn.ready).toBe(false); - expect(spawn.error).toBe("original admission failure"); - spawn.cancel(); + expect(gate.ready()).toBe(false); + expect(gate.error()).toBe("original admission failure"); + vi.useRealTimers(); }, ); it("cancels a pending scene without leaving its readiness promise or timer behind", async () => { - vi.useFakeTimers(); - const spawn = createSpawnReadiness("Forest spawn"); - const rejection = expect(spawn.promise).rejects.toThrow("Forest spawn cancelled"); - spawn.cancel(); + const gate = spawnGate(); + const rejection = expect(gate.spawn).rejects.toThrow("Strata spawn cancelled"); + gate.settle(new Error("Strata spawn cancelled")); await rejection; expect(vi.getTimerCount()).toBe(0); - expect(spawn.ready).toBe(false); + expect(gate.ready()).toBe(false); vi.useRealTimers(); }); it("keeps even an empty-message rejection failed after late coverage", async () => { - const spawn = createSpawnReadiness("Strata spawn"); - const rejection = expect(spawn.promise).rejects.toThrow("Strata spawn failed"); - spawn.fail(new Error()); + const gate = spawnGate(); + const rejection = expect(gate.spawn).rejects.toThrow("Strata spawn failed"); + gate.fail(new Error()); await rejection; - spawn.observe(true); - expect(spawn.ready).toBe(false); - spawn.cancel(); + gate.settle(); + expect(gate.ready()).toBe(false); + vi.useRealTimers(); +}); + +it("updates ctx state immediately when the spawn times out before any render frame", async () => { + const gate = spawnGate(); + const rejection = gate.spawn.catch(() => undefined); + expect(gate.state.loadingError).toBe(""); + expect(gate.state.worldReady).toBe(false); + await vi.advanceTimersByTimeAsync(120_000); + await rejection; + expect(gate.state.loadingError).toBe("Strata spawn admission exceeded 120000 ms"); + expect(gate.state.worldReady).toBe(false); + vi.useRealTimers(); +}); + +const KIT_GATES = ["Alpine", "Coastal", "Desert", "Forest", "Tundra"] as const; +/** Module constants a kit gate reads while it loads; the rest of each fixture stays out of the VM. */ +const KIT_CONSTANTS = new Set(["DRIVE_SECONDS", "viewWindows"]); + +interface IKitProbe { + ready(): boolean; + error(): string; + settle(error?: Error): void; + fail(ctx: unknown, message: string): void; +} + +interface IKitGate { + load(ctx: unknown): Promise; + testProbe(): IKitProbe; +} + +/** Run a starter-kit fixture's real gate class in a fresh context, with its spawn rejecting as `reason`. */ +function kitGate(kit: string, reason: string): IKitGate { + const url = new URL( + `../../../examples/strata-terrain-preview/scripts/fixtures/kit-game/${kit}.ts`, + import.meta.url, + ); + const source = ts.createSourceFile( + `${kit}.ts`, + readFileSync(url, "utf8"), + ts.ScriptTarget.Latest, + true, + ); + const constants: string[] = []; + let gate = ""; + function visit(node: ts.Node): void { + if (ts.isClassDeclaration(node) && node.name?.text === kit) gate = node.getText(source); + else if (ts.isVariableDeclaration(node) && KIT_CONSTANTS.has(node.name.getText(source))) + constants.push(node.parent.parent.getText(source)); + ts.forEachChild(node, visit); + } + visit(source); + if (gate === "") throw new Error(`Missing ${kit} kit gate class`); + // The probe sits inside the class to reach private gate state; the gate code itself is untouched. + const probe = + "testProbe() { return { ready: () => this.#ready, error: () => this.#error, settle: (error) => this.#settle?.(error), fail: (ctx, message) => this.#fail(ctx, message) }; }"; + const withProbe = `${gate.slice(0, gate.lastIndexOf("}"))}${probe}\n}`; + const compiled = ts.transpileModule([...constants, withProbe].join("\n"), { + compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }, + }).outputText; + const scope = { + exports: {} as Record IKitGate>, + Scene: class {}, + // Each kit's static `initialState` field reads this global; the gate under test never uses it. + initialState: {}, + VIEWS: { ground: { at: new Vector3(), look: new Vector3() } }, + VIEW_NAMES: [], + stand: { spawn: new Vector3(), groundY: 0 }, + HALF_HEIGHT: 0, + RADIUS: 0, + Object3D, + Vector3, + // The host Error keeps `error instanceof Error` true for the rejection the spawn raises. + Error, + setTimeout: () => 1, + clearTimeout: () => undefined, + performance: { now: () => 0 }, + createLoadingScreen: () => ({ update: () => undefined, finish: () => undefined }), + [`add${kit}`]: () => Promise.reject(new Error(reason)), + }; + runInNewContext(compiled, scope); + const Gate = scope.exports[kit]; + if (Gate === undefined) throw new Error(`Kit gate ${kit} did not load`); + return new Gate(); +} + +function kitContext() { + return { + startup: { phase: "loading", progress: 0, hold: vi.fn(), whenReady: () => Promise.resolve() }, + state: { set: vi.fn(), getState: () => ({ loadingError: "" }) }, + camera: new PerspectiveCamera(), + }; +} + +it.each(KIT_GATES)("%s normalizes an empty spawn failure to its kit label", async (kit) => { + const ctx = kitContext(); + const gate = kitGate(kit, ""); + await gate.load(ctx); + expect(gate.testProbe().error()).toBe(`${kit} spawn failed`); + expect(ctx.state.set).toHaveBeenLastCalledWith({ + loadingError: `${kit} spawn failed`, + worldReady: 0, + }); +}); + +it.each(KIT_GATES)("%s keeps an empty spawn failure latched through late success", async (kit) => { + const ctx = kitContext(); + const gate = kitGate(kit, ""); + await gate.load(ctx); + const probe = gate.testProbe(); + probe.settle(); + expect(probe.ready()).toBe(false); + probe.fail(ctx, "A later failure"); + expect(probe.error()).toBe(`${kit} spawn failed`); + expect(ctx.state.set).toHaveBeenLastCalledWith({ + loadingError: `${kit} spawn failed`, + worldReady: 0, + }); }); From 0f8d7a3282919c99b2a38a1d8716737d04a5c619 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sat, 10 Oct 2026 23:45:50 -0700 Subject: [PATCH 031/210] docs(strata): record hardware recovery and incomplete preview qualification --- ...-stops-reimplementing-engine-mechanisms.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md b/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md index a5890a60c..71865896f 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-591-the-preview-stops-reimplementing-engine-mechanisms.md @@ -78,6 +78,26 @@ both material paths. also passed (other nine tests skipped). Terrain typecheck and direct preview `tsc --noEmit` passed. Scoped Biome and `git diff --check` passed. The web playtest, all-kit runtime captures and caller full native-matrix proof remain unverified; this box stays open. + A subsequent `test:terrain:web` attempt exited 1: framework startup reached ready, but the + headed WebGPU recipe selected SwiftShader, then reported device loss and a blank forest + capture. Eleven assertions failed; this is not render or performance evidence. The captured + adapter was read from that attempt's `capture.json`; the retained run log is + `591-preview-web.out` in the coordinator cache. + Environment diagnosis found loaded NVIDIA kernel driver 615.71.09 against NVML userspace + 615.78.08 (`nvidia-smi` exit 18). The laptop refused offload at 99°C. Hardware qualification + was restored with process-local matching cached libraries: `nvidia-smi` reports + RTX 2080 / 615.71.09. A fresh headed `world-terrain-inline-hash` selected case passed + in 4.8 s on NVIDIA/Turing, with no page or GPU validation errors and a nonuniform capture + (`packages/runtime-native/artifacts/strata591-matching-driver-web/report.json`). + The harness exited 2 because 102 unselected cases are blocked; this proves only the selected + case. No system driver change, assertion change or budget change was needed. Full preview + and all-kit qualification remain open. + The matching-driver full preview rerun reached visible forest, coastal and alpine captures, + then hit the external 720 s deadline (exit 124 / SIGTERM) before a final assertion report. + It does not prove runtime budgets or all-kit success. The fresh forest smoke screenshot is + [attached to the EPIC PR](https://github.com/ThreeNativeHQ/threenative-engine/pull/483#issuecomment-6106317255), + labelled with performance and visual comparison still pending. Documentation checks passed: + 3,057 links across 1,360 Markdown files, plus all 252 tests across the six prose-lane specs. - Native availability corrected: the linked checkout has no `third_party/` or `build/` of its own, and the primary owning checkout already ships `packages/runtime-native/build/tn-linux-wgpu/mystral` (also `tn-linux-jsprofile` and From 97fad60ca707725bad71e6aa5548207bbe0dc42b Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sun, 11 Oct 2026 00:05:05 -0700 Subject: [PATCH 032/210] feat(terrain): move starter heightmaps to byte-stable RAW16 sidecars --- ...terrain-package-ships-code-not-kit-data.md | 15 +- packages/terrain/__tests__/alpine-kit.spec.ts | 2 +- .../terrain/__tests__/coastal-kit.spec.ts | 2 +- packages/terrain/__tests__/desert-kit.spec.ts | 2 +- .../terrain/__tests__/starter-kit.spec.ts | 74 +- packages/terrain/__tests__/tundra-kit.spec.ts | 2 +- packages/terrain/starter/alpine/bake.mjs | 14 +- packages/terrain/starter/alpine/heightmap.r16 | Bin 0 -> 132098 bytes packages/terrain/starter/alpine/recipe.json | 66054 +--------------- packages/terrain/starter/coastal/bake.mjs | 14 +- .../terrain/starter/coastal/heightmap.r16 | Bin 0 -> 132098 bytes packages/terrain/starter/coastal/recipe.json | 66054 +--------------- packages/terrain/starter/desert/bake.mjs | 14 +- packages/terrain/starter/desert/heightmap.r16 | Bin 0 -> 132098 bytes packages/terrain/starter/desert/recipe.json | 66054 +--------------- packages/terrain/starter/forest/bake.mjs | 14 +- packages/terrain/starter/forest/heightmap.r16 | Bin 0 -> 132098 bytes packages/terrain/starter/forest/recipe.json | 4545 +- packages/terrain/starter/tundra/bake.mjs | 14 +- packages/terrain/starter/tundra/heightmap.r16 | Bin 0 -> 132098 bytes packages/terrain/starter/tundra/recipe.json | 66054 +--------------- 21 files changed, 267 insertions(+), 268661 deletions(-) create mode 100644 packages/terrain/starter/alpine/heightmap.r16 create mode 100644 packages/terrain/starter/coastal/heightmap.r16 create mode 100644 packages/terrain/starter/desert/heightmap.r16 create mode 100644 packages/terrain/starter/forest/heightmap.r16 create mode 100644 packages/terrain/starter/tundra/heightmap.r16 diff --git a/docs/PRDs/strata-terrain-hardening/PRD-589-the-terrain-package-ships-code-not-kit-data.md b/docs/PRDs/strata-terrain-hardening/PRD-589-the-terrain-package-ships-code-not-kit-data.md index 8bb9ea7d3..dc8ba13ea 100644 --- a/docs/PRDs/strata-terrain-hardening/PRD-589-the-terrain-package-ships-code-not-kit-data.md +++ b/docs/PRDs/strata-terrain-hardening/PRD-589-the-terrain-package-ships-code-not-kit-data.md @@ -1,6 +1,6 @@ # PRD-589 — `@threenative/terrain` ships code, not 33 MB of kit data -**Status:** NOT STARTED +**Status:** PARTIAL **Priority:** P2 — Every `npm i @threenative/terrain` downloads 32.6 MB of kit data and four 1.3 MB recipes, whether or not the game uses a kit (AC-1, AC-2). **Complexity:** 4 (MEDIUM); risk override: none **Owner:** ThreeNative maintainers @@ -35,7 +35,7 @@ example. Fix the generator string, the peer-range sync and the CHARTER row. ## Acceptance Criteria - [ ] AC-1 [local]: The packed tarball is under 2 MB, enforced by a cap in `pnpm budgets`. proof: `pnpm budgets` — Evidence: pending. -- [ ] AC-2 [local]: Every `recipe.json` is under 200 KB, and each kit bake's output bytes are unchanged by the sidecar move. proof: `pnpm exec vitest run packages/terrain/__tests__/starter-kit.spec.ts` with a golden hash — Evidence: pending. +- [x] AC-2 [local]: Every `recipe.json` is under 200 KB, and each kit bake's output bytes are unchanged by the sidecar move. proof: `pnpm exec vitest run packages/terrain/__tests__/starter-kit.spec.ts` with a golden hash — Evidence: 27 tests pass across five kit specs (parent run, 20.55 s); all 79 old/new bake files are byte-identical (7 s). - [ ] AC-3 [local]: A scaffolded game that asks for the forest kit gets its recipe and assets, and the kit playtest passes. proof: `KIT=forest pnpm --filter strata-terrain-preview test:kit` — Evidence: pending. - [ ] AC-4 [local]: No asset directory without a reference remains. proof: an asset-reference spec — Evidence: pending. - [ ] AC-5 [local]: `prepare-release` syncs terrain's peer ranges, and the CHARTER closed-list row names the terrain allowance. proof: `pnpm check:docs` and the package-lists drift spec — Evidence: pending. @@ -43,9 +43,14 @@ example. Fix the generator string, the peer-range sync and the CHARTER row. ## Execution Phases #### Phase 1: recipes shrink, bytes hold -**Status:** NOT STARTED -**Files:** `packages/terrain/starter/*/recipe.json`, sidecar `.r16` files, `starter-kit.spec.ts` -- [ ] Recipes under 200 KB, same bake bytes. proof: `starter-kit.spec.ts` +**Status:** COMPLETE +**Files:** `packages/terrain/starter/*/recipe.json`, sidecar `.r16` files, kit specs and bake loaders +- [x] Recipes under 200 KB, same bake bytes. proof: `starter-kit.spec.ts` + - All five recipe JSON files are 2,253–5,359 B; each little-endian RAW16 sidecar is 132,098 B. Existing `decodeRAW16` hydrates values in the Node bake loaders; pure `Terrain` owns no file I/O. The authored 0.1 m precision limit is explicit in each loader. + - Self-contained pinned SHA256 tests prove decoded Float32 heights and evaluated height grids equal the original inline recipes. Parent full focused run: all 27 tests pass across `starter-kit`, `alpine-kit`, `coastal-kit`, `desert-kit` and `tundra-kit` (20.55 s). + - Parent complete bake comparison against `e12a5d578`: all ten bakes exit 0; every emitted file matches by path and full-byte SHA256 (79 files: alpine 15, coastal 12, desert 15, forest 22, tundra 15; 7 s). No bake arithmetic, seed or placement changed. + - Fixture copy lists include the sidecars; alpine first reproduced `ENOENT heightmap.r16`, then all four biome specs passed. The game harness already copies kit directories recursively. + - Two stale forest assertions were aligned with cooker commit `4cf0664e7`: historical comparison proves only six model path aliases changed in the manifest. The combined GLB (24,641,592 B) and streamed package (24,073,955 B) each retain the 25 MiB cap. All other golden hashes remain unchanged. Scoped Biome and diff checks pass. #### Phase 2: kits leave the tarball **Status:** NOT STARTED diff --git a/packages/terrain/__tests__/alpine-kit.spec.ts b/packages/terrain/__tests__/alpine-kit.spec.ts index 360248ef5..28dceacf8 100644 --- a/packages/terrain/__tests__/alpine-kit.spec.ts +++ b/packages/terrain/__tests__/alpine-kit.spec.ts @@ -13,7 +13,7 @@ const starterAssets = join(packageRoot, "starter-assets"); /** Copies the alpine kit into a temp directory whose node_modules resolves the workspace package. */ function kitInTempDir(): string { const dir = makeTempDirSync("terrain-alpine-kit-"); - for (const name of ["bake.mjs", "recipe.json", "assets.json", "surface.json"]) + for (const name of ["bake.mjs", "recipe.json", "assets.json", "surface.json", "heightmap.r16"]) copyFileSync(join(kit, name), join(dir, name)); mkdirSync(join(dir, "node_modules", "@threenative"), { recursive: true }); symlinkSync(packageRoot, join(dir, "node_modules", "@threenative", "terrain"), "dir"); diff --git a/packages/terrain/__tests__/coastal-kit.spec.ts b/packages/terrain/__tests__/coastal-kit.spec.ts index bfefea2ee..f845010ea 100644 --- a/packages/terrain/__tests__/coastal-kit.spec.ts +++ b/packages/terrain/__tests__/coastal-kit.spec.ts @@ -13,7 +13,7 @@ const starterAssets = join(packageRoot, "starter-assets"); /** Copies the coastal kit into a temp directory whose node_modules resolves the workspace package. */ function kitInTempDir(): string { const dir = makeTempDirSync("terrain-coastal-kit-"); - for (const name of ["bake.mjs", "recipe.json", "assets.json", "surface.json"]) + for (const name of ["bake.mjs", "recipe.json", "assets.json", "surface.json", "heightmap.r16"]) copyFileSync(join(kit, name), join(dir, name)); mkdirSync(join(dir, "node_modules", "@threenative"), { recursive: true }); symlinkSync(packageRoot, join(dir, "node_modules", "@threenative", "terrain"), "dir"); diff --git a/packages/terrain/__tests__/desert-kit.spec.ts b/packages/terrain/__tests__/desert-kit.spec.ts index 49af0ef89..e2c17fea3 100644 --- a/packages/terrain/__tests__/desert-kit.spec.ts +++ b/packages/terrain/__tests__/desert-kit.spec.ts @@ -13,7 +13,7 @@ const starterAssets = join(packageRoot, "starter-assets"); /** Copies the desert kit into a temp directory whose node_modules resolves the workspace package. */ function kitInTempDir(): string { const dir = makeTempDirSync("terrain-desert-kit-"); - for (const name of ["bake.mjs", "recipe.json", "assets.json", "surface.json"]) + for (const name of ["bake.mjs", "recipe.json", "assets.json", "surface.json", "heightmap.r16"]) copyFileSync(join(kit, name), join(dir, name)); mkdirSync(join(dir, "node_modules", "@threenative"), { recursive: true }); symlinkSync(packageRoot, join(dir, "node_modules", "@threenative", "terrain"), "dir"); diff --git a/packages/terrain/__tests__/starter-kit.spec.ts b/packages/terrain/__tests__/starter-kit.spec.ts index e9a8de230..260a2fbe8 100644 --- a/packages/terrain/__tests__/starter-kit.spec.ts +++ b/packages/terrain/__tests__/starter-kit.spec.ts @@ -4,6 +4,7 @@ import { copyFileSync, mkdirSync, readFileSync, readdirSync, statSync, symlinkSy import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { type IWorldPackage, validateWorldPackage } from "@threenative/core/world"; +import { Terrain, decodeRAW16 } from "@threenative/terrain"; import { describe, expect, it } from "vitest"; import { makeTempDirSync } from "../../../test-support/temp-dir.js"; @@ -15,7 +16,7 @@ const GOLDEN_HASHES: Record> = { "heightmap.u16": "33b3fc66f63ca4df76531a44a2bd84e1aff6f642eb9ab0c6622ebe87d1057c2f", "placements.bin": "36ab2fd57e76b4b0bd998c5398f983e5f030968b83d5326c0cb98df1b67e4019", "splat.rgba": "f5e45e3740eea9e751e94570abe785854670d060fda6fe4b5518ccd4a562c7ec", - "world.json": "1bcd3d2c9f056d78b30fbc68b68d5e1a7c31176f0f79471ae473f3bc526b11e9", + "world.json": "3ffa8c08ede40639373e4bff8bf3c707c65df72da6f28f98794fb17119051010", }, alpine: { "heightmap.u16": "bd84cb6c52a1142303dfb0f304190a981f29d77d118d3880e3eb246549838ae1", @@ -43,6 +44,30 @@ const GOLDEN_HASHES: Record> = { }, }; +/** SHA256 of each kit's inline heights and evaluated height grid at e12a5d578, as Float32 bytes. */ +const BASELINE_HEIGHTS: Record = { + forest: { + values: "46e9bdddaae82a29f770358e51f282486b41d0cab268a25bd58ba885c70a5ed8", + evaluated: "16db2efbc4f44fa016a69f8240ae5bf9ce92f15070ea67d436e575cea02ad55f", + }, + alpine: { + values: "72995391d8244365e32f8d8c38f80c04e6ec948b819918876d1f5d6eeed66c81", + evaluated: "5000eea008516889508c4efc0074bb8a36c41a0c5e0f1484c3c667d60644fc1c", + }, + coastal: { + values: "726595506d7dda30da88401b74c878375bcb40d88cef0debb4a10f27f47a2d17", + evaluated: "020ce3ba6dadb740886941287303c7ab37e53da2143cfcea3072eb197c934feb", + }, + desert: { + values: "b0f471e65576313f26f98e456323c4c6eb6c4e688c931e4cafc2c66663abce85", + evaluated: "86740e21dad3412d21524d1c85e85d5106d05eccc6e66f010ffd8168d1cdda54", + }, + tundra: { + values: "3fbcbc21b23e03a1c89d8ce0ccc2c8b93380973304e4be7662bc23185a706b58", + evaluated: "3c8e91abeacbd4469974513812c1ffc1703064808ba9facd1dbfb63a59fbdc62", + }, +}; + interface ITable { readonly base: { id: string }; readonly layers: readonly { id: string }[]; @@ -56,14 +81,20 @@ interface IKitWater { readonly rivers: readonly { id: string; width: number; points: number[][] }[]; } -/** Copies the named kit into a temp directory whose node_modules resolves the workspace package. */ +/** Copies the named kit into a temp directory whose node_modules resolves the workspace packages. */ function kitInTempDir(kitName = "forest"): string { const dir = makeTempDirSync(`terrain-starter-kit-${kitName}-`); const kit = join(packageRoot, "starter", kitName); - for (const name of ["bake.mjs", "recipe.json", "assets.json", "surface.json"]) + for (const name of ["bake.mjs", "recipe.json", "assets.json", "surface.json", "heightmap.r16"]) copyFileSync(join(kit, name), join(dir, name)); mkdirSync(join(dir, "node_modules", "@threenative"), { recursive: true }); symlinkSync(packageRoot, join(dir, "node_modules", "@threenative", "terrain"), "dir"); + // forest/bake.mjs imports the cook from @threenative/assets, so the fixture links this checkout's. + symlinkSync( + join(packageRoot, "..", "assets"), + join(dir, "node_modules", "@threenative", "assets"), + "dir", + ); return dir; } @@ -105,7 +136,10 @@ describe("forest starter kit", () => { expect(perAsset.fir).toBeGreaterThanOrEqual(2000); expect(perAsset.boulder).toBe(320); expect(Object.keys(perAsset).sort()).toEqual(["boulder", "fir", "fir-c"]); - expect(total).toBeLessThanOrEqual(25 * 1024 * 1024); + // The combined and streamed representations are alternatives, not additive. + const combinedBytes = statSync(join(out, "world.glb")).size; + expect(combinedBytes).toBeLessThanOrEqual(25 * 1024 * 1024); + expect(total - combinedBytes).toBeLessThanOrEqual(25 * 1024 * 1024); // Pinned golden hashes for starter-kit forest bake (PRD-592 AC-1) expect(files.get("heightmap.u16")).toBe( @@ -118,7 +152,7 @@ describe("forest starter kit", () => { "f5e45e3740eea9e751e94570abe785854670d060fda6fe4b5518ccd4a562c7ec", ); expect(files.get("world.json")).toBe( - "1bcd3d2c9f056d78b30fbc68b68d5e1a7c31176f0f79471ae473f3bc526b11e9", + "3ffa8c08ede40639373e4bff8bf3c707c65df72da6f28f98794fb17119051010", ); // One recipe and seed, one package: a second bake is byte-identical. @@ -139,6 +173,33 @@ describe("forest starter kit", () => { }, ); + it.each(["forest", "alpine", "coastal", "desert", "tundra"] as const)( + "hydrates the %s RAW16 sidecar to the original inline heights, bit for bit (PRD-589 AC-2)", + (kitName) => { + const kit = join(packageRoot, "starter", kitName); + expect(statSync(join(kit, "recipe.json")).size).toBeLessThan(200_000); + const current = JSON.parse(readFileSync(join(kit, "recipe.json"), "utf8")); + const data = current.recipe.layers[0].params.data; + // Decode the sidecar exactly as bake.mjs does, from the path the recipe names. + const sidecar = readFileSync(join(kit, data.raw16)); + expect(sidecar.byteLength).toBe(132098); + // The same rounding as the bake loader: each decoded float returns to the 0.1 m lattice. + data.values = Array.from( + decodeRAW16(sidecar, data).values, + (value) => Math.round(value * 10) / 10, + ); + + // Equal Float32 bytes mean no height moved, not even by one unit in the last place. + const digest = (heights: ArrayLike) => + createHash("sha256") + .update(new Uint8Array(Float32Array.from(heights).buffer)) + .digest("hex"); + const pinned = BASELINE_HEIGHTS[kitName]; + expect(digest(data.values)).toBe(pinned?.values); + expect(digest(Terrain.fromJSON(current.recipe).evaluate().height)).toBe(pinned?.evaluated); + }, + ); + it("writes every model and texture its manifest and table name", () => { const out = join(makeTempDirSync("terrain-starter-world-"), "world"); bake(out); @@ -236,6 +297,9 @@ describe("forest starter kit", () => { "starter/coastal/sky.ts", "starter/coastal/sea.ts", "starter-assets/fir_tree_01/fir-b-lod2.glb", + ...["forest", "alpine", "desert", "tundra", "coastal"].map( + (kit) => `starter/${kit}/heightmap.r16`, + ), ]) expect(files.has(path), `packed tarball is missing ${path}`).toBe(true); const kitBytes = packedFiles diff --git a/packages/terrain/__tests__/tundra-kit.spec.ts b/packages/terrain/__tests__/tundra-kit.spec.ts index 32f553bd0..6e46d761f 100644 --- a/packages/terrain/__tests__/tundra-kit.spec.ts +++ b/packages/terrain/__tests__/tundra-kit.spec.ts @@ -13,7 +13,7 @@ const starterAssets = join(packageRoot, "starter-assets"); /** Copies the tundra kit into a temp directory whose node_modules resolves the workspace package. */ function kitInTempDir(): string { const dir = makeTempDirSync("terrain-tundra-kit-"); - for (const name of ["bake.mjs", "recipe.json", "assets.json", "surface.json"]) + for (const name of ["bake.mjs", "recipe.json", "assets.json", "surface.json", "heightmap.r16"]) copyFileSync(join(kit, name), join(dir, name)); mkdirSync(join(dir, "node_modules", "@threenative"), { recursive: true }); symlinkSync(packageRoot, join(dir, "node_modules", "@threenative", "terrain"), "dir"); diff --git a/packages/terrain/starter/alpine/bake.mjs b/packages/terrain/starter/alpine/bake.mjs index 88f876779..82a478886 100644 --- a/packages/terrain/starter/alpine/bake.mjs +++ b/packages/terrain/starter/alpine/bake.mjs @@ -6,7 +6,12 @@ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { Terrain, applyPlacementOverrides, bakeWorldPackage } from "@threenative/terrain"; +import { + Terrain, + applyPlacementOverrides, + bakeWorldPackage, + decodeRAW16, +} from "@threenative/terrain"; const here = dirname(fileURLToPath(import.meta.url)); @@ -28,6 +33,13 @@ const assetsDir = flag("assets", join(installed, "starter-assets")); const outDir = flag("out", join(here, "world")); const recipe = JSON.parse(await readFile(join(here, "recipe.json"), "utf8")); +// shortcut: starter heights use a 0.1 m lattice; revise decoding if their precision changes. +for (const layer of recipe.recipe.layers) { + const data = layer.params?.data; + if (data?.raw16 === undefined) continue; + const raw = await readFile(join(here, data.raw16)); + data.values = Array.from(decodeRAW16(raw, data).values, (value) => Math.round(value * 10) / 10); +} const assetTable = JSON.parse(await readFile(join(here, "assets.json"), "utf8")); const surface = JSON.parse(await readFile(join(here, "surface.json"), "utf8")); diff --git a/packages/terrain/starter/alpine/heightmap.r16 b/packages/terrain/starter/alpine/heightmap.r16 new file mode 100644 index 0000000000000000000000000000000000000000..283d2d1ecdf2caba090441c0ea7b146440a9b911 GIT binary patch literal 132098 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 Math.round(value * 10) / 10); +} const assetTable = JSON.parse(await readFile(join(here, "assets.json"), "utf8")); const surface = JSON.parse(await readFile(join(here, "surface.json"), "utf8")); diff --git a/packages/terrain/starter/coastal/heightmap.r16 b/packages/terrain/starter/coastal/heightmap.r16 new file mode 100644 index 0000000000000000000000000000000000000000..b2db425b79364f554a8d5b75bba73d058673f93c GIT binary patch literal 132098 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%0A2tHcwW%}oFKdC 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 Math.round(value * 10) / 10); +} const assetTable = JSON.parse(await readFile(join(here, "assets.json"), "utf8")); const surface = JSON.parse(await readFile(join(here, "surface.json"), "utf8")); diff --git a/packages/terrain/starter/desert/heightmap.r16 b/packages/terrain/starter/desert/heightmap.r16 new file mode 100644 index 0000000000000000000000000000000000000000..9485861c448edd975a7ad76a39a84dfdfb12cc73 GIT binary patch literal 132098 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