using System.Collections.Generic; using AcDream.Core.World; namespace AcDream.App.Rendering.Wb; /// /// Bridges landblock streaming events to 's /// reference-count lifecycle. Tier-aware by design: only atlas-tier /// entities (procedural / dat-hydrated, identified by /// ServerGuid == 0) drive ref counts. Server-spawned entities /// (per-instance tier) are skipped — those go through /// EntitySpawnAdapter + TextureCache.GetOrUploadWithPaletteOverride /// (see Phase N.4 spec, Architecture → Two-tier rendering split). /// /// /// On load: walks the landblock's atlas-tier entities, collects unique /// GfxObj ids from their MeshRefs, calls /// IncrementRefCount per id, and pins each specialized EnvCell geometry /// id without starting generic GfxObj decode. Snapshots both id-sets per /// landblock so unload can match the load 1:1. /// /// /// /// On unload: looks up both snapshots, calls DecrementRefCount per id, /// drops the snapshots. Unknown / never-loaded landblocks no-op. /// /// /// /// Idempotency: repeated notifications for the same landblock only register /// newly-seen ids. This matters for two-tier streaming: a far-tier terrain /// load first snapshots an empty entity set, then a later Far-to-Near promotion /// supplies the actual stabs/buildings. Treating the second notification as a /// blanket no-op leaves the world-state entity list populated while the WB /// mesh cache never pins the promoted GfxObj ids. /// /// /// /// Thread safety: the internal snapshots are intentionally not synchronized. /// invokes every load, unload, and readiness query /// on the owning render/update thread. /// /// public sealed class LandblockSpawnAdapter { private readonly IWbMeshAdapter _adapter; // Maps landblock id → unique GfxObj ids registered for that landblock. // Written on load, read+cleared on unload. Single-threaded (streaming worker). private readonly Dictionary> _idsByLandblock = new(); // EnvCell shells are prepared through PrepareEnvCellGeomMeshDataAsync rather // than generic GfxObj loading, but still require explicit lifetime pins. // Keep their synthetic ids separate so registration uses the no-decode pin. private readonly Dictionary> _additionalReadinessIdsByLandblock = new(); public LandblockSpawnAdapter(IWbMeshAdapter adapter) { System.ArgumentNullException.ThrowIfNull(adapter); _adapter = adapter; } /// /// Called when a landblock finishes streaming in or receives promoted /// atlas-tier entities. Registers a ref-count increment with WB for each /// unique atlas-tier GfxObj id that has not already been registered for /// this landblock. /// public void OnLandblockLoaded( LoadedLandblock landblock, IEnumerable? additionalReadinessIds = null) { System.ArgumentNullException.ThrowIfNull(landblock); var unique = new HashSet(); foreach (var entity in landblock.Entities) { // Atlas-tier filter: server-spawned entities (ServerGuid != 0) // belong to the per-instance path and are NOT registered with WB. if (entity.ServerGuid != 0) continue; foreach (var meshRef in entity.MeshRefs) unique.Add((ulong)meshRef.GfxObjId); } if (!_idsByLandblock.TryGetValue(landblock.LandblockId, out var registered)) { _idsByLandblock[landblock.LandblockId] = unique; foreach (var id in unique) _adapter.IncrementRefCount(id); } else { foreach (var id in unique) { if (registered.Add(id)) _adapter.IncrementRefCount(id); } } if (!_additionalReadinessIdsByLandblock.TryGetValue( landblock.LandblockId, out var additional)) { additional = new HashSet(); _additionalReadinessIdsByLandblock[landblock.LandblockId] = additional; } if (additionalReadinessIds is not null) { foreach (var id in additionalReadinessIds) if (additional.Add(id)) _adapter.PinPreparedRenderData(id); } } /// /// True only after every render mesh required by this published landblock /// is drawable. This includes both ref-counted static GfxObjs and EnvCell /// shell geometry prepared by the independent indoor pipeline. /// public bool IsLandblockRenderReady(uint landblockId) { if (!_idsByLandblock.TryGetValue(landblockId, out var registered) || !_additionalReadinessIdsByLandblock.TryGetValue(landblockId, out var additional)) { return false; } foreach (var id in registered) if (!_adapter.IsRenderDataReady(id)) return false; foreach (var id in additional) if (!_adapter.IsRenderDataReady(id)) return false; return true; } /// /// Called when a landblock is unloaded from the streaming window. /// Releases the ref-count for every GfxObj id that was registered on load. /// Unknown landblock ids (never loaded, or already unloaded) are no-ops. /// public void OnLandblockUnloaded(uint landblockId) { if (!_idsByLandblock.TryGetValue(landblockId, out var unique)) return; foreach (var id in unique) _adapter.DecrementRefCount(id); if (_additionalReadinessIdsByLandblock.TryGetValue(landblockId, out var additional)) foreach (var id in additional) _adapter.DecrementRefCount(id); _idsByLandblock.Remove(landblockId); _additionalReadinessIdsByLandblock.Remove(landblockId); } }