phase(N.4) Tasks 22+23 fixup: trigger WB mesh loads + correct SurfaceId source

Task 26 visual verification surfaced three bugs in the dispatcher.
Two are fixed here; the third is documented as a remaining issue.

1. WB's IncrementRefCount only bumps a usage counter — it does NOT
   trigger mesh loading. Fixed in WbMeshAdapter.IncrementRefCount:
   call PrepareMeshDataAsync(id, isSetup: false) on first registration.
   Result auto-enqueues to _stagedMeshData (line 510 of WB's
   ObjectMeshManager) which Tick() drains onto the GPU.

2. EntitySpawnAdapter never registered per-instance entity meshes
   with WB. LandblockSpawnAdapter only registers atlas-tier
   (ServerGuid == 0); per-instance entities fell through. Fixed by
   adding optional IWbMeshAdapter constructor param + tracking unique
   GfxObj ids per server-guid for IncrementRefCount on OnCreate /
   DecrementRefCount on OnRemove.

3. WbDrawDispatcher.ResolveTexture used batch.SurfaceId which WB
   never populates (line 1746 of ObjectMeshManager only sets
   batch.Key — the TextureKey struct that has SurfaceId). Switched
   to batch.Key.SurfaceId.

Plus diagnostic counters (ACDREAM_WB_DIAG=1) for entity-seen / drawn
/ mesh-missing / draws-issued counts.

Status: with these fixes the dispatcher now issues real draw calls
(~16K/frame, validated via diagnostic). However visual verification
shows characters appear "exploded" (parts spaced too far apart) and
scenery (trees/rocks/fences/buildings) does not appear. Root cause
analysis pending — Adjustment 7 in the plan documents the deferred
work. Flag stays default-off; legacy renderer remains the
production path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-08 15:50:21 +02:00
parent fc80c252d6
commit 943652dc97
4 changed files with 96 additions and 6 deletions

View file

@ -55,6 +55,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
private bool _disposed;
// Diagnostic counters logged once per ~5s under ACDREAM_WB_DIAG=1.
private int _entitiesSeen;
private int _entitiesDrawn;
private int _meshesMissing;
private int _drawsIssued;
private long _lastLogTick;
public WbDrawDispatcher(
GL gl,
Shader shader,
@ -98,6 +105,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
_shader.SetMatrix4("uViewProjection", vp);
var metaTable = _meshAdapter.MetadataTable;
bool diag = string.Equals(Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"), "1", StringComparison.Ordinal);
// Collect visible entities into opaque and translucent lists for two-pass rendering.
// We walk entities once and classify each (entity, meshRef, batch) triple.
@ -124,6 +132,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
&& !visibleCellIds.Contains(entity.ParentCellId.Value))
continue;
if (diag) _entitiesSeen++;
var entityWorld =
Matrix4x4.CreateFromQuaternion(entity.Rotation) *
Matrix4x4.CreateTranslation(entity.Position);
@ -133,6 +143,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
? _entitySpawnAdapter.GetState(entity.ServerGuid)
: null;
bool drewAny = false;
for (int partIdx = 0; partIdx < entity.MeshRefs.Count; partIdx++)
{
if (animState is not null && animState.IsPartHidden(partIdx))
@ -145,7 +156,12 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
gfxObjId = animState.ResolvePartGfxObj(partIdx, gfxObjId);
var renderData = _meshAdapter.TryGetRenderData(gfxObjId);
if (renderData is null) continue;
if (renderData is null)
{
if (diag) _meshesMissing++;
continue;
}
drewAny = true;
// For Setup objects, WB stores sub-parts in SetupParts. For
// single GfxObjs, SetupParts is empty and the render data
@ -172,6 +188,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
entity, meshRef, metaTable, opaqueDraws, translucentDraws);
}
}
if (diag && drewAny) _entitiesDrawn++;
}
}
@ -223,6 +241,19 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
_gl.Disable(EnableCap.Blend);
_gl.Disable(EnableCap.CullFace);
_gl.BindVertexArray(0);
if (diag)
{
_drawsIssued += opaqueDraws.Count + translucentDraws.Count;
long now = Environment.TickCount64;
if (now - _lastLogTick > 5000)
{
Console.WriteLine(
$"[WB-DIAG] entSeen={_entitiesSeen} entDrawn={_entitiesDrawn} meshMissing={_meshesMissing} drawsIssued={_drawsIssued}");
_entitiesSeen = _entitiesDrawn = _meshesMissing = _drawsIssued = 0;
_lastLogTick = now;
}
}
}
private void ClassifyBatches(
@ -274,8 +305,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
private uint ResolveTexture(WorldEntity entity, MeshRef meshRef, ObjectRenderBatch batch)
{
uint surfaceId = batch.SurfaceId;
if (surfaceId == 0) return 0;
// WB stores the surface id on batch.Key.SurfaceId (TextureKey struct);
// batch.SurfaceId is unset (zero) for batches built by ObjectMeshManager.
uint surfaceId = batch.Key.SurfaceId;
if (surfaceId == 0 || surfaceId == 0xFFFFFFFF) return 0;
uint overrideOrigTex = 0;
bool hasOrigTexOverride = meshRef.SurfaceOverrides is not null