Merge branch 'claude/quirky-jepsen-fd60f1' — N.4 Adjustment 2 (Task 9 routing revert)

This commit is contained in:
Erik 2026-05-08 13:48:30 +02:00
commit dc6410b56f
2 changed files with 53 additions and 38 deletions

View file

@ -837,6 +837,47 @@ writing a real bridge that shares index caches with our `DatCollection`.
**No work for this task — skip and proceed to Task 7.**
---
### Adjustment 2 (2026-05-08): Task 9 routing reverted — tier decision belongs at spawn-callback layer
**Discovered during Week 1 visual smoke test**: with flag on, characters /
NPCs disappeared along with static scenery. Root cause: Task 9 routed
**all** `InstancedMeshRenderer.EnsureUploaded` calls through
`WbMeshAdapter.IncrementRefCount` and marked their cache entries with
`WbManagedSentinel`. But `InstancedMeshRenderer` is used for both tiers
in production:
- **Atlas-tier** call sites: `_pendingCellMeshes` drain
([GameWindow.cs:5137](../../../src/AcDream.App/Rendering/GameWindow.cs:5137)),
per-MeshRef GfxObj loop on `lb.Entities`
([:5155](../../../src/AcDream.App/Rendering/GameWindow.cs:5155)).
- **Per-instance-tier** call sites: per-part loop in spawn handling
([:2302](../../../src/AcDream.App/Rendering/GameWindow.cs:2302)) — this is
character / creature rendering driven by server `CreateObject`.
The renderer is **tier-blind by design**: it doesn't know spawn source.
Putting routing logic there violates separation of concerns. The spec's
Data-Flow section already specifies the right placement — routing happens
at the **spawn-callback layer**:
- `LandblockSpawnAdapter.OnLandblockLoaded(...)` (Task 11) calls
`IncrementRefCount` per unique GfxObj — atlas-tier only.
- `EntitySpawnAdapter.OnCreate(entity)` (Task 17) routes through
per-instance path (`TextureCache.GetOrUploadWithPaletteOverride`) —
never calls `IncrementRefCount` for atlas.
**Resolution:** reverted Task 9's renderer-level routing. Removed the
sentinel logic and the 4 sentinel-skip checks in
`InstancedMeshRenderer`. **Kept** the `_wbMeshAdapter` constructor
parameter (unused for now) so `GameWindow.cs` doesn't shift when
later tasks need adapter access. Kept all the real WB pipeline
construction in `WbMeshAdapter` (verified working under flag-off).
**Week 1 endpoint shifts:** "WB infrastructure constructed; flag-on and
flag-off visually identical." Routing arrives in Week 2 (Task 11) at
the correct layer. Smoke verification is now: flag-on === flag-off.
### Task 6 (original — kept for history)
**Files:**

View file

@ -35,19 +35,16 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
private readonly TextureCache _textures;
/// <summary>
/// Optional WB adapter. When non-null and <see cref="WbFoundationFlag.IsEnabled"/>,
/// <see cref="EnsureUploaded"/> hands the GfxObj ref to the WB pipeline instead of
/// uploading into our own VAO pool. The draw loop skips sentinel entries — Task 22's
/// WbDrawDispatcher will eventually draw them.
/// Optional WB adapter. Held but currently unused — Phase N.4 Adjustment 2
/// (2026-05-08) reverted Task 9's renderer-level routing. Tier-routing decisions
/// (atlas vs per-instance) belong at the spawn-callback layer (Task 11
/// LandblockSpawnAdapter for atlas-tier; Task 17 EntitySpawnAdapter for
/// per-instance), not in the renderer which is intentionally tier-blind. The
/// constructor parameter is preserved so GameWindow's wire-up doesn't shift
/// when later tasks need adapter access.
/// </summary>
private readonly WbMeshAdapter? _wbMeshAdapter;
// Sentinel: a GfxObj that has been handed to the WB pipeline gets this list
// stored in _gpuByGfxObj. The Draw loop recognises it by reference identity
// (object.ReferenceEquals) and skips it — no legacy VAO draw for WB-managed
// objects until Task 22 wires up WbDrawDispatcher.
private static readonly List<SubMeshGpu> WbManagedSentinel = new(0);
// One GPU bundle per unique GfxObj id. Each GfxObj can have multiple sub-meshes.
private readonly Dictionary<uint, List<SubMeshGpu>> _gpuByGfxObj = new();
@ -100,17 +97,11 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
if (_gpuByGfxObj.ContainsKey(gfxObjId))
return;
// Phase N.4 Task 9: when the WB foundation flag is on and we have an
// adapter, hand this GfxObj to the WB pipeline instead of uploading our
// own VAO. The sentinel entry marks "this GfxObj lives in WB now" so the
// draw loop knows to skip it. Task 22's WbDrawDispatcher will draw them.
if (WbFoundationFlag.IsEnabled && _wbMeshAdapter is not null)
{
_wbMeshAdapter.IncrementRefCount(gfxObjId);
_gpuByGfxObj[gfxObjId] = WbManagedSentinel;
return;
}
// Phase N.4 Adjustment 2 (2026-05-08): renderer is tier-blind. Tier-routing
// (atlas vs per-instance) lives at the spawn-callback layer (Tasks 11 + 17),
// not here. Smoke-test of the original Task 9 routing showed it caught
// characters / NPCs (server-spawned, per-instance tier) along with static
// scenery, because EnsureUploaded is called from both spawn paths.
var list = new List<SubMeshGpu>(subMeshes.Count);
foreach (var sm in subMeshes)
list.Add(UploadSubMesh(sm));
@ -245,11 +236,6 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
if (!_gpuByGfxObj.TryGetValue(key.GfxObjId, out var subMeshes))
continue;
// WB-managed GfxObjs have a sentinel entry; Task 22 (WbDrawDispatcher)
// will draw them. Skip here to avoid drawing with stale/null VAO data.
if (object.ReferenceEquals(subMeshes, WbManagedSentinel))
continue;
bool hasOpaqueSubMesh = false;
foreach (var sub in subMeshes)
{
@ -325,10 +311,6 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
if (!_gpuByGfxObj.TryGetValue(key.GfxObjId, out var subMeshes))
continue;
// WB-managed GfxObjs — skip; Task 22 will draw them.
if (object.ReferenceEquals(subMeshes, WbManagedSentinel))
continue;
bool hasTranslucentSubMesh = false;
foreach (var sub in subMeshes)
{
@ -458,9 +440,6 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
{
if (!_gpuByGfxObj.TryGetValue(meshRef.GfxObjId, out var cachedMeshes))
continue;
// WB-managed GfxObjs don't go through our instance pipeline.
if (object.ReferenceEquals(cachedMeshes, WbManagedSentinel))
continue;
var model = meshRef.PartTransform * entityRoot;
@ -565,11 +544,6 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
{
foreach (var subs in _gpuByGfxObj.Values)
{
// WB-managed entries use the sentinel — no GL resources to free here;
// ObjectMeshManager owns those resources.
if (object.ReferenceEquals(subs, WbManagedSentinel))
continue;
foreach (var sub in subs)
{
_gl.DeleteBuffer(sub.Vbo);