From 943652dc9734ba7cfa2af984def15f65d4d04d0a Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 8 May 2026 15:50:21 +0200 Subject: [PATCH] phase(N.4) Tasks 22+23 fixup: trigger WB mesh loads + correct SurfaceId source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/AcDream.App/Rendering/GameWindow.cs | 2 +- .../Rendering/Wb/EntitySpawnAdapter.cs | 46 ++++++++++++++++++- .../Rendering/Wb/WbDrawDispatcher.cs | 39 ++++++++++++++-- src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs | 15 ++++++ 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 1e821ee..1048e02 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1487,7 +1487,7 @@ public sealed class GameWindow : IDisposable new NullAnimLoader()); } wbEntitySpawnAdapter = new AcDream.App.Rendering.Wb.EntitySpawnAdapter( - _textureCache, SequencerFactory); + _textureCache, SequencerFactory, _wbMeshAdapter); _wbEntitySpawnAdapter = wbEntitySpawnAdapter; } _worldState = new AcDream.App.Streaming.GpuWorldState(wbSpawnAdapter, wbEntitySpawnAdapter); diff --git a/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs b/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs index 0315c94..eb05d92 100644 --- a/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs @@ -49,11 +49,18 @@ public sealed class EntitySpawnAdapter { private readonly ITextureCachePerInstance _textureCache; private readonly Func _sequencerFactory; + private readonly IWbMeshAdapter? _meshAdapter; // Per-server-guid state. Written on OnCreate, released on OnRemove. // Single-threaded: called only from the render thread (same as GpuWorldState). private readonly Dictionary _stateByGuid = new(); + // Per-server-guid set of GfxObj ids registered with the mesh adapter, + // so OnRemove can decrement each. Per-instance entities don't go through + // LandblockSpawnAdapter, so without this their meshes would never load + // (WB doesn't know they exist). + private readonly Dictionary> _meshIdsByGuid = new(); + /// /// Per-instance texture decode path. In production this is the /// instance (which implements @@ -66,14 +73,23 @@ public sealed class EntitySpawnAdapter /// and server-supplied motion table override. Tests pass a lambda that /// returns a stub sequencer. /// + /// + /// Optional WB mesh adapter. When non-null, + /// registers each unique MeshRef.GfxObjId with the adapter so WB + /// background-loads the mesh data; decrements the + /// matching ref counts. When null, the adapter only tracks per-instance + /// state without driving WB lifecycle (test mode + flag-off mode). + /// public EntitySpawnAdapter( ITextureCachePerInstance textureCache, - Func sequencerFactory) + Func sequencerFactory, + IWbMeshAdapter? meshAdapter = null) { ArgumentNullException.ThrowIfNull(textureCache); ArgumentNullException.ThrowIfNull(sequencerFactory); _textureCache = textureCache; _sequencerFactory = sequencerFactory; + _meshAdapter = meshAdapter; } /// @@ -126,6 +142,23 @@ public sealed class EntitySpawnAdapter state.SetPartOverride(po.PartIndex, po.GfxObjId); _stateByGuid[entity.ServerGuid] = state; + + // Register each unique GfxObj id with WB so the meshes background-load. + // Includes both the entity's natural MeshRefs AND any server-sent + // PartOverride GfxObjs (weapons, clothing, helmets) — those replace the + // Setup default and need their own mesh data uploaded. + if (_meshAdapter is not null) + { + var unique = new HashSet(); + foreach (var meshRef in entity.MeshRefs) + unique.Add((ulong)meshRef.GfxObjId); + foreach (var po in entity.PartOverrides) + unique.Add((ulong)po.GfxObjId); + + _meshIdsByGuid[entity.ServerGuid] = unique; + foreach (var id in unique) _meshAdapter.IncrementRefCount(id); + } + return state; } @@ -134,7 +167,16 @@ public sealed class EntitySpawnAdapter /// on RemoveObject. Unknown guids (never spawned, or already /// removed) are silently ignored. /// - public void OnRemove(uint serverGuid) => _stateByGuid.Remove(serverGuid); + public void OnRemove(uint serverGuid) + { + _stateByGuid.Remove(serverGuid); + + if (_meshAdapter is not null && _meshIdsByGuid.TryGetValue(serverGuid, out var ids)) + { + foreach (var id in ids) _meshAdapter.DecrementRefCount(id); + _meshIdsByGuid.Remove(serverGuid); + } + } /// /// Look up the for a server guid. diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 0cedba9..3388887 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -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 diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs index 06a6c85..b57e043 100644 --- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs @@ -119,7 +119,22 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter _meshManager.IncrementRefCount(id); if (_metadataPopulated.Add(id)) + { PopulateMetadata(id); + + // WB's IncrementRefCount alone only bumps a usage counter; it does + // NOT trigger mesh loading. We must explicitly call PrepareMeshDataAsync + // so the background workers actually decode the GfxObj. The result + // auto-enqueues into _stagedMeshData (ObjectMeshManager line 510), + // which Tick() drains onto the GPU. Until that completes, + // TryGetRenderData(id) returns null and the dispatcher silently + // skips the entity — standard streaming flicker. + // + // isSetup: false — acdream's MeshRefs already carry expanded + // per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is + // unused. + _ = _meshManager.PrepareMeshDataAsync(id, isSetup: false); + } } ///