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:
parent
fc80c252d6
commit
943652dc97
4 changed files with 96 additions and 6 deletions
|
|
@ -1487,7 +1487,7 @@ public sealed class GameWindow : IDisposable
|
||||||
new NullAnimLoader());
|
new NullAnimLoader());
|
||||||
}
|
}
|
||||||
wbEntitySpawnAdapter = new AcDream.App.Rendering.Wb.EntitySpawnAdapter(
|
wbEntitySpawnAdapter = new AcDream.App.Rendering.Wb.EntitySpawnAdapter(
|
||||||
_textureCache, SequencerFactory);
|
_textureCache, SequencerFactory, _wbMeshAdapter);
|
||||||
_wbEntitySpawnAdapter = wbEntitySpawnAdapter;
|
_wbEntitySpawnAdapter = wbEntitySpawnAdapter;
|
||||||
}
|
}
|
||||||
_worldState = new AcDream.App.Streaming.GpuWorldState(wbSpawnAdapter, wbEntitySpawnAdapter);
|
_worldState = new AcDream.App.Streaming.GpuWorldState(wbSpawnAdapter, wbEntitySpawnAdapter);
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,18 @@ public sealed class EntitySpawnAdapter
|
||||||
{
|
{
|
||||||
private readonly ITextureCachePerInstance _textureCache;
|
private readonly ITextureCachePerInstance _textureCache;
|
||||||
private readonly Func<WorldEntity, AnimationSequencer> _sequencerFactory;
|
private readonly Func<WorldEntity, AnimationSequencer> _sequencerFactory;
|
||||||
|
private readonly IWbMeshAdapter? _meshAdapter;
|
||||||
|
|
||||||
// Per-server-guid state. Written on OnCreate, released on OnRemove.
|
// Per-server-guid state. Written on OnCreate, released on OnRemove.
|
||||||
// Single-threaded: called only from the render thread (same as GpuWorldState).
|
// Single-threaded: called only from the render thread (same as GpuWorldState).
|
||||||
private readonly Dictionary<uint, AnimatedEntityState> _stateByGuid = new();
|
private readonly Dictionary<uint, AnimatedEntityState> _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<uint, HashSet<ulong>> _meshIdsByGuid = new();
|
||||||
|
|
||||||
/// <param name="textureCache">
|
/// <param name="textureCache">
|
||||||
/// Per-instance texture decode path. In production this is the
|
/// Per-instance texture decode path. In production this is the
|
||||||
/// <see cref="TextureCache"/> instance (which implements
|
/// <see cref="TextureCache"/> instance (which implements
|
||||||
|
|
@ -66,14 +73,23 @@ public sealed class EntitySpawnAdapter
|
||||||
/// and server-supplied motion table override. Tests pass a lambda that
|
/// and server-supplied motion table override. Tests pass a lambda that
|
||||||
/// returns a stub sequencer.
|
/// returns a stub sequencer.
|
||||||
/// </param>
|
/// </param>
|
||||||
|
/// <param name="meshAdapter">
|
||||||
|
/// Optional WB mesh adapter. When non-null, <see cref="OnCreate"/>
|
||||||
|
/// registers each unique <c>MeshRef.GfxObjId</c> with the adapter so WB
|
||||||
|
/// background-loads the mesh data; <see cref="OnRemove"/> decrements the
|
||||||
|
/// matching ref counts. When null, the adapter only tracks per-instance
|
||||||
|
/// state without driving WB lifecycle (test mode + flag-off mode).
|
||||||
|
/// </param>
|
||||||
public EntitySpawnAdapter(
|
public EntitySpawnAdapter(
|
||||||
ITextureCachePerInstance textureCache,
|
ITextureCachePerInstance textureCache,
|
||||||
Func<WorldEntity, AnimationSequencer> sequencerFactory)
|
Func<WorldEntity, AnimationSequencer> sequencerFactory,
|
||||||
|
IWbMeshAdapter? meshAdapter = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(textureCache);
|
ArgumentNullException.ThrowIfNull(textureCache);
|
||||||
ArgumentNullException.ThrowIfNull(sequencerFactory);
|
ArgumentNullException.ThrowIfNull(sequencerFactory);
|
||||||
_textureCache = textureCache;
|
_textureCache = textureCache;
|
||||||
_sequencerFactory = sequencerFactory;
|
_sequencerFactory = sequencerFactory;
|
||||||
|
_meshAdapter = meshAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -126,6 +142,23 @@ public sealed class EntitySpawnAdapter
|
||||||
state.SetPartOverride(po.PartIndex, po.GfxObjId);
|
state.SetPartOverride(po.PartIndex, po.GfxObjId);
|
||||||
|
|
||||||
_stateByGuid[entity.ServerGuid] = state;
|
_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<ulong>();
|
||||||
|
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;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,7 +167,16 @@ public sealed class EntitySpawnAdapter
|
||||||
/// on <c>RemoveObject</c>. Unknown guids (never spawned, or already
|
/// on <c>RemoveObject</c>. Unknown guids (never spawned, or already
|
||||||
/// removed) are silently ignored.
|
/// removed) are silently ignored.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Look up the <see cref="AnimatedEntityState"/> for a server guid.
|
/// Look up the <see cref="AnimatedEntityState"/> for a server guid.
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
|
|
||||||
private bool _disposed;
|
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(
|
public WbDrawDispatcher(
|
||||||
GL gl,
|
GL gl,
|
||||||
Shader shader,
|
Shader shader,
|
||||||
|
|
@ -98,6 +105,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
_shader.SetMatrix4("uViewProjection", vp);
|
_shader.SetMatrix4("uViewProjection", vp);
|
||||||
|
|
||||||
var metaTable = _meshAdapter.MetadataTable;
|
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.
|
// Collect visible entities into opaque and translucent lists for two-pass rendering.
|
||||||
// We walk entities once and classify each (entity, meshRef, batch) triple.
|
// 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))
|
&& !visibleCellIds.Contains(entity.ParentCellId.Value))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (diag) _entitiesSeen++;
|
||||||
|
|
||||||
var entityWorld =
|
var entityWorld =
|
||||||
Matrix4x4.CreateFromQuaternion(entity.Rotation) *
|
Matrix4x4.CreateFromQuaternion(entity.Rotation) *
|
||||||
Matrix4x4.CreateTranslation(entity.Position);
|
Matrix4x4.CreateTranslation(entity.Position);
|
||||||
|
|
@ -133,6 +143,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
? _entitySpawnAdapter.GetState(entity.ServerGuid)
|
? _entitySpawnAdapter.GetState(entity.ServerGuid)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
bool drewAny = false;
|
||||||
for (int partIdx = 0; partIdx < entity.MeshRefs.Count; partIdx++)
|
for (int partIdx = 0; partIdx < entity.MeshRefs.Count; partIdx++)
|
||||||
{
|
{
|
||||||
if (animState is not null && animState.IsPartHidden(partIdx))
|
if (animState is not null && animState.IsPartHidden(partIdx))
|
||||||
|
|
@ -145,7 +156,12 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
gfxObjId = animState.ResolvePartGfxObj(partIdx, gfxObjId);
|
gfxObjId = animState.ResolvePartGfxObj(partIdx, gfxObjId);
|
||||||
|
|
||||||
var renderData = _meshAdapter.TryGetRenderData(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
|
// For Setup objects, WB stores sub-parts in SetupParts. For
|
||||||
// single GfxObjs, SetupParts is empty and the render data
|
// single GfxObjs, SetupParts is empty and the render data
|
||||||
|
|
@ -172,6 +188,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
entity, meshRef, metaTable, opaqueDraws, translucentDraws);
|
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.Blend);
|
||||||
_gl.Disable(EnableCap.CullFace);
|
_gl.Disable(EnableCap.CullFace);
|
||||||
_gl.BindVertexArray(0);
|
_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(
|
private void ClassifyBatches(
|
||||||
|
|
@ -274,8 +305,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
|
|
||||||
private uint ResolveTexture(WorldEntity entity, MeshRef meshRef, ObjectRenderBatch batch)
|
private uint ResolveTexture(WorldEntity entity, MeshRef meshRef, ObjectRenderBatch batch)
|
||||||
{
|
{
|
||||||
uint surfaceId = batch.SurfaceId;
|
// WB stores the surface id on batch.Key.SurfaceId (TextureKey struct);
|
||||||
if (surfaceId == 0) return 0;
|
// 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;
|
uint overrideOrigTex = 0;
|
||||||
bool hasOrigTexOverride = meshRef.SurfaceOverrides is not null
|
bool hasOrigTexOverride = meshRef.SurfaceOverrides is not null
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,22 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
_meshManager.IncrementRefCount(id);
|
_meshManager.IncrementRefCount(id);
|
||||||
|
|
||||||
if (_metadataPopulated.Add(id))
|
if (_metadataPopulated.Add(id))
|
||||||
|
{
|
||||||
PopulateMetadata(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue