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>
89 lines
3.4 KiB
C#
89 lines
3.4 KiB
C#
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.Core.World;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public class InteriorEntityPartitionTests
|
|
{
|
|
private const uint CellA = 0xA9B40170;
|
|
private const uint CellB = 0xA9B40171;
|
|
private const uint HiddenCell = 0xA9B40199;
|
|
private const uint OutdoorCell = 0xA9B40020;
|
|
|
|
private static WorldEntity Ent(uint id, uint serverGuid, uint? parentCell) => new()
|
|
{
|
|
Id = id,
|
|
ServerGuid = serverGuid,
|
|
SourceGfxObjOrSetupId = 0x01000001,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = new[] { new MeshRef(0x01000001, Matrix4x4.Identity) },
|
|
ParentCellId = parentCell,
|
|
};
|
|
|
|
private static IEnumerable<(uint, Vector3, Vector3, IReadOnlyList<WorldEntity>,
|
|
IReadOnlyDictionary<uint, WorldEntity>?)> OneLb(uint lbId, params WorldEntity[] ents)
|
|
=> new[] { (lbId, Vector3.Zero, Vector3.Zero, (IReadOnlyList<WorldEntity>)ents,
|
|
(IReadOnlyDictionary<uint, WorldEntity>?)null) };
|
|
|
|
[Fact]
|
|
public void Partitions_LiveAndStaticEntities_ByCellOutdoorAndFallback()
|
|
{
|
|
var unresolvedLive = Ent(1, serverGuid: 0x5000000A, parentCell: null);
|
|
var liveNpcInCell = Ent(2, serverGuid: 0x80001234, parentCell: CellA);
|
|
var staticA = Ent(3, serverGuid: 0, parentCell: CellA);
|
|
var staticB = Ent(4, serverGuid: 0, parentCell: CellB);
|
|
var scenery = Ent(5, serverGuid: 0, parentCell: null);
|
|
var liveOutdoor = Ent(6, serverGuid: 0x80005678, parentCell: OutdoorCell);
|
|
|
|
var visible = new HashSet<uint> { CellA, CellB };
|
|
var result = InteriorEntityPartition.Partition(
|
|
visible, OneLb(0xA9B4FFFF, unresolvedLive, liveNpcInCell, staticA, staticB, scenery, liveOutdoor));
|
|
|
|
Assert.Single(result.LiveDynamic);
|
|
Assert.Contains(unresolvedLive, result.LiveDynamic);
|
|
|
|
Assert.Equal(2, result.ByCell[CellA].Count);
|
|
Assert.Contains(liveNpcInCell, result.ByCell[CellA]);
|
|
Assert.Contains(staticA, result.ByCell[CellA]);
|
|
Assert.Single(result.ByCell[CellB]);
|
|
Assert.Contains(staticB, result.ByCell[CellB]);
|
|
|
|
Assert.Equal(2, result.Outdoor.Count);
|
|
Assert.Contains(scenery, result.Outdoor);
|
|
Assert.Contains(liveOutdoor, result.Outdoor);
|
|
}
|
|
|
|
[Fact]
|
|
public void IndoorEntity_InNonVisibleCell_IsDropped()
|
|
{
|
|
var staticHidden = Ent(3, serverGuid: 0, parentCell: HiddenCell);
|
|
var liveHidden = Ent(4, serverGuid: 0x80001234, parentCell: HiddenCell);
|
|
var visible = new HashSet<uint> { CellA };
|
|
|
|
var result = InteriorEntityPartition.Partition(
|
|
visible, OneLb(0xA9B4FFFF, staticHidden, liveHidden));
|
|
|
|
Assert.False(result.ByCell.ContainsKey(HiddenCell));
|
|
Assert.Empty(result.Outdoor);
|
|
Assert.Empty(result.LiveDynamic);
|
|
}
|
|
|
|
[Fact]
|
|
public void EntityWithNoMeshRefs_IsSkipped()
|
|
{
|
|
var noMesh = new WorldEntity
|
|
{
|
|
Id = 9, ServerGuid = 0, SourceGfxObjOrSetupId = 0x01000001,
|
|
Position = Vector3.Zero, Rotation = Quaternion.Identity,
|
|
MeshRefs = System.Array.Empty<MeshRef>(), ParentCellId = CellA,
|
|
};
|
|
var result = InteriorEntityPartition.Partition(
|
|
new HashSet<uint> { CellA }, OneLb(0xA9B4FFFF, noMesh));
|
|
|
|
Assert.False(result.ByCell.ContainsKey(CellA));
|
|
}
|
|
}
|