Retail never clips a particle to a portal view: each emitter's polys
join the ONE alpha list during its owner cell's far-to-near walk turn
(LScape::draw @0x00506330 iterates block_draw_list reversed; DrawBlock
@0x005A17C0 walks cells; ShouldDrawParticles @0x0050FE60 gates by cell
and distance), and occlusion is the depth test at FlushAlphaList
@0x0059D2E0 (its float is a COUNT threshold - 0f = flush all). The
1d2f2f73 architecture instead re-submitted particles once per
OutsideView slice under that slice's hardware clip slot, which cut
effects at aperture boundaries and drew nothing when no outside slice
was in view (the cathedral look-north disappearance).
Now: unattached emitters submit once per frame by owner-cell kind
(outdoor landcells in the landscape stage, interior EnvCells in the
final world scope - new UnattachedEmitterCellScope filter); cell,
shell-route, barrier-static, and late-stage owners submit their
per-slice cone-cull UNION once with clipSlot 0; and particles emit in
the stage matching their PARENT CELL - an interior dynamic whose
sphere straddles an exit-portal plane keeps its mesh in both stages
(#118) but its particles move to the final pass, so the interior
stage can no longer repaint over them (the aperture-band star cut).
Also lands the inert Change-2 primitives for the AP-236 retirement
(candle-behind-door): RetailAlphaQueue.FlushFartherThan drains only
the far prefix without resetting sources, plus the executor
passthrough and the conservative look-in threshold helper - nothing
calls them yet.
User-gated 2026-08-29 round 2 at the Sanctuary Cathedral: spell and
recall stars cover the whole room at every camera direction including
north; waterfall containment holds on retail's depth/seal mechanism;
adjacent-room particles/lights, walls, Holtburg, recall unregressed
(paperdoll remains pre-existing intermittent #443). Register: AP-236
filed for the remaining barrier-order divergence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
829 lines
31 KiB
C#
829 lines
31 KiB
C#
using System.Numerics;
|
|
using System.Reflection;
|
|
using AcDream.App.Composition;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.App.Tests.Architecture;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public sealed class RetailPViewPassExecutorTests
|
|
{
|
|
[Fact]
|
|
public void DrawInside_outdoor_executes_the_real_typed_sequence_without_interior_clear()
|
|
{
|
|
var renderer = new RetailPViewRenderer();
|
|
using var executor = new RecordingExecutor();
|
|
LoadedCell root = OutdoorCellNode.Build(0xA9B40000u);
|
|
|
|
renderer.DrawInside(Frame(root), executor);
|
|
|
|
Assert.Equal(
|
|
[
|
|
"begin",
|
|
"assemble",
|
|
"append-look-in-clips",
|
|
"prepare-clip:3",
|
|
"indoor-routing",
|
|
"prepare-cells",
|
|
"diagnostics",
|
|
"terrain-clip",
|
|
"clear-routing",
|
|
"landscape-early",
|
|
"terrain-clip",
|
|
"clear-routing",
|
|
"landscape-late",
|
|
"unattached-particles-outdoor",
|
|
"landscape-alpha",
|
|
"indoor-routing",
|
|
"indoor-routing",
|
|
"exit-mask",
|
|
"indoor-routing",
|
|
"opaque-shells",
|
|
"unattached-particles-interior",
|
|
],
|
|
executor.Operations);
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawInside_interior_exit_flushes_before_clear_and_resets_borrowed_result()
|
|
{
|
|
var renderer = new RetailPViewRenderer();
|
|
using var executor = new RecordingExecutor();
|
|
LoadedCell outdoor = OutdoorCellNode.Build(0xA9B40000u);
|
|
RetailPViewFrameResult borrowed = renderer.DrawInside(Frame(outdoor), executor);
|
|
Assert.Contains(outdoor.CellId, borrowed.DrawableCells);
|
|
|
|
executor.Operations.Clear();
|
|
LoadedCell interior = InteriorWithExit(0xA9B40100u);
|
|
RetailPViewFrameResult reused = renderer.DrawInside(Frame(interior), executor);
|
|
|
|
Assert.Same(borrowed, reused);
|
|
Assert.DoesNotContain(outdoor.CellId, reused.DrawableCells);
|
|
Assert.Contains(interior.CellId, reused.DrawableCells);
|
|
AssertAppearsInOrder(
|
|
string.Join('|', executor.Operations),
|
|
"landscape-early",
|
|
"landscape-late",
|
|
"unattached-particles-outdoor",
|
|
"landscape-alpha",
|
|
"interior-depth-clear",
|
|
"indoor-routing",
|
|
"exit-mask");
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawInside_interior_without_an_outside_slice_skips_landscape_and_depth_clear()
|
|
{
|
|
var renderer = new RetailPViewRenderer();
|
|
using var executor = new RecordingExecutor();
|
|
var root = new LoadedCell
|
|
{
|
|
CellId = 0xA9B40100u,
|
|
WorldTransform = Matrix4x4.Identity,
|
|
InverseWorldTransform = Matrix4x4.Identity,
|
|
};
|
|
|
|
renderer.DrawInside(Frame(root), executor);
|
|
|
|
Assert.DoesNotContain("terrain-clip", executor.Operations);
|
|
Assert.DoesNotContain("landscape-early", executor.Operations);
|
|
Assert.DoesNotContain("landscape-late", executor.Operations);
|
|
Assert.DoesNotContain("landscape-alpha", executor.Operations);
|
|
Assert.DoesNotContain("interior-depth-clear", executor.Operations);
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawInside_interior_without_an_outside_slice_still_draws_interior_unattached_particles()
|
|
{
|
|
// Repro (Sanctuary middle cell, looking north): spell ground effects
|
|
// vanished whenever no exit portal was in view, because unattached
|
|
// emitters submitted once PER outside slice under that slice's
|
|
// hardware clip slot — zero slices meant zero submissions. Retail
|
|
// draws such an emitter during its owner cell's walk turn
|
|
// (ShouldDrawParticles @0x0050FE60) and never clips it to a view.
|
|
var renderer = new RetailPViewRenderer();
|
|
using var executor = new RecordingExecutor();
|
|
var root = new LoadedCell
|
|
{
|
|
CellId = 0xA9B40100u,
|
|
WorldTransform = Matrix4x4.Identity,
|
|
InverseWorldTransform = Matrix4x4.Identity,
|
|
};
|
|
|
|
renderer.DrawInside(Frame(root), executor);
|
|
|
|
Assert.Contains("unattached-particles-interior", executor.Operations);
|
|
Assert.DoesNotContain(
|
|
"unattached-particles-outdoor",
|
|
executor.Operations);
|
|
}
|
|
|
|
[Fact]
|
|
public void Particle_classifications_reset_before_an_empty_following_frame()
|
|
{
|
|
var classifications = new RetailPViewParticleClassifications();
|
|
classifications.ReplaceOutdoor([Entity(7u)]);
|
|
classifications.Visible.Add(8u);
|
|
classifications.Dynamics.Add(9u);
|
|
|
|
classifications.BeginFrame();
|
|
|
|
Assert.Empty(classifications.Outdoor);
|
|
Assert.Empty(classifications.Visible);
|
|
Assert.Empty(classifications.Dynamics);
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawInside_real_fixtures_reach_entity_particle_and_transparent_shell_operations()
|
|
{
|
|
var renderer = new RetailPViewRenderer();
|
|
using var executor = new RecordingExecutor { HasTransparentShells = true };
|
|
LoadedCell interior = InteriorWithExit(0xA9B40100u);
|
|
WorldEntity cellStatic = Entity(10u, parentCellId: interior.CellId);
|
|
WorldEntity dynamic = Entity(
|
|
11u,
|
|
serverGuid: 0x80000011u,
|
|
parentCellId: interior.CellId);
|
|
|
|
renderer.DrawInside(Frame(interior, [cellStatic, dynamic]), executor);
|
|
|
|
Assert.Contains("opaque-shells", executor.Operations);
|
|
Assert.Contains("transparent-shells-ordered", executor.Operations);
|
|
Assert.Contains("entity-bucket", executor.Operations);
|
|
Assert.Contains("cell-particles", executor.Operations);
|
|
Assert.Contains("dynamics-particles", executor.Operations);
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawInside_referee_records_the_exact_routed_entity_buckets()
|
|
{
|
|
var oracle = new CurrentRenderSceneOracle();
|
|
var renderer = new RetailPViewRenderer(oracle);
|
|
using var executor = new RecordingExecutor();
|
|
LoadedCell interior = InteriorWithExit(0xA9B40100u);
|
|
WorldEntity cellStatic = Entity(20u, parentCellId: interior.CellId);
|
|
WorldEntity dynamic = Entity(
|
|
21u,
|
|
serverGuid: 0x80000021u,
|
|
parentCellId: interior.CellId);
|
|
|
|
renderer.DrawInside(Frame(interior, [cellStatic, dynamic]), executor);
|
|
|
|
Assert.Equal(1uL, oracle.Snapshot.CompletedPViewFrameSequence);
|
|
Assert.Equal(2, oracle.Snapshot.PViewCandidateCount);
|
|
Assert.Collection(
|
|
oracle.PViewCandidates,
|
|
candidate =>
|
|
{
|
|
Assert.Equal(CurrentRenderPViewRoute.CellStatic, candidate.Route);
|
|
Assert.Equal(cellStatic.Id, candidate.Projection.EntityId);
|
|
},
|
|
candidate =>
|
|
{
|
|
Assert.Equal(CurrentRenderPViewRoute.DynamicLast, candidate.Route);
|
|
Assert.Equal(dynamic.Id, candidate.Projection.EntityId);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawInside_production_product_supplies_the_ordered_entity_routes()
|
|
{
|
|
LoadedCell interior = InteriorWithExit(0xA9B40100u);
|
|
WorldEntity cellStatic = Entity(
|
|
30u,
|
|
parentCellId: interior.CellId);
|
|
WorldEntity dynamic = Entity(
|
|
31u,
|
|
serverGuid: 0x80000031u,
|
|
parentCellId: interior.CellId);
|
|
uint landblockId = interior.CellId & 0xFFFF0000u;
|
|
var oracle = new CurrentRenderSceneOracle();
|
|
using var shadow = new RenderSceneShadowRuntime(
|
|
RenderSceneGeneration.FromRaw(1));
|
|
RenderProjectionRecord staticRecord =
|
|
RenderProjectionRecordFactory.ProjectEntity(
|
|
StaticRenderProjectionJournal.StaticEntityId(
|
|
landblockId,
|
|
cellStatic.Id),
|
|
RenderProjectionClass.IndoorCellStatic,
|
|
RenderOwnerIncarnation.FromRaw(cellStatic.Id),
|
|
landblockId,
|
|
interior.CellId,
|
|
cellStatic,
|
|
spatiallyVisible: true);
|
|
RenderProjectionRecord dynamicRecord =
|
|
RenderProjectionRecordFactory.ProjectEntity(
|
|
LiveRenderProjectionJournal.ProjectionId(dynamic.Id),
|
|
RenderProjectionClass.LiveDynamicRoot,
|
|
RenderOwnerIncarnation.FromRaw(dynamic.Id),
|
|
landblockId,
|
|
interior.CellId,
|
|
dynamic,
|
|
spatiallyVisible: true);
|
|
shadow.Journal.Register(in staticRecord);
|
|
shadow.Journal.Register(in dynamicRecord);
|
|
shadow.DrainUpdateBoundary();
|
|
var product = new RenderScenePViewFrameProductController(
|
|
shadow,
|
|
oracle);
|
|
var renderer = new RetailPViewRenderer(oracle, product);
|
|
using var executor = new RecordingExecutor();
|
|
|
|
renderer.DrawInside(
|
|
Frame(interior, [cellStatic, dynamic]),
|
|
executor);
|
|
|
|
AssertAppearsInOrder(
|
|
string.Join('|', executor.Operations),
|
|
"entity-frame-begin",
|
|
"entity-route:CellStatic:0:00000000",
|
|
"entity-route:DynamicLast:0:00000000",
|
|
"entity-frame-complete");
|
|
Assert.Equal(0, product.Snapshot.MismatchCount);
|
|
Assert.Equal(
|
|
product.Snapshot.ExpectedDigest,
|
|
product.Snapshot.ActualDigest);
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawInside_production_product_uses_retained_routes_without_legacy_partition_when_diagnostics_are_disabled()
|
|
{
|
|
LoadedCell interior = InteriorWithExit(0xA9B40100u);
|
|
WorldEntity dynamic = Entity(
|
|
32u,
|
|
serverGuid: 0x80000032u,
|
|
parentCellId: interior.CellId);
|
|
uint landblockId = interior.CellId & 0xFFFF0000u;
|
|
using var shadow = new RenderSceneShadowRuntime(
|
|
RenderSceneGeneration.FromRaw(1));
|
|
RenderProjectionRecord dynamicRecord =
|
|
RenderProjectionRecordFactory.ProjectEntity(
|
|
LiveRenderProjectionJournal.ProjectionId(dynamic.Id),
|
|
RenderProjectionClass.LiveDynamicRoot,
|
|
RenderOwnerIncarnation.FromRaw(dynamic.Id),
|
|
landblockId,
|
|
interior.CellId,
|
|
dynamic,
|
|
spatiallyVisible: true);
|
|
shadow.Journal.Register(in dynamicRecord);
|
|
shadow.DrainUpdateBoundary();
|
|
var product = new RenderScenePViewFrameProductController(shadow);
|
|
var renderer = new RetailPViewRenderer(null, product);
|
|
using var executor = new RecordingExecutor();
|
|
|
|
renderer.DrawInside(Frame(interior, [dynamic]), executor);
|
|
|
|
AssertAppearsInOrder(
|
|
string.Join('|', executor.Operations),
|
|
"entity-frame-begin",
|
|
"entity-route:DynamicLast:0:00000000",
|
|
"entity-frame-complete");
|
|
Assert.Equal(0uL, product.Snapshot.ComparisonCount);
|
|
Assert.Equal(1, product.Snapshot.ActualCandidateCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawInside_interior_root_executes_the_nearby_building_look_in_punch()
|
|
{
|
|
var renderer = new RetailPViewRenderer();
|
|
using var executor = new RecordingExecutor();
|
|
LoadedCell root = InteriorWithExit(0xA9B40100u);
|
|
root.BuildingId = 1u;
|
|
LoadedCell[] building = NearbyTwoCellBuilding();
|
|
|
|
renderer.DrawInside(
|
|
Frame(
|
|
root,
|
|
nearbyBuildingCells: building,
|
|
additionalCells: building),
|
|
executor);
|
|
|
|
Assert.Contains("look-in-punch", executor.Operations);
|
|
AssertAppearsInOrder(
|
|
string.Join('|', executor.Operations),
|
|
"landscape-early",
|
|
"unattached-particles-outdoor",
|
|
"landscape-static-particles",
|
|
"landscape-alpha",
|
|
"look-in-punch",
|
|
"landscape-late",
|
|
"landscape-alpha",
|
|
"interior-depth-clear");
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawInside_repaints_the_exterior_building_shell_after_its_look_in()
|
|
{
|
|
var renderer = new RetailPViewRenderer();
|
|
using var executor = new RecordingExecutor();
|
|
LoadedCell root = InteriorWithExit(0xA9B40100u);
|
|
root.BuildingId = 1u;
|
|
LoadedCell[] building = NearbyTwoCellBuilding();
|
|
WorldEntity exteriorShell = Entity(
|
|
0x700u,
|
|
isBuildingShell: true,
|
|
buildingShellAnchorCellId: building[0].CellId);
|
|
|
|
renderer.DrawInside(
|
|
Frame(
|
|
root,
|
|
[exteriorShell],
|
|
nearbyBuildingCells: building,
|
|
additionalCells: building),
|
|
executor);
|
|
|
|
AssertAppearsInOrder(
|
|
string.Join('|', executor.Operations),
|
|
"landscape-early",
|
|
"look-in-punch",
|
|
"landscape-building-shell",
|
|
"landscape-late");
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawInside_pairs_each_look_in_with_only_its_own_shell_and_alpha_barrier()
|
|
{
|
|
var renderer = new RetailPViewRenderer();
|
|
using var executor = new RecordingExecutor();
|
|
LoadedCell root = InteriorWithExit(0xA9B40100u);
|
|
root.BuildingId = 1u;
|
|
LoadedCell[] first = NearbyTwoCellBuilding();
|
|
LoadedCell[] second = NearbyTwoCellBuilding(
|
|
0xA9B40172u,
|
|
0xA9B40173u,
|
|
buildingId: 3u);
|
|
LoadedCell[] buildings = [.. first, .. second];
|
|
WorldEntity[] shells =
|
|
[
|
|
Entity(
|
|
0x700u,
|
|
isBuildingShell: true,
|
|
buildingShellAnchorCellId: first[0].CellId),
|
|
Entity(
|
|
0x701u,
|
|
isBuildingShell: true,
|
|
buildingShellAnchorCellId: second[0].CellId),
|
|
];
|
|
|
|
renderer.DrawInside(
|
|
Frame(
|
|
root,
|
|
shells,
|
|
nearbyBuildingCells: buildings,
|
|
additionalCells: buildings),
|
|
executor);
|
|
|
|
AssertAppearsInOrder(
|
|
string.Join('|', executor.Operations),
|
|
"look-in-punch",
|
|
"landscape-building-shell",
|
|
"landscape-static-particles",
|
|
"landscape-alpha",
|
|
"look-in-punch",
|
|
"landscape-building-shell");
|
|
}
|
|
|
|
[Fact]
|
|
public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner()
|
|
{
|
|
Assert.DoesNotContain(
|
|
typeof(RetailPViewFrameInput).GetProperties(),
|
|
property => typeof(Delegate).IsAssignableFrom(property.PropertyType));
|
|
|
|
FieldInfo[] fields = typeof(RetailPViewPassExecutor).GetFields(
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(CellVisibility));
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameInput));
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameResult));
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(ClipFrameAssembly));
|
|
Assert.DoesNotContain(
|
|
fields,
|
|
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
|
}
|
|
|
|
[Fact]
|
|
public void Concrete_executor_forwards_frame_reset_and_brackets_terrain_diagnostics()
|
|
{
|
|
MethodInfo begin = typeof(RetailPViewPassExecutor).GetMethod(
|
|
nameof(RetailPViewPassExecutor.BeginFrame))!;
|
|
IReadOnlyList<CompiledCall> beginCalls = CompiledCallGraph.Read(begin);
|
|
Assert.True(
|
|
CompiledCallGraph.IndexOf(
|
|
beginCalls,
|
|
typeof(RetailPViewParticleClassifications),
|
|
nameof(RetailPViewParticleClassifications.BeginFrame)) >= 0);
|
|
|
|
MethodInfo landscape = typeof(RetailPViewPassExecutor).GetMethod(
|
|
nameof(RetailPViewPassExecutor.DrawLandscapeSlice))!;
|
|
IReadOnlyList<CompiledCall> landscapeCalls = CompiledCallGraph.Read(landscape);
|
|
int diagnosticsBegin = RequiredCallIndex(
|
|
landscapeCalls,
|
|
typeof(TerrainDrawDiagnosticsController),
|
|
nameof(TerrainDrawDiagnosticsController.Begin));
|
|
int terrainDraw = RequiredCallIndex(
|
|
landscapeCalls,
|
|
typeof(TerrainModernRenderer),
|
|
nameof(TerrainModernRenderer.Draw));
|
|
int diagnosticsComplete = RequiredCallIndex(
|
|
landscapeCalls,
|
|
typeof(TerrainDrawDiagnosticsController),
|
|
nameof(TerrainDrawDiagnosticsController.Complete));
|
|
|
|
Assert.True(diagnosticsBegin < terrainDraw);
|
|
Assert.True(terrainDraw < diagnosticsComplete);
|
|
}
|
|
|
|
[Fact]
|
|
public void GameWindow_composes_one_executor_instead_of_a_draw_callback_bag()
|
|
{
|
|
MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
|
|
"ComposeCore",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
|
|
|
|
int executor = RequiredCallIndex(
|
|
calls,
|
|
typeof(RetailPViewPassExecutor),
|
|
".ctor");
|
|
int renderer = RequiredCallIndex(
|
|
calls,
|
|
typeof(WorldScenePViewRenderer),
|
|
".ctor");
|
|
|
|
Assert.True(executor < renderer);
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
|
|
&& call.Target.Name == ".ctor");
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(WorldScenePViewRenderer)
|
|
&& call.Target.Name == ".ctor");
|
|
}
|
|
|
|
private static RetailPViewFrameInput Frame(
|
|
LoadedCell root,
|
|
IReadOnlyList<WorldEntity>? entities = null,
|
|
IReadOnlyList<LoadedCell>? nearbyBuildingCells = null,
|
|
IReadOnlyList<LoadedCell>? additionalCells = null)
|
|
{
|
|
var cells = new Dictionary<uint, LoadedCell> { [root.CellId] = root };
|
|
if (additionalCells is not null)
|
|
{
|
|
foreach (LoadedCell cell in additionalCells)
|
|
cells[cell.CellId] = cell;
|
|
}
|
|
|
|
var entries = new List<(
|
|
uint LandblockId,
|
|
Vector3 AabbMin,
|
|
Vector3 AabbMax,
|
|
IReadOnlyList<WorldEntity> Entities,
|
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)>();
|
|
if (entities is not null)
|
|
{
|
|
entries.Add((
|
|
root.CellId & 0xFFFF0000u,
|
|
Vector3.Zero,
|
|
Vector3.Zero,
|
|
entities,
|
|
null));
|
|
}
|
|
|
|
return new RetailPViewFrameInput().Reset(
|
|
root,
|
|
nearbyBuildingCells,
|
|
Vector3.Zero,
|
|
TestCamera.ViewProjection,
|
|
new DictionaryCellSource(cells),
|
|
new TestCamera(),
|
|
Vector3.Zero,
|
|
frustum: null,
|
|
playerLandblockId: root.CellId & 0xFFFF0000u,
|
|
animatedEntityIds: null,
|
|
renderCenterLbX: 0,
|
|
renderCenterLbY: 0,
|
|
renderRadius: 1,
|
|
landblockEntries: entries,
|
|
renderSky: true,
|
|
renderWeather: true,
|
|
dayFraction: 0f,
|
|
activeDayGroup: null,
|
|
skyKeyframe: default,
|
|
environOverrideActive: false,
|
|
viewerCellId: root.CellId,
|
|
playerCellId: root.CellId,
|
|
playerViewPosition: Vector3.Zero,
|
|
cameraView: TestCamera.ViewMatrix,
|
|
cameraCellResolution: CameraCellResolution.None);
|
|
}
|
|
|
|
private static LoadedCell InteriorWithExit(uint cellId)
|
|
{
|
|
var cell = new LoadedCell
|
|
{
|
|
CellId = cellId,
|
|
WorldTransform = Matrix4x4.Identity,
|
|
InverseWorldTransform = Matrix4x4.Identity,
|
|
Portals = [new CellPortalInfo(0xFFFF, 0, 0, 0)],
|
|
};
|
|
cell.PortalPolygons.Add(
|
|
[
|
|
new Vector3(-1f, -1f, -2f),
|
|
new Vector3(1f, -1f, -2f),
|
|
new Vector3(1f, 1f, -2f),
|
|
new Vector3(-1f, 1f, -2f),
|
|
]);
|
|
return cell;
|
|
}
|
|
|
|
private static LoadedCell[] NearbyTwoCellBuilding(
|
|
uint vestibuleId = 0xA9B40170u,
|
|
uint roomId = 0xA9B40171u,
|
|
uint buildingId = 2u)
|
|
{
|
|
var vestibule = new LoadedCell
|
|
{
|
|
CellId = vestibuleId,
|
|
BuildingId = buildingId,
|
|
WorldTransform = Matrix4x4.Identity,
|
|
InverseWorldTransform = Matrix4x4.Identity,
|
|
Portals =
|
|
[
|
|
new CellPortalInfo(0xFFFF, 0, 0, 0),
|
|
new CellPortalInfo((ushort)(roomId & 0xFFFFu), 1, 0, 0),
|
|
],
|
|
ClipPlanes =
|
|
[
|
|
new PortalClipPlane
|
|
{
|
|
Normal = new Vector3(0, 0, 1),
|
|
D = 3f,
|
|
InsideSide = 1,
|
|
},
|
|
],
|
|
};
|
|
vestibule.PortalPolygons.Add(
|
|
[
|
|
new Vector3(-0.5f, -0.5f, -2f),
|
|
new Vector3(0.5f, -0.5f, -2f),
|
|
new Vector3(0.5f, 0.5f, -2f),
|
|
new Vector3(-0.5f, 0.5f, -2f),
|
|
]);
|
|
vestibule.PortalPolygons.Add(
|
|
[
|
|
new Vector3(-0.6f, -0.6f, -4f),
|
|
new Vector3(0.6f, -0.6f, -4f),
|
|
new Vector3(0.6f, 0.6f, -4f),
|
|
new Vector3(-0.6f, 0.6f, -4f),
|
|
]);
|
|
|
|
var room = new LoadedCell
|
|
{
|
|
CellId = roomId,
|
|
BuildingId = buildingId,
|
|
WorldTransform = Matrix4x4.Identity,
|
|
InverseWorldTransform = Matrix4x4.Identity,
|
|
Portals =
|
|
[
|
|
new CellPortalInfo(
|
|
(ushort)(vestibuleId & 0xFFFFu),
|
|
0,
|
|
0,
|
|
1),
|
|
],
|
|
};
|
|
room.PortalPolygons.Add(
|
|
[
|
|
new Vector3(-0.6f, -0.6f, -4f),
|
|
new Vector3(0.6f, -0.6f, -4f),
|
|
new Vector3(0.6f, 0.6f, -4f),
|
|
new Vector3(-0.6f, 0.6f, -4f),
|
|
]);
|
|
return [vestibule, room];
|
|
}
|
|
|
|
private static WorldEntity Entity(
|
|
uint id,
|
|
uint serverGuid = 0,
|
|
uint? parentCellId = null,
|
|
bool isBuildingShell = false,
|
|
uint? buildingShellAnchorCellId = null) => new()
|
|
{
|
|
Id = id,
|
|
ServerGuid = serverGuid,
|
|
SourceGfxObjOrSetupId = 0,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = [new MeshRef(1u, Matrix4x4.Identity)],
|
|
ParentCellId = parentCellId,
|
|
IsBuildingShell = isBuildingShell,
|
|
BuildingShellAnchorCellId = buildingShellAnchorCellId,
|
|
};
|
|
|
|
private static void AssertAppearsInOrder(string source, params string[] needles)
|
|
{
|
|
int cursor = -1;
|
|
foreach (string needle in needles)
|
|
{
|
|
int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal);
|
|
Assert.True(next > cursor, $"Missing or out-of-order fragment: {needle}");
|
|
cursor = next;
|
|
}
|
|
}
|
|
|
|
private static int RequiredCallIndex(
|
|
IReadOnlyList<CompiledCall> calls,
|
|
Type declaringType,
|
|
string methodName)
|
|
{
|
|
int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName);
|
|
Assert.True(index >= 0, $"Missing compiled call: {declaringType.Name}.{methodName}");
|
|
return index;
|
|
}
|
|
|
|
private sealed class DictionaryCellSource(
|
|
IReadOnlyDictionary<uint, LoadedCell> cells) : IRetailPViewCellSource
|
|
{
|
|
public LoadedCell? Find(uint cellId) =>
|
|
cells.TryGetValue(cellId, out LoadedCell? cell) ? cell : null;
|
|
}
|
|
|
|
private sealed class TestCamera : ICamera
|
|
{
|
|
public static Matrix4x4 ViewMatrix { get; } = Matrix4x4.CreateLookAt(
|
|
Vector3.Zero,
|
|
new Vector3(0, 0, -1),
|
|
Vector3.UnitY);
|
|
|
|
public static Matrix4x4 ViewProjection { get; } = ViewMatrix
|
|
* Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1f, 0.1f, 1000f);
|
|
|
|
public Matrix4x4 View => ViewMatrix;
|
|
public Matrix4x4 Projection { get; } =
|
|
Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1f, 0.1f, 1000f);
|
|
public float Aspect { get; set; } = 1f;
|
|
}
|
|
|
|
private sealed class RecordingExecutor :
|
|
IRetailPViewPassExecutor,
|
|
IRenderFrameEntityPassExecutor,
|
|
IDisposable
|
|
{
|
|
public void AbortFrame() => Operations.Add("abort");
|
|
|
|
private readonly ClipFrame _clipFrame = ClipFrame.NoClip();
|
|
|
|
public List<string> Operations { get; } = [];
|
|
public bool HasTransparentShells { get; init; }
|
|
|
|
public void BeginFrame() => Operations.Add("begin");
|
|
public ClipFrameAssembly AssembleClipFrame(
|
|
PortalVisibilityFrame portalFrame,
|
|
ClipFrameAssembly reuseAssembly)
|
|
{
|
|
Operations.Add("assemble");
|
|
return ClipFrameAssembler.Assemble(_clipFrame, portalFrame, reuseAssembly);
|
|
}
|
|
|
|
public void AppendLookInClipFrames(
|
|
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
|
|
ClipFrameAssembly assembly)
|
|
{
|
|
Operations.Add("append-look-in-clips");
|
|
ClipFrameAssembler.AppendLookInFrames(
|
|
_clipFrame,
|
|
lookInFrames,
|
|
assembly);
|
|
}
|
|
|
|
public void PrepareClipFrame(int terrainUploadCount) =>
|
|
Operations.Add($"prepare-clip:{terrainUploadCount}");
|
|
|
|
public void SetTerrainClip(ReadOnlySpan<Vector4> planes) =>
|
|
Operations.Add("terrain-clip");
|
|
|
|
public void ClearClipRouting() => Operations.Add("clear-routing");
|
|
public void UseIndoorMembershipOnlyRouting() => Operations.Add("indoor-routing");
|
|
public void UseCellPortalViewRouting(uint cellId, ClipViewSlice slice) =>
|
|
Operations.Add($"cell-portal-routing:{cellId:X8}:{slice.Slot}");
|
|
public void PrepareCellBatches(RetailPViewFrameInput frame, HashSet<uint> visibleCellIds) =>
|
|
Operations.Add("prepare-cells");
|
|
public void DrawOpaqueCellShells(HashSet<uint> cellIds) => Operations.Add("opaque-shells");
|
|
public bool CellHasTransparentShell(uint cellId) => HasTransparentShells;
|
|
public void DrawTransparentCellShells(HashSet<uint> cellIds) => Operations.Add("transparent-shells");
|
|
public void DrawTransparentCellShellsOrdered(IReadOnlyList<uint> cellIds) =>
|
|
Operations.Add("transparent-shells-ordered");
|
|
public void DrawEntityBucket(
|
|
RetailPViewFrameInput frame,
|
|
IReadOnlyList<WorldEntity> entities,
|
|
HashSet<uint>? visibleCellIds) => Operations.Add("entity-bucket");
|
|
public void EmitClipRouteProbe(
|
|
ClipFrameAssembly clipAssembly,
|
|
ClipViewSlice slice,
|
|
int sliceIndex) => Operations.Add("clip-probe");
|
|
public void DrawLandscapeSlice(
|
|
RetailPViewFrameInput frame,
|
|
RetailPViewLandscapeSliceContext context)
|
|
{
|
|
Operations.Add("landscape-early");
|
|
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
|
|
{
|
|
RenderFrameView view = request.View;
|
|
DrawEntityRoute(
|
|
frame.Camera,
|
|
in view,
|
|
request.Route,
|
|
request.RouteIndex,
|
|
request.CellId,
|
|
request.TupleLandblockId);
|
|
}
|
|
}
|
|
|
|
public void DrawLandscapeSliceLate(
|
|
RetailPViewFrameInput frame,
|
|
RetailPViewLandscapeLateSliceContext context)
|
|
{
|
|
Operations.Add("landscape-late");
|
|
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
|
|
{
|
|
RenderFrameView view = request.View;
|
|
DrawEntityRoute(
|
|
frame.Camera,
|
|
in view,
|
|
request.Route,
|
|
request.RouteIndex,
|
|
request.CellId,
|
|
request.TupleLandblockId);
|
|
}
|
|
}
|
|
|
|
public void DrawLandscapeStaticParticles(
|
|
RetailPViewFrameInput frame,
|
|
RetailPViewLandscapeStaticParticleContext context) =>
|
|
Operations.Add("landscape-static-particles");
|
|
public void DrawLandscapeBuildingShellSlice(
|
|
RetailPViewFrameInput frame,
|
|
RetailPViewLandscapeBuildingShellSliceContext context)
|
|
{
|
|
Operations.Add("landscape-building-shell");
|
|
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
|
|
{
|
|
RenderFrameView view = request.View;
|
|
DrawEntityRoute(
|
|
frame.Camera,
|
|
in view,
|
|
request.Route,
|
|
request.RouteIndex,
|
|
request.CellId,
|
|
request.TupleLandblockId);
|
|
}
|
|
}
|
|
public void ClearInteriorDepth() => Operations.Add("interior-depth-clear");
|
|
public void DrawExitPortalMask(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("exit-mask");
|
|
public void DrawLookInPortalPunch(
|
|
RetailPViewFrameInput frame,
|
|
RetailPViewCellSliceContext context,
|
|
int portalIndex) => Operations.Add("look-in-punch");
|
|
public void DrawUnattachedSceneParticles(
|
|
RetailPViewFrameInput frame,
|
|
bool outdoorCells) => Operations.Add(
|
|
outdoorCells
|
|
? "unattached-particles-outdoor"
|
|
: "unattached-particles-interior");
|
|
public void FlushLandscapeAlpha() => Operations.Add("landscape-alpha");
|
|
public void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("cell-particles");
|
|
public void DrawDynamicsParticles(
|
|
RetailPViewFrameInput frame,
|
|
IReadOnlySet<uint> ownerIds) =>
|
|
Operations.Add("dynamics-particles");
|
|
public void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result) => Operations.Add("diagnostics");
|
|
|
|
public void BeginEntityFrame(in RenderFrameView view) =>
|
|
Operations.Add("entity-frame-begin");
|
|
|
|
public bool DrawEntityRoute(
|
|
ICamera camera,
|
|
in RenderFrameView view,
|
|
RenderFrameCandidateRoute route,
|
|
int routeIndex,
|
|
uint cellId,
|
|
uint tupleLandblockId)
|
|
{
|
|
Operations.Add(
|
|
$"entity-route:{route}:{routeIndex}:{cellId:X8}");
|
|
return true;
|
|
}
|
|
|
|
public void CompleteEntityFrame(in RenderFrameView view) =>
|
|
Operations.Add("entity-frame-complete");
|
|
|
|
public void AbortEntityFrame() =>
|
|
Operations.Add("entity-frame-abort");
|
|
|
|
public void Dispose() => _clipFrame.Dispose();
|
|
}
|
|
}
|