using System.Numerics; using System.Runtime.InteropServices; using System.Text; using AcDream.App.Rendering.Scene; using AcDream.Core.Physics; namespace AcDream.App.Rendering.Walk; /// /// Campaign FW3.2b-2: the production over /// the retained scene () and the FW3.1 /// . /// /// /// Campaign OVERHAUL S2 chunk 5: cell membership is a BORROWED VIEW over /// 's retail per-cell part-entry product /// ( — retail's /// CPartArray::AddPartsShadow 0x00517e40 output, keyed by the SAME /// CELLARRAY /// answers). This class no longer sweeps the scene's static/dynamic indices /// to REBUILD membership: for a queried cell id, /// walks the registry's per-cell entries (already in retail /// CELLARRAY-then-part-array insertion order), collapses them to their /// distinct owning entity ids in that same order, and resolves each id back /// to its current through /// — a presentation-side /// lookup by the SAME the /// registry is keyed by (chunk 5's App-side addition to /// ArchRenderScene). Statics vs. dynamics are the same /// ProjectionClass split ArchRenderScene itself uses (a live /// dynamic root or an equipped child is "dynamic"; every other class is /// "static"), and building shells are excluded — they draw at their own /// building's shell turn (retail CPhysicsPart::Draw(parts, 0) /// @0x0059f331), never at the cell's ordinary DrawObjCell turn. /// /// /// /// An entity the registry HAS flooded into this cell but whose projected /// record the presentation journal has not applied yet this frame (the /// transient race between LiveEntityRuntime's projection journal and /// ShadowObjectRegistry's physics-side registration, both driven off /// the same Create/appearance edge but landing through independent /// incremental pipelines) contributes to NO cell — retail draws nothing for /// an object not yet in a cell; there is no second, conservative fallback. /// Every DISTINCT entity id this happens for in one frame is counted once in /// , regardless of how many /// cells its CELLARRAY touches or how many Get* calls observe it. /// /// /// Building shells are the ONE exception left to a per-frame sweep: /// retail's shell/portal machinery is out of S2's scope (the landcell /// building channel), so still reads a /// small per-frame bucket (_shellsByAnchor) filled by a single narrow /// sweep in /// that keeps only IsBuildingShell records — /// it never resolves ordinary static/dynamic membership. /// /// Campaign FW3.4a: , , /// , , and /// materialize their result into /// — a grow-only buffer reset to length 0 once per frame /// in — rather than a fresh RenderProjectionRecord[] /// allocation per distinct cell/anchor per frame (the single largest /// contributor to the FW3.4 perf checkpoint's 14× frame-allocation /// regression, 1.9 MB/frame p50, before the arena existed). /// is the matching grow-on-demand scratch /// buffer collects one cell's filtered records /// into before a single call — the same /// steady-state-zero-allocation shape already /// has. Every segment is /// STRICTLY per-frame scratch — nothing holds one across a frame boundary /// (the driver/populator consume it immediately) — so reusing the same /// backing array's memory next frame is safe. Per-cell RESULTS /// ( etc.) are cleared and re-materialized once per /// frame on first ask, same as before chunk 5. /// internal sealed class WalkProductionWorldData : IWalkFrameWorldData { private readonly WalkBuildingRegistry _buildings; private readonly ShadowObjectRegistry _shadows; private RenderSceneQuery _scene; private uint _tupleLandblockId; private readonly Dictionary _cellCache = new(); private readonly Dictionary _cellDynamicCache = new(); private readonly Dictionary _cellObjectsCache = new(); private readonly Dictionary _facilityShadowProbeSignatures = new(); private readonly Dictionary> _shellsByAnchor = new(); private readonly Dictionary _outdoorMaterialized = new(); private readonly Dictionary _outdoorDynamicsMaterialized = new(); private readonly Dictionary _outdoorObjectsMaterialized = new(); private readonly Dictionary _shellMaterialized = new(); // Campaign OVERHAUL S2 chunk 5: every distinct entity id counted into // UnregisteredRenderMembershipCount this frame, so the SAME entity // touching several cells (or being asked for through both the static and // dynamic Get* pair) is counted once — not once per cell visit. Cleared // in BeginFrame alongside the counter itself. private readonly HashSet _unregisteredEntitiesThisFrame = new(); // The one surviving per-frame sweep: building shells only (see this // type's own doc comment). Statics/dynamics no longer sweep the scene at // all — membership is a borrowed, on-demand view over the registry. private RenderProjectionRecord[] _sweepScratch = new RenderProjectionRecord[1024]; // Campaign OVERHAUL S2 chunk 5: ResolveCellView's grow-on-demand scratch // buffer — collects one cell's filtered records before a single // AppendToArena call. Starts small: a cell's real membership is usually // a handful of parts, not the hundreds an old full-scene sweep held. private RenderProjectionRecord[] _cellViewScratch = new RenderProjectionRecord[64]; // Campaign FW3.4a: the per-frame, grow-only materialization arena — see // this type's own doc comment. private RenderProjectionRecord[] _arena = new RenderProjectionRecord[4096]; private int _arenaLength; internal WalkProductionWorldData( WalkBuildingRegistry buildings, ShadowObjectRegistry shadows) { _buildings = buildings ?? throw new ArgumentNullException(nameof(buildings)); _shadows = shadows ?? throw new ArgumentNullException(nameof(shadows)); } /// /// Campaign OVERHAUL S2 chunk 5: how many DISTINCT entities this frame /// were present in the registry's retail CELLARRAY (so /// named /// them) but had no resolvable yet /// through — the /// transient streaming window where the physics publisher's registration /// outran the presentation journal's applied projection for the same /// Create/appearance edge. Retail has no such gap /// (CEnvCell::init_static_objects installs the CELLARRAY before a /// static is ever drawable) so a nonzero count here reflects two /// independent incremental state machines racing during streaming, not /// steady-state behavior; filed as AD-116, citing the same residency-race /// reasoning AD-49 already accepts for /// 's own outdoor /// seed. Such an entity /// contributes to NO cell this frame — there is no second, conservative /// fallback (chunk 5 removed the authored-parent-cell and /// root-position-cell fallbacks chunk 2/4 carried). Reset to zero at the /// start of every , after that PRIOR frame's /// value has already been reported by the diagnostic line below. /// public int UnregisteredRenderMembershipCount { get; private set; } /// Rebuilds the frame's building-shell bucket and clears the /// per-cell caches. Call once per frame before the driver runs. /// / /// were the streaming recenter origin the deleted position-based /// fallbacks used to convert a render-origin-relative position back to /// its true landblock byte (LandscapeCellId). Campaign OVERHAUL /// S2 chunk 5 deleted every position-based fallback — the registry's /// CELLARRAY is already expressed in exact cell ids — so these two /// parameters are accepted (unchanged call-site signature) but no /// longer stored or read. internal void BeginFrame( RenderSceneQuery scene, uint tupleLandblockId, int renderCenterLbX, int renderCenterLbY) { // Campaign OVERHAUL S2 chunk 5: report the PRIOR frame's count // before resetting it — the count is only final once that frame's // walk (a sequence of on-demand Get* calls this class has no other // "frame is done" hook for) has finished, so this is necessarily a // one-frame-delayed report, matching every other per-second/per- // change probe in this file family (print-only; never influences // admission). if (UnregisteredRenderMembershipCount > 0 && AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled) { Console.WriteLine( $"[walk-membership] unregistered={UnregisteredRenderMembershipCount} " + $"tupleLandblock=0x{_tupleLandblockId:X8}"); } _scene = scene; _tupleLandblockId = tupleLandblockId; _cellCache.Clear(); _cellDynamicCache.Clear(); _cellObjectsCache.Clear(); _outdoorMaterialized.Clear(); _outdoorDynamicsMaterialized.Clear(); _outdoorObjectsMaterialized.Clear(); _shellMaterialized.Clear(); _arenaLength = 0; UnregisteredRenderMembershipCount = 0; _unregisteredEntitiesThisFrame.Clear(); foreach (List bucket in _shellsByAnchor.Values) bucket.Clear(); // Building shells are the landcell BUILDING channel (out of S2): // retail draws a shell at its own DrawBuilding turn, never through // the cell's ordinary shadow_part_list walk (Contract C), so this // is the one sweep that survives chunk 5 — it exists ONLY to bucket // IsBuildingShell records by their authored anchor, never to // resolve ordinary static/dynamic cell membership. int required = _scene.IndexCounts.For(RenderSceneIndex.OutdoorStatic); if (required > _sweepScratch.Length) { _sweepScratch = new RenderProjectionRecord[ Math.Max(required, _sweepScratch.Length * 2)]; } int count = _scene.CopyIndexTo(RenderSceneIndex.OutdoorStatic, _sweepScratch); for (int i = 0; i < count; i++) { ref readonly RenderProjectionRecord record = ref _sweepScratch[i]; if (!record.EntityPayload.IsBuildingShell) continue; uint anchor = BuildingShellBucketCellId(in record); if (!_shellsByAnchor.TryGetValue(anchor, out List? shells)) _shellsByAnchor[anchor] = shells = new List(); shells.Add(record); } } /// Facility Hub / cathedral discriminator: does the given cell's /// borrowed retail render membership contain a record with this exact /// authored SourceId? Reimplemented directly over the registry's /// per-cell entries (chunk 5) rather than reading a swept bucket /// dictionary — 's /// ProbeFacilityStairsEnabled block is the only caller. internal bool StaticBucketContains(uint cellId, uint sourceId) { IReadOnlyList entries = _shadows.GetRetailPartEntriesInCell(cellId); uint previousEntityId = 0; bool havePrevious = false; for (int i = 0; i < entries.Count; i++) { uint entityId = entries[i].EntityId; if (havePrevious && entityId == previousEntityId) continue; previousEntityId = entityId; havePrevious = true; if (_scene.TryGetByLocalEntityId(entityId, out RenderProjectionRecord record) && record.Source.SourceId == sourceId) { return true; } } return false; } public WalkFrameStaticRecords GetCellStatics(uint cellId) { if (_cellCache.TryGetValue(cellId, out WalkFrameStaticRecords cached)) return cached; WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: false); EmitFacilityShadowProbe(records.Records, cellId, "static"); _cellCache[cellId] = records; return records; } public WalkFrameStaticRecords GetCellObjects(uint cellId) { if (_cellObjectsCache.TryGetValue(cellId, out WalkFrameStaticRecords cached)) return cached; WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: null); EmitFacilityShadowProbe(records.Records, cellId, "combined"); _cellObjectsCache[cellId] = records; return records; } public WalkFrameStaticRecords GetCellDynamics(uint cellId) { if (_cellDynamicCache.TryGetValue(cellId, out WalkFrameStaticRecords cached)) return cached; WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: true); EmitFacilityShadowProbe(records.Records, cellId, "dynamic"); _cellDynamicCache[cellId] = records; return records; } public WalkFrameStaticRecords GetOutdoorStatics(uint cellId) { if (_outdoorMaterialized.TryGetValue(cellId, out WalkFrameStaticRecords cached)) return cached; WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: false); _outdoorMaterialized[cellId] = records; return records; } public WalkFrameStaticRecords GetOutdoorDynamics(uint cellId) { if (_outdoorDynamicsMaterialized.TryGetValue( cellId, out WalkFrameStaticRecords cached)) { return cached; } WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: true); _outdoorDynamicsMaterialized[cellId] = records; return records; } public WalkFrameStaticRecords GetOutdoorObjects(uint cellId) { if (_outdoorObjectsMaterialized.TryGetValue( cellId, out WalkFrameStaticRecords cached)) { return cached; } WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: null); _outdoorObjectsMaterialized[cellId] = records; return records; } /// /// Campaign OVERHAUL S2 chunk 5: one cell id's borrowed retail render /// membership. Reads /// — already in retail CELLARRAY-then-part-array insertion order — /// collapses adjacent entries down to their distinct owning entity ids /// (an entity's own entries for one cell are always written as one /// contiguous run: PublishRetailPartEntries removes then re-adds a /// whole entity's rows atomically, never interleaving two entities' /// rows), resolves each id to its current projected record through /// , and keeps only /// the records matching 's static/dynamic /// class and excluding building shells. Indoor vs. outdoor is entirely a /// property of WHICH cellId the caller passes (an indoor cell id's low /// word is >= 0x100, an outdoor one is in [1, 64] — /// retail's own two id spaces): the registry already published this /// entity into every crossed cell under its own true id, so no /// additional indoor/outdoor dispatch is needed here. /// private WalkFrameStaticRecords ResolveCellView(uint cellId, bool? dynamic) { IReadOnlyList entries = _shadows.GetRetailPartEntriesInCell(cellId); if (entries.Count == 0) return WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId }; int written = 0; uint previousEntityId = 0; bool havePrevious = false; for (int i = 0; i < entries.Count; i++) { uint entityId = entries[i].EntityId; if (havePrevious && entityId == previousEntityId) continue; previousEntityId = entityId; havePrevious = true; if (!_scene.TryGetByLocalEntityId(entityId, out RenderProjectionRecord record)) { // The transient streaming window (this type's own doc // comment): the registry already flooded this entity into // its retail CELLARRAY, but the presentation journal has // not applied its projected record yet this frame. Retail // draws nothing for an object not yet in a cell — no second // fallback. if (_unregisteredEntitiesThisFrame.Add(entityId)) UnregisteredRenderMembershipCount++; continue; } if (record.EntityPayload.IsBuildingShell) continue; // buildings draw at their own shell turn. if (dynamic.HasValue && IsDynamicProjectionClass(record.ProjectionClass) != dynamic.Value) continue; if (written == _cellViewScratch.Length) { var grown = new RenderProjectionRecord[_cellViewScratch.Length * 2]; Array.Copy(_cellViewScratch, grown, written); _cellViewScratch = grown; } _cellViewScratch[written++] = record; } return written == 0 ? WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId } : new WalkFrameStaticRecords( AppendToArena(_cellViewScratch.AsSpan(0, written)), _tupleLandblockId); } /// The same static/dynamic split ArchRenderScene's own /// internal indexing uses: a live dynamic root or an equipped child is /// "dynamic"; every other (including /// ) is /// "static". private static bool IsDynamicProjectionClass(RenderProjectionClass projectionClass) => projectionClass is RenderProjectionClass.LiveDynamicRoot or RenderProjectionClass.EquippedChild; /// /// Facility Hub discriminator for retail's cross-cell render-shadow path. /// The scene query is currently keyed by authored parent cell, while retail /// also appends each object's parts to every cell in its physics CELLARRAY /// through CPhysicsObj::add_shadows_to_cells / CPartArray::AddPartsShadow. /// This probe prints the authoritative physics owner set without changing /// admission, so a correction is made only if the live staircase/player /// registration proves that path is populated. /// private void EmitFacilityShadowProbe( ReadOnlySpan records, uint queriedCellId, string route) { if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled) return; for (int recordIndex = 0; recordIndex < records.Length; recordIndex++) { ref readonly RenderProjectionRecord record = ref records[recordIndex]; bool targetPlayer = record.EntityPayload.CasterIdentity == RenderCasterIdentityKind.LocalPlayer; bool targetStair = record.Source.SourceId == 0x02000623u; bool targetCathedralStair = record.Source.SourceId == 0x020009A2u; if (!targetStair && !targetCathedralStair && !targetPlayer) continue; IReadOnlyList ownerCells = _shadows.GetOwnerCells(record.Source.LocalEntityId); var cells = new StringBuilder(ownerCells.Count * 11 + 2); cells.Append('['); for (int cellIndex = 0; cellIndex < ownerCells.Count; cellIndex++) { if (cellIndex != 0) cells.Append(','); cells.Append("0x").Append(ownerCells[cellIndex].ToString("X8")); } cells.Append(']'); string key = $"{route}:{queriedCellId:X8}:" + $"{record.Source.LocalEntityId:X8}"; string signature = $"{record.Source.ParentCellId:X8}:{cells}"; if (_facilityShadowProbeSignatures.TryGetValue(key, out string? prior) && string.Equals(prior, signature, StringComparison.Ordinal)) { continue; } _facilityShadowProbeSignatures[key] = signature; Console.WriteLine( $"[facility-shadow] route={route} " + $"kind={(targetPlayer ? "player" : targetCathedralStair ? "cathedral-stair" : "stair")} " + $"guid=0x{record.Source.ServerGuid:X8} " + $"local=0x{record.Source.LocalEntityId:X8} " + $"query=0x{queriedCellId:X8} " + $"parent=0x{record.Source.ParentCellId:X8} " + $"owners={cells}"); } } public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building) { uint anchor = BuildingShellBucketCellId(building); if (anchor == 0) return WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId }; if (_shellMaterialized.TryGetValue(anchor, out WalkFrameStaticRecords cached)) return cached; WalkFrameStaticRecords records = _shellsByAnchor.TryGetValue(anchor, out List? shells) && shells.Count > 0 ? new WalkFrameStaticRecords( AppendToArena(CollectionsMarshal.AsSpan(shells)), _tupleLandblockId) : WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId }; _shellMaterialized[anchor] = records; return records; } /// Copies into at /// its current length, growing the arena first if needed (doubling, or /// exactly enough for an unusually large sweep — the same growth shape /// / already use), /// and returns the segment the copy landed in. A prior frame's growth can /// leave an earlier-returned segment pointing at a retired backing array /// — harmless, since that array's content stays valid and nothing reads /// a segment across a frame boundary (see this type's own doc /// comment). private ArraySegment AppendToArena( ReadOnlySpan source) { if (source.Length == 0) return ArraySegment.Empty; int required = _arenaLength + source.Length; if (required > _arena.Length) { var grown = new RenderProjectionRecord[Math.Max(required, _arena.Length * 2)]; Array.Copy(_arena, grown, _arenaLength); _arena = grown; } source.CopyTo(_arena.AsSpan(_arenaLength, source.Length)); var segment = new ArraySegment(_arena, _arenaLength, source.Length); _arenaLength += source.Length; return segment; } /// The building's authored shell anchor: its first non-exit /// portal's destination cell — the SAME rule LandblockLoader used /// when it stamped BuildingShellAnchorCellId on the shell entity. internal static uint AnchorCellId(WalkBuilding building) { foreach (ref readonly WalkBldPortal portal in building.Portals.AsSpan()) { if (portal.OtherCellId != 0xFFFFFFFFu) return portal.OtherCellId; } return 0; } /// /// Selects the retained-scene bucket for a shell record. Portal-bearing /// buildings use their authored EnvCell anchor. A portal-less building has /// no such anchor, but it is still a real CBuildingObj; retail's /// DrawSortCell reaches it through the outdoor cell containing its /// placement, represented by EffectCellId on the retained record. /// internal static uint BuildingShellBucketCellId(in RenderProjectionRecord record) => record.Source.BuildingShellAnchorCellId != 0 ? record.Source.BuildingShellAnchorCellId : record.Source.EffectCellId; /// /// Resolves the same bucket from the walk-side building. The authored /// interior anchor wins when present; otherwise the landscape assembler's /// exact position cell is the shell turn that retail uses. /// internal static uint BuildingShellBucketCellId(WalkBuilding building) { ArgumentNullException.ThrowIfNull(building); uint anchor = AnchorCellId(building); return anchor != 0 ? anchor : building.PositionCellId; } public Matrix4x4 GetBuildingWorldTransform(WalkBuilding building) { if (!_buildings.TryGetEntry(building, out WalkBuildingFactory.Entry? entry)) { throw new InvalidOperationException( $"walk building 0x{building.PositionCellId:X8} has no committed registry entry"); } return entry.PartZeroWorldTransform; } }