diff --git a/docs/ISSUES.md b/docs/ISSUES.md index e6d52f4..c0446da 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -46,6 +46,37 @@ Copy this block when adding a new issue: # Active issues +## #56 — `ParticleHookSink` ignores `CreateParticleHook.PartIndex`; multi-emitter scripts collapse to entity root + +**Status:** OPEN +**Severity:** MEDIUM (every multi-emitter PES on a multi-part entity is visually wrong — portals, chimneys, fireplaces, animation-hook spell effects, anything where the dat author distributed emitters across mesh parts) +**Filed:** 2026-05-12 +**Component:** vfx / `ParticleHookSink` (call-site contract gap with the renderer) + +**Description:** When `EntityScriptActivator` (Phase C.1.5a) fires a multi-emitter `PhysicsScript` such as a portal's `DefaultScript`, every `CreateParticleHook` in the script spawns at `entity.Position + rotated(hook.Offset.Origin)` — ignoring the hook's `PartIndex`. The Holtburg Town network portal's script `0x3300126D` has 10 hooks (8 `CreateParticle` + sounds + sub-script calls) intended to attach to different mesh parts of the Setup (arch base, columns, apex). All 10 collapse to one point, producing a compressed, ground-buried swirl instead of the multi-tier shape retail renders. + +Captured during C.1.5a visual verification 2026-05-12: +- Portal A: entity `0x7A9B405B`, script `0x3300126D`, anchor `(27.33, 137.49, 66.30)`, 10 hooks +- Portal B: entity `0x7A9B4080`, script `0x3300067A`, anchor `(14.39, 55.61, 78.20)`, 4 hooks +User report: "It's less flat [than pre-rotation-fix] but in retail it seems to expand more in all directions. … still buried in the ground." + +**Root cause / status:** Documented in [ParticleHookSink.cs:18-24](../src/AcDream.Core/Vfx/ParticleHookSink.cs#L18) as a known C.1 limitation: "Retail attaches to a specific mesh part; we attach to the entity's root and will refine per-part when the renderer exposes per-part world transforms." The renderer (`WbDrawDispatcher`) does compute per-part transforms each frame for the modern bindless path, but they're not surfaced to the sink. The activator passes only `entity.Position` + `entity.Rotation`; the part-relative offsets the dat author chose are lost. + +**Files:** +- [src/AcDream.Core/Vfx/ParticleHookSink.cs:176-217](../src/AcDream.Core/Vfx/ParticleHookSink.cs) (`SpawnFromHook` — currently `anchor = worldPos + Vector3.Transform(offset, rotation)`; missing the `part[PartIndex].Transform` multiplication). +- [src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs](../src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs) (would need to pass per-part transforms, or arrange for the sink to query them). +- Renderer-side: per-part transforms live in `WbDrawDispatcher` / `AnimatedEntityState`. + +**Research:** For static entities (portals, chimneys, fireplaces), per-part offsets can be precomputed from `Setup.PlacementFrames[Resting]` at spawn time — no animation tick needed. For animated entities, the per-part transform varies per frame and the sink would need a per-tick refresh similar to how `UpdateEntityAnchor` works for AttachLocal emitters today. + +**Acceptance:** The Holtburg Town network portal's swirl matches retail in vertical extent (no ground-burial) and lateral spread (multiple emitters at distinct part positions, not collapsed to one point). Side-by-side dual-client visual check, same procedure as the C.1.5a acceptance gate. + +**Blocks / unblocks:** +- Phase C.1.5b (EnvCell static chimneys + fireplaces) will visually disappoint without this fix — chimneys are multi-part dat objects with smoke emitters attached to the chimney-top part. +- Phase C.1.5a (portal wiring) shipped without it because the mechanism is correct end-to-end and the gap is a separate concern that benefits every multi-part PES path. + +--- + ## #55 — Static-entity slow path reports ~1.45M `meshMissing` per 5s at r4 standstill **Status:** OPEN diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 91b674a..e5f317e 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -64,6 +64,7 @@ | N.5 | Modern rendering path — lifted `WbDrawDispatcher` onto bindless textures (`GL_ARB_bindless_texture`) + `glMultiDrawElementsIndirect`. Per-frame entity rendering: 3 SSBO uploads (instance matrices @ binding=0, batch data @ binding=1, indirect commands) + 2 indirect draw calls (opaque + transparent). ~12-15 GL calls per frame regardless of group count, down from hundreds-of-per-group in N.4. CPU dispatcher: 1.23 ms/frame median at Holtburg courtyard (1662 groups, ~810 fps sustained). All textures on the WB modern path use 1-layer `Texture2DArray` + `sampler2DArray`. Legacy callers keep `Texture2D` / `sampler2D` via the parallel `TextureCache` path until N.6 retires them. Three gotchas captured in memory: texture target lock-in, bindless Dispose order (two-phase non-resident before delete), GL_TIME_ELAPSED double-buffering. **Ship amendment 2026-05-08:** legacy renderers (`InstancedMeshRenderer`, `StaticMeshRenderer`, `WbFoundationFlag`) retired within N.5 — modern path is mandatory; missing bindless throws `NotSupportedException` at startup. N.6 scope narrowed accordingly. Plan archived at `docs/superpowers/plans/2026-05-08-phase-n5-modern-rendering.md`. | Live ✓ | | N.5b | Terrain on the modern rendering path — `TerrainModernRenderer` replaces `TerrainChunkRenderer` (the latter plus `TerrainRenderer` + `terrain.vert/.frag` deleted). Single global VBO/EBO with slot allocator (one slot per landblock), per-frame `DrawElementsIndirectCommand[]` upload + `glMultiDrawElementsIndirect`, bindless atlas handles passed as `uvec2` uniforms reconstructed via `sampler2DArray(handle)`. **Path C** chosen: mirrors WB's `TerrainRenderManager` pattern but consumes `LandblockMesh.Build` so retail's `FSplitNESW` formula is preserved (closes ISSUE #51). Path A killed by 49.98% measured divergence between WB's `CalculateSplitDirection` and retail's at addr `00531d10`; Path B (fork-patch WB) rejected for permanent maintenance burden. Perf at Holtburg radius=5 (commit `da56063`): modern 6.4-7.0 µs / 9-14 µs p95 vs legacy 1.5 µs / 3.0 µs — **modern is ~4× SLOWER on CPU at radius=5** because legacy's 16×16-LB chunking collapsed visible LBs to one `glDrawElements`. Architectural wins (zero `glBindTexture`/frame, constant-cost dispatch, per-LB frustum cull) manifest at higher radius (A.5 territory). Spec acceptance criterion 5 ("≥10% lower CPU at radius=5") amended via `docs/plans/2026-05-09-phase-n5b-perf-baseline.md`. Three gotchas captured in memory: `uniform sampler2DArray` + `glProgramUniformHandleARB` GL_INVALID_OPERATIONs on at least one driver (use `uniform uvec2` + `sampler2DArray(handle)` constructor instead — N.5's mesh_modern pattern); `MaybeFlushTerrainDiag` median-calc underflow on first sample; visual gates need actual visual confirmation, not assent. Plan archived at `docs/superpowers/plans/2026-05-09-phase-n5b-terrain-modern.md`. | Live ✓ | | N.6 slice 1 | GPU timing fix + radius=12 perf baseline. Fixed the gpu_us double-buffering bug in `WbDrawDispatcher` (ring-of-3 query slots, read-before-overwrite, vendor-neutral across AMD/NVIDIA/Intel desktop GL). Added env-gated `ACDREAM_DUMP_SURFACES=1` one-shot surface-format histogram dump in `TextureCache` for the atlas-opportunity audit. Captured authoritative baseline at Holtburg radii 4 / 8 / 12 (standstill + walking) with the now-working `gpu_us` diagnostic; baseline doc concludes CPU dominates GPU by 30–50× at every radius and recommends C.1.5 next then reduced-scope slice 2 (atlas + persistent-mapped buffers dropped). Baseline numbers at [docs/plans/2026-05-11-phase-n6-perf-baseline.md](2026-05-11-phase-n6-perf-baseline.md). Plan archived at `docs/superpowers/plans/2026-05-11-phase-n6-slice1.md`. | Live ✓ | +| C.1.5a | Portal PES wiring — server-spawned `WorldEntity` entities now fire their `Setup.DefaultScript` through the already-shipped `PhysicsScriptRunner` on enter-world. New ~70-line [`EntityScriptActivator`](../../src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs) class wires into `GpuWorldState`'s spawn lifecycle (`AppendLiveEntity` → `OnCreate`, `RemoveEntityByServerGuid` → `OnRemove`). Resolver lambda in `GameWindow` hits `_dats.Get(...)?.DefaultScript.DataId` with defensive try/catch returning `0u` on miss. Activator also seeds `_particleSink.SetEntityRotation` so hook offsets transform from entity-local to world space correctly. **Verified at the Holtburg Town network portal**: 10-hook portal script fires end-to-end with correct color, persistence, orientation, multi-emitter dispatch. **Known limitation surfaced and filed as issue #56**: `ParticleHookSink` ignores `CreateParticleHook.PartIndex`, so the 10 emitters collapse to one root position instead of distributing across the portal Setup's parts — visually produces a compressed, partly-ground-buried swirl. Mechanism is correct; per-part transform handling is the next vfx-pipeline work (blocks slice 2 visual delight; affects every multi-emitter PES). Spec: [`docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md`](../superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md). Plan: [`docs/superpowers/plans/2026-05-12-phase-c1.5a-portals.md`](../superpowers/plans/2026-05-12-phase-c1.5a-portals.md). | Live ✓ (with #56) | Plus polish that doesn't get its own phase number: - FlyCamera default speed lowered + Shift-to-boost diff --git a/docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md b/docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md index c1eaf5d..b92f3b2 100644 --- a/docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md +++ b/docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md @@ -335,13 +335,23 @@ do not blindly retry. These are intentionally not fixed in slice 1; tracked here so the next slice or a future phase picks them up: -1. **Moving entities** don't re-anchor their DefaultScript emitters per +1. **`PartIndex` collapse on multi-part entities** (NEW — verified 2026-05-12 + at the Holtburg Town network portal). `ParticleHookSink.SpawnFromHook` + ignores `CreateParticleHook.PartIndex`, so every emitter in a multi-emitter + script collapses to `entity.Position + rotated(hook.Offset.Origin)`. Retail + distributes the script's emitters across the entity's mesh parts (arch base, + columns, apex). Visual symptom for the Holtburg portal: the 10-hook script + produces a compressed swirl partially buried in the ground instead of the + multi-tier shape retail renders. Filed as `docs/ISSUES.md` #56 with the + captured entity guids + script ids; affects slice 2 (EnvCell chimneys / + fireplaces are multi-part) and any future multi-emitter PES path. +2. **Moving entities** don't re-anchor their DefaultScript emitters per frame. No evidence retail's portals or chimneys move; revisit if visual verification surfaces a regression. -2. **WB's re-fire-after-1s loop** is not implemented. Persistent emitters +3. **WB's re-fire-after-1s loop** is not implemented. Persistent emitters work today; looping non-persistent emitters (if EnvCell static objects use them) would need it in C.1.5b. -3. **Animation-hook particle path** (`MotionInterpreter` → +4. **Animation-hook particle path** (`MotionInterpreter` → `ParticleHookSink`) is shipped in C.1 but **not verified** by a recent visual test in this codebase state. Confirming this path is the second half of C.1.5b.