Checkpoint of the unified retail-faithful indoor render. The two-week HANG/grey is fixed and the interior seals (live-verified by the user). Commits the session render-rewrite foundation together with the fixes that made it functional. - HANG fix: PortalVisibilityBuilder.Build portal flood did not terminate (the faithful ProjectToClip near-side clip drifts per round, defeating the CellView dedup; the BFS had no bound after U.2a removed MaxReprocessPerCell). Fix = drift-tolerant snapped/canonical CellView.Add dedup (PortalView.cs) plus restored MaxReprocessPerCell=16 bounded re-enqueue (PortalVisibilityBuilder.cs). Re-enqueue is kept (load-bearing for late-slice propagation, Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit); only its count is capped. CellViewDedupTests added. - Seal (DrawCells Task 2): RetailPViewRenderer.DrawEnvCellShells draws EVERY visible cell via IndoorDrawPlan.ShellPass (was gated on the ClipFrameAssembler slot filter, leaving slot-less cells grey). - Look-in FPS: GameWindow exterior look-in candidates limited to the player landblock +-1 (was all ~81 loaded LBs iterated every outdoor frame). No behaviour change (far cells were >48m, already culled). Remaining dominant issue = the FLAP at transitions: viewer-cell metastability (render roots at the camera-eye cell, which oscillates outdoor-indoor as the 3rd-person boom drifts across the doorway, confirmed in render-sig). SEPARATE fix, NOT the DrawCells port. Full handoff + flap fix plan + tracked follow-ups (#78 terrain, look-in-from-inside, look-in FPS, L-spotlight): docs/research/2026-06-07-indoor-render-session-handoff.md. Baselines: build 0 err; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
2.4 KiB
C#
80 lines
2.4 KiB
C#
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// Splits a frame's landblock entities into the draw buckets used by the
|
|
/// retail-style DrawInside flood. Indoor ownership wins for live dynamics too:
|
|
/// a player, NPC, door, or item with a current indoor ParentCellId belongs to
|
|
/// that cell's portal-clipped object list, not a global overlay pass.
|
|
/// </summary>
|
|
public static class InteriorEntityPartition
|
|
{
|
|
public sealed class Result
|
|
{
|
|
public Dictionary<uint, List<WorldEntity>> ByCell { get; } = new();
|
|
public List<WorldEntity> Outdoor { get; } = new();
|
|
public List<WorldEntity> LiveDynamic { get; } = new();
|
|
}
|
|
|
|
public static Result Partition(
|
|
HashSet<uint> visibleCells,
|
|
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
|
IReadOnlyList<WorldEntity> Entities,
|
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> landblockEntries)
|
|
{
|
|
var result = new Result();
|
|
foreach (var entry in landblockEntries)
|
|
{
|
|
foreach (var e in entry.Entities)
|
|
{
|
|
if (e.MeshRefs.Count == 0) continue;
|
|
|
|
if (e.ServerGuid != 0)
|
|
{
|
|
if (e.ParentCellId is uint liveCell)
|
|
AddByCellOrOutdoor(e, liveCell, visibleCells, result);
|
|
else
|
|
result.LiveDynamic.Add(e);
|
|
}
|
|
else if (e.ParentCellId is uint cell)
|
|
{
|
|
AddByCellOrOutdoor(e, cell, visibleCells, result);
|
|
}
|
|
else
|
|
{
|
|
result.Outdoor.Add(e);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static void AddByCellOrOutdoor(
|
|
WorldEntity entity,
|
|
uint cellId,
|
|
HashSet<uint> visibleCells,
|
|
Result result)
|
|
{
|
|
if (!IsIndoorCellId(cellId))
|
|
{
|
|
result.Outdoor.Add(entity);
|
|
return;
|
|
}
|
|
|
|
if (!visibleCells.Contains(cellId))
|
|
return;
|
|
|
|
if (!result.ByCell.TryGetValue(cellId, out var list))
|
|
result.ByCell[cellId] = list = new List<WorldEntity>();
|
|
list.Add(entity);
|
|
}
|
|
|
|
private static bool IsIndoorCellId(uint cellId)
|
|
{
|
|
uint low = cellId & 0xFFFFu;
|
|
return low >= 0x0100u && low != 0xFFFFu;
|
|
}
|
|
}
|