fix(render): clip walk content to authored portal views

This commit is contained in:
Erik 2026-08-31 06:15:12 +02:00
parent 90eba0ec3c
commit 11e68aad82
10 changed files with 408 additions and 78 deletions

View file

@ -43,12 +43,17 @@ public sealed class WalkFrameDriverTests
private sealed class RecordingLeafRenderer(List<string> log) : IWalkFrameLeafRenderer
{
public readonly List<WalkPolygon> Punches = new();
public readonly List<(uint CellId, uint ClipSlot)> Shells = new();
public void DrawSky() => log.Add("SKY");
public void DrawTerrainSlice(int sliceIndex) => log.Add($"TERRAIN:{sliceIndex}");
public void DrawCellShell(uint cellId) => log.Add($"SHELL:{cellId:x8}");
public void DrawCellShell(uint cellId, uint clipSlot)
{
Shells.Add((cellId, clipSlot));
log.Add($"SHELL:{cellId:x8}");
}
public void ClearInteriorDepth() => log.Add("CLEAR");
@ -427,7 +432,9 @@ public sealed class WalkFrameDriverTests
var leaf = new RecordingLeafRenderer(log);
var trace = new RecordingTrace(log);
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData, trace);
using ClipFrame clipFrame = ClipFrame.NoClip();
var driver = new WalkFrameDriver(
fx.Dispatcher, leaf, worldData, trace, clipFrame);
var walk = new RetailFrameWalk();
var activeView = new WalkPortalView();
@ -456,6 +463,13 @@ public sealed class WalkFrameDriverTests
Assert.Equal([0x104u], driver.LookInCellTurns);
Assert.Equal([0x104u], driver.LookInCells);
Assert.Collection(
leaf.Shells,
shell =>
{
Assert.Equal(0x104u, shell.CellId);
Assert.NotEqual(0u, shell.ClipSlot);
});
WalkPortalView capturedView = ctx.Cells[0x104].PortalViews[0];
WalkViewPoly capturedPoly = Assert.Single(capturedView.View.Polys);
Vector2 capturedCenter = Vector2.Zero;
@ -470,6 +484,10 @@ public sealed class WalkFrameDriverTests
0, in insideCone, 0.1f));
Assert.False(driver.SphereVisibleInLookInTurn(
0, new Vector3(10_000f, 0f, 10f), 0.1f));
uint clipSlot = Assert.Single(driver.VisibleClipSlotsInLookInTurn(
0, in insideCone, 0.1f, testSphere: true));
Assert.NotEqual(0u, clipSlot);
Assert.Equal(2, clipFrame.SlotCount);
// The punch polygon reached the leaf renderer in WORLD space: the
// building-local Quad(-2f) vertex (-0.5,-0.5,-2) translates by

View file

@ -48,7 +48,13 @@ public sealed class WalkProductionWorldConformanceTests
private sealed class Recorder : IWalkEventSink
{
public readonly List<WalkEvent> Events = new();
public void Emit(in WalkEvent walkEvent) => Events.Add(walkEvent);
public Action<WalkEvent>? OnEmit { get; init; }
public void Emit(in WalkEvent walkEvent)
{
Events.Add(walkEvent);
OnEmit?.Invoke(walkEvent);
}
}
private static DatCollection OpenDats()
@ -227,6 +233,93 @@ public sealed class WalkProductionWorldConformanceTests
$"production walk diverged from the FW1 test adapter ({fixture})\nEXPECTED: {expected}\nACTUAL: {actual}");
}
[Theory]
[InlineData(0xF4180100u, 36.166267f, 79.828407f)]
[InlineData(0xF4180101u, 36.391270f, 72.167931f)]
public void Cathedral_transition_keeps_south_hall_draws_inside_the_authored_aperture(
uint cameraCellId,
float x,
float y)
{
// Owner's exact 2026-08-31 repro: the remote player and special NPC
// are parented in 0xF4180112, behind opaque cathedral walls. Retail
// hides them on BOTH sides of the 0x100 <-> 0x101 transition. The walk
// legitimately reaches 0x112 through one authored building aperture;
// the regression was submitting each admitted mesh with slot 0, so the
// whole player/NPC escaped that aperture. Preserve the installed-DAT
// fact this fix depends on: every admitted route is a real, bounded
// portal polygon, never a pass-all zero-plane route.
var pose = new WalkOraclePose(
cameraCellId,
new Vector3(x, y, 169.804993f),
Q0: -0.004591f,
Q1: 0f,
Q2: 0f,
Q3: 0.999989f);
using DatCollection dats = OpenDats();
using var adapter = new DatCollectionAdapter(dats);
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
BuildProductionWorld(adapter, pose.CellId, pose.Origin);
var ctx = new WalkTraceReplayContext(pose, cells) { Buildings = buildings };
WalkCell camera = Assert.Contains(cameraCellId, cells);
var remotePlayer = new Vector3(36.299465f, 18.594580f, 169.804993f);
var southHallAdmission = new List<string>();
var recorder = new Recorder
{
OnEmit = e =>
{
if (e.Kind != WalkEventKind.DrawCells
|| !e.Cells.Contains(0xF4180112u))
{
return;
}
WalkPortalView views = cells[0xF4180112u].TopView;
for (int viewIndex = 0; viewIndex < views.ViewCount; viewIndex++)
{
WalkViewPoly poly = views.View.Polys[viewIndex];
var planes = new WalkPlane[poly.VertexCount];
for (int edge = 0; edge < poly.VertexCount; edge++)
{
planes[edge] = views.View.Vertices[
poly.VertexIndex + edge].Plane;
}
WalkBoundingType verdict = WalkVisibilityMath.ViewconeCheck(
remotePlayer,
radius: 1.5f,
ctx.CyPlane,
planes);
float minDistance = planes
.Select(plane => Vector3.Dot(plane.Normal, remotePlayer) + plane.D)
.Append(Vector3.Dot(ctx.CyPlane.Normal, remotePlayer) + ctx.CyPlane.D)
.Min();
southHallAdmission.Add(
$"view={viewIndex} planes={poly.VertexCount} verdict={verdict} min={minDistance:F4}");
}
},
};
new RetailFrameWalk().WalkFrame(
cameraCellId,
camera,
assembler.Landscape,
ctx,
recorder);
WalkEvent[] drawCells = recorder.Events
.Where(static e => e.Kind == WalkEventKind.DrawCells)
.ToArray();
Assert.NotEmpty(drawCells);
Assert.Contains(cameraCellId, drawCells[0].Cells);
Assert.NotEmpty(southHallAdmission);
Assert.Contains(
southHallAdmission,
static verdict => verdict.Contains("verdict=PartiallyInside")
&& !verdict.Contains("planes=0"));
}
[Fact]
public void Foundry_entry_reproduces_every_frame_before_the_f67_order_segment()
{

View file

@ -45,6 +45,22 @@ public sealed class WalkStaticStreamPopulatorTests
Calls.Add((serverGuid, localEntityId, partIndex, gfxObjId, partWorld));
}
private sealed class FixedWalkViews(params uint[] slots) : IWalkLookInViewSource
{
public IReadOnlyList<uint> LookInCellTurns { get; } = [0x8C040112u];
public bool SphereVisibleInLookInTurn(
int routeIndex,
in Vector3 center,
float radius) => slots.Length != 0;
public IReadOnlyList<uint> VisibleClipSlotsInLookInTurn(
int routeIndex,
in Vector3 center,
float radius,
bool testSphere) => slots;
}
// ── Synthetic RenderProjectionRecord construction ──────────────────────
private static RenderProjectionRecord MakeRecord(
@ -212,6 +228,35 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Equal((uint)leavesGfxObj, selectionParts[1].GfxObjId);
}
[Fact]
public void ClassifyEntityForWalk_EmitsOneGpuClippedInstancePerPortalViewSlice()
{
using var fx = new DispatcherFixture();
const ulong gfxObj = 0x0100_0013UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08000013u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
RenderProjectionRecord record = MakeRecord(
201, 0, Vector3.Zero,
[new MeshRef((uint)gfxObj, Matrix4x4.Identity)],
parentCellId: 0x8C040112u);
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
var selectionParts = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
var views = new FixedWalkViews(7u, 9u);
fx.Dispatcher.ClassifyEntityForWalk(
in record,
0x8C04u,
batches,
selectionParts,
liveDynamic: true,
views,
lookInRouteIndex: 0,
lookInCellId: 0x8C040112u);
Assert.Equal([7u, 9u], batches.Select(static batch => batch.ClipSlot));
Assert.Single(selectionParts);
}
// ── Deliverable 2: WalkStaticStreamPopulator routing ───────────────────
[Fact]

View file

@ -56,5 +56,17 @@ public sealed class WbDrawDispatcherLookInConeTests
Radius = radius;
return true;
}
public IReadOnlyList<uint> VisibleClipSlotsInLookInTurn(
int routeIndex,
in Vector3 center,
float radius,
bool testSphere)
{
RouteIndex = routeIndex;
Center = center;
Radius = radius;
return [7u];
}
}
}