Retail draws every cell's objects inside LScape::draw's far-to-near walk (DrawSortCell @0x005A17C0) and drains the one delayed-alpha list at the DrawCells boundary AFTER the finished walk (@0x005A4872). Our outdoor frame drained at the landscape-stage end and then drew punches, interior shells, cell objects, and ALL dynamics — every one of those opaque passes overwrote the already-composited flames (the reopened #132 candle class: "the door draws over the candle", creatures at openings). Depth and barrier A/Bs were no-ops because the eraser is opaque color painted after the drain. Two retail-cited ordering corrections, outdoor-node roots only: 1. The stage-boundary drain is skipped and FlushLandscapeAlpha() runs after DrawDynamicsLast, where the frame's opaque world depth is complete — the one far-to-near list composites over everything, exactly like retail's boundary flush relative to its finished walk. 2. Before DrawExitPortalMasks, FlushLandscapeAlphaFartherThan( ExitPortalMaskBarrierDistance(...)) drains everything at or beyond the nearest cell whose exit-portal mask is about to write far-Z — retail DrawBuilding @0x0059F2A0 runs FlushAlphaList(0f) BEFORE its portal-only pass, so in the far-to-near walk nothing already drained can meet a punched aperture's falsified depth. Without this, the first correction let exterior waterfalls z-pass across punched apertures whose true landscape depth the punch erased (found live at the cathedral gate). Nearer content stays queued and legitimately composites in front of punched structures. Interior roots keep the pre-clear stage-boundary drain unchanged. User-gated live: Holtburg sign candle whole in front of the sign and tower door at the aligned pose; cathedral waterfalls contained at every camera zoom, inside and outside. Register row AP-236 retired (the walk-order outcome reconstruction is complete; AP-34 remains the umbrella for the CYpt-sort reconstruction itself). Filed #456 for the separate occluded-distant-building/creature admission residual this session diagnosed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
837 lines
31 KiB
C#
837 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",
|
|
// #132: an OUTDOOR root does NOT drain at the stage boundary.
|
|
// The far prefix drains at the pre-punch barrier (retail
|
|
// DrawBuilding @0x0059F2A0 flushes before its portal-only
|
|
// far-Z pass), and the full drain runs after the dynamics
|
|
// pass, where the frame's opaque world depth is complete.
|
|
"indoor-routing",
|
|
"indoor-routing",
|
|
"landscape-alpha-farther",
|
|
"exit-mask",
|
|
"indoor-routing",
|
|
"opaque-shells",
|
|
"landscape-alpha",
|
|
"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-farther",
|
|
"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-farther",
|
|
"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 FlushLandscapeAlphaFartherThan(float minViewerDistance) =>
|
|
Operations.Add("landscape-alpha-farther");
|
|
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();
|
|
}
|
|
}
|