checkpoint(render): preserve pre-overhaul investigation state
This commit is contained in:
parent
e880860291
commit
b3b7d922f1
45 changed files with 3168 additions and 619 deletions
|
|
@ -111,6 +111,25 @@ public class ClipFrameLayoutTests
|
|||
Assert.Equal(0u, ReadUInt(bytes, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSlotPlanes_BorrowsTheExactPackedClipRegion()
|
||||
{
|
||||
using ClipFrame frame = ClipFrame.NoClip();
|
||||
Vector4[] expected =
|
||||
[
|
||||
new(1f, 2f, 3f, 4f),
|
||||
new(-5f, 6f, -7f, 8f),
|
||||
];
|
||||
|
||||
int slot = frame.AppendSlot(expected);
|
||||
|
||||
ReadOnlySpan<Vector4> actual = frame.GetSlotPlanes(checked((uint)slot));
|
||||
Assert.Equal(expected.Length, actual.Length);
|
||||
Assert.Equal(expected[0], actual[0]);
|
||||
Assert.Equal(expected[1], actual[1]);
|
||||
Assert.Equal(0, frame.GetSlotPlanes(0).Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendSlot_EmptyPlaneList_PacksNoClipSlot_Count0()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
using System;
|
||||
using AcDream.App.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// #129 — doors/doorways leak through terrain and houses from over a landblock
|
||||
/// away. The punch's mark pass (#117, AD-18) biased the aperture fan toward
|
||||
/// the viewer by a CONSTANT 0.0005 NDC. NDC depth is non-linear: a constant
|
||||
/// NDC bias b spans ≈ b·d²·(f−n)/(f·n) meters of eye depth at eye distance d
|
||||
/// — 0.125 m at 5 m but ~190 m at a landblock (znear 0.1), so distant
|
||||
/// occluders in front of an aperture passed the mark and were far-Z punched:
|
||||
/// the door-shaped leak. The fix caps the bias's eye-space span
|
||||
/// (PortalDepthMaskRenderer.MarkBiasNdc): identical to the validated constant
|
||||
/// below the ~10 m crossover, never more than the cap beyond it.
|
||||
/// </summary>
|
||||
public class Issue129PunchBiasTests
|
||||
{
|
||||
private const float Near = PortalDepthMaskRenderer.CameraNearPlaneMeters; // 0.1 (retail znear)
|
||||
private const float Far = 5000f;
|
||||
|
||||
/// <summary>Eye-depth span (meters) covered by an NDC depth bias b at eye
|
||||
/// distance d: ndc(d) = f(d−n)/((f−n)d) ⇒ d(ndc) inverse ⇒
|
||||
/// span = b·d²·(f−n)/(f·n) (exact for small b via the derivative).</summary>
|
||||
private static float EyeSpanMeters(float biasNdc, float d) =>
|
||||
biasNdc * d * d * (Far - Near) / (Far * Near);
|
||||
|
||||
[Fact]
|
||||
public void OldConstantBias_SpansMetersAtALandblock_TheLeak()
|
||||
{
|
||||
// The refuted form (documentation of WHY the constant was wrong):
|
||||
// 0.0005 NDC at ~one landblock spans far more eye depth than any
|
||||
// occluder separation — everything in front got punched.
|
||||
Assert.True(EyeSpanMeters(0.0005f, 192f) > 100f);
|
||||
// ...while at close range it was a sane sliver:
|
||||
Assert.InRange(EyeSpanMeters(0.0005f, 5f), 0.05f, 0.30f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CappedBias_MatchesValidatedConstant_AtCloseRange()
|
||||
{
|
||||
// Below the crossover the T5-validated constant must win unchanged —
|
||||
// this preserves the #108 grass coverage bit-for-bit.
|
||||
foreach (float d in new[] { 0.5f, 1f, 3f, 5f, 8f, 9.9f })
|
||||
Assert.Equal(0.0005f, PortalDepthMaskRenderer.MarkBiasNdc(d), 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CappedBias_EyeSpanNeverExceedsCap_AtAnyDistance()
|
||||
{
|
||||
for (float d = 1f; d <= 400f; d += 1f)
|
||||
{
|
||||
float span = EyeSpanMeters(PortalDepthMaskRenderer.MarkBiasNdc(d), d);
|
||||
Assert.True(span <= PortalDepthMaskRenderer.PunchMarkBiasEyeCapMeters * 1.02f,
|
||||
FormattableString.Invariant($"bias spans {span:F2} m of eye depth at d={d} m"));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CappedBias_At200m_CannotReachOccluders()
|
||||
{
|
||||
// The reported #129 distance: occluder separations are tens of
|
||||
// meters; the punch reach must stay under the 0.5 m cap.
|
||||
float span = EyeSpanMeters(PortalDepthMaskRenderer.MarkBiasNdc(200f), 200f);
|
||||
Assert.True(span <= 0.51f, FormattableString.Invariant($"span {span:F3} m at 200 m"));
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,19 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Scene.Arch;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Rendering;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Core.Meshing;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.Options;
|
||||
using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
|
||||
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
|
||||
using DatGfxObj = DatReaderWriter.DBObjs.GfxObj;
|
||||
using DatSetup = DatReaderWriter.DBObjs.Setup;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
|
|
@ -113,7 +120,7 @@ public class Issue177StairDescentCameraFloodTests
|
|||
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
|
||||
foreach (uint low in new uint[] { 0x01C8, 0x01C4, 0x01C9, 0x0210, 0x020E, 0x01C1, 0x01C0 })
|
||||
foreach (uint low in new uint[] { 0x015E, 0x015F, 0x01C8, 0x01C4, 0x01C9, 0x0210, 0x020E, 0x01C1, 0x01C0 })
|
||||
{
|
||||
uint id = FacilityHub | low;
|
||||
var envCell = dats.Get<DatEnvCell>(id);
|
||||
|
|
@ -160,6 +167,113 @@ public class Issue177StairDescentCameraFloodTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FacilityStairAssembly_RegisterAcross015FTo015EWithoutCollisionRows()
|
||||
{
|
||||
string? datDir = ResolveDatDir();
|
||||
if (datDir is null)
|
||||
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
|
||||
const uint setupId = 0x02000623u;
|
||||
DatSetup setup = Assert.IsType<DatSetup>(dats.Get<DatSetup>(setupId));
|
||||
PhysicsEngine engine = BuildHubEngine(dats);
|
||||
PhysicsDataCache cache = Assert.IsType<PhysicsDataCache>(engine.DataCache);
|
||||
IReadOnlyList<MeshRef> meshRefs = SetupMesh.Flatten(setup);
|
||||
foreach (MeshRef meshRef in meshRefs)
|
||||
{
|
||||
DatGfxObj gfxObj = Assert.IsType<DatGfxObj>(
|
||||
dats.Get<DatGfxObj>(meshRef.GfxObjId));
|
||||
cache.CacheGfxObj(meshRef.GfxObjId, gfxObj);
|
||||
}
|
||||
|
||||
DatEnvCell parent = Assert.IsType<DatEnvCell>(
|
||||
dats.Get<DatEnvCell>(FacilityHub | 0x015Fu));
|
||||
var stair = Assert.Single(
|
||||
parent.StaticObjects,
|
||||
staticObject =>
|
||||
staticObject.Id == setupId);
|
||||
var rootPosition = new Vector3(
|
||||
stair.Frame.Origin.X,
|
||||
stair.Frame.Origin.Y,
|
||||
stair.Frame.Origin.Z);
|
||||
Quaternion rootRotation = stair.Frame.Orientation;
|
||||
|
||||
List<ShadowShape> parts = ShadowShapeBuilder.FromStaticRenderParts(
|
||||
meshRefs,
|
||||
cache.GetGfxObj,
|
||||
cache.GetVisualBounds,
|
||||
out bool hasPhysicsBsp);
|
||||
Assert.True(hasPhysicsBsp);
|
||||
Assert.NotEmpty(parts);
|
||||
|
||||
const uint syntheticOwner = 0x7F00DEADu;
|
||||
Assert.Empty(engine.ShadowObjects.GetOwnerCells(syntheticOwner));
|
||||
IReadOnlyList<uint> cells = engine.ShadowObjects.ComputeStaticRenderCells(
|
||||
FacilityHub | 0x015Fu,
|
||||
rootPosition,
|
||||
rootRotation,
|
||||
parts);
|
||||
|
||||
ShadowShape part = parts[0];
|
||||
ShadowPartBox box = ShadowPartBox.FromShape(
|
||||
part,
|
||||
rootPosition,
|
||||
rootRotation);
|
||||
LoadedCell rootCell = CornerFloodReplayTests.LoadCell(
|
||||
dats,
|
||||
FacilityHub | 0x015Fu);
|
||||
box.RefitToLocal(rootCell.InverseWorldTransform, out Vector3 boxMin, out Vector3 boxMax);
|
||||
_out.WriteLine(FormattableString.Invariant(
|
||||
$"stair root=({rootPosition.X:F3},{rootPosition.Y:F3},{rootPosition.Z:F3}) localBox=({boxMin.X:F3},{boxMin.Y:F3},{boxMin.Z:F3})..({boxMax.X:F3},{boxMax.Y:F3},{boxMax.Z:F3}) cells=[{string.Join(',', cells.Select(static id => $"0x{id:X8}"))}]"));
|
||||
|
||||
Assert.Contains(FacilityHub | 0x015Fu, cells);
|
||||
Assert.Contains(FacilityHub | 0x015Eu, cells);
|
||||
Assert.Empty(engine.ShadowObjects.GetOwnerCells(syntheticOwner));
|
||||
|
||||
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
RenderProjectionId projectionId = RenderProjectionId.FromRaw(0x48A02000u);
|
||||
RenderTransform transform = RenderTransform.FromRoot(
|
||||
rootPosition,
|
||||
rootRotation,
|
||||
1f);
|
||||
RenderProjectionRecord projection = new RenderProjectionRecord() with
|
||||
{
|
||||
Id = projectionId,
|
||||
ProjectionClass = RenderProjectionClass.IndoorCellStatic,
|
||||
OwnerIncarnation = RenderOwnerIncarnation.FromRaw(1),
|
||||
Transform = transform,
|
||||
PreviousTransform = new PreviousRenderTransform(transform.LocalToWorld),
|
||||
Residency = new RenderSpatialResidency(
|
||||
RenderSpatialBucket.FromRaw(FacilityHub | 0x015Fu),
|
||||
FacilityHub,
|
||||
FacilityHub | 0x015Fu),
|
||||
Flags = RenderProjectionFlags.Draw,
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = 0x48A02000u,
|
||||
SourceId = setupId,
|
||||
ParentCellId = FacilityHub | 0x015Fu,
|
||||
},
|
||||
EntityPayload = new RenderEntityPayload(meshRefs, null, false),
|
||||
};
|
||||
scene.Apply([RenderProjectionDelta.Register(generation, 1, projection)]);
|
||||
|
||||
var worldData = new WalkProductionWorldData(
|
||||
new WalkBuildingRegistry(),
|
||||
engine.ShadowObjects);
|
||||
worldData.BeginFrame(
|
||||
scene.OpenQuery(),
|
||||
FacilityHub,
|
||||
renderCenterLbX: 0x8A,
|
||||
renderCenterLbY: 0x02);
|
||||
|
||||
Assert.Contains(
|
||||
worldData.GetCellStatics(FacilityHub | 0x015Eu).Records,
|
||||
record => record.Id == projectionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #177 re-diagnosis (fix#1 failed the visual gate): the vanish flips on a SLIGHT turn +
|
||||
/// depends on zoom, so it is a gaze/eye knife-edge, not the eye-squarely-in-opening case
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Gpu.Vk;
|
||||
using AcDream.App.Tests.Rendering.Gpu;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class PortalDepthMaskRetailStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void SealAndPunchUseOneRetailAlwaysWritePipelineAndOneDrawEach()
|
||||
{
|
||||
using var device = new RecordingGpuDevice();
|
||||
var frames = new GpuDeviceFrameLifetime(device);
|
||||
var scope = new VulkanWorldPassScope(sampleCount: 1);
|
||||
using var renderer = new PortalDepthMaskRenderer(device, frames, scope);
|
||||
|
||||
RecordingGpuPipeline pipeline = Assert.Single(device.CreatedPipelines);
|
||||
Assert.Equal("portal-depth-write", pipeline.Description.Name);
|
||||
Assert.Equal(
|
||||
new GpuDepthState(Test: true, Write: true, GpuCompareOp.Always),
|
||||
pipeline.Description.Depth);
|
||||
Assert.False(pipeline.Description.StencilTest);
|
||||
Assert.False(pipeline.Description.ColorWrite);
|
||||
Assert.Equal(GpuCullMode.None, pipeline.Description.Cull);
|
||||
|
||||
frames.BeginFrame();
|
||||
renderer.BeginFrame(frameSlot: 0);
|
||||
using (IGpuPassEncoder pass = frames.CurrentFrame!.BeginPass(
|
||||
GpuPassDescription.BackbufferClear(
|
||||
"portal-depth-retail-state",
|
||||
Vector4.Zero,
|
||||
sampleCount: 1)))
|
||||
using (scope.Publish(pass))
|
||||
{
|
||||
Vector3[] triangle =
|
||||
[
|
||||
new(-1f, -1f, 1f),
|
||||
new(1f, -1f, 1f),
|
||||
new(0f, 1f, 1f),
|
||||
];
|
||||
renderer.DrawDepthFan(
|
||||
triangle,
|
||||
Matrix4x4.Identity,
|
||||
ReadOnlySpan<Vector4>.Empty,
|
||||
forceFarZ: true);
|
||||
renderer.DrawDepthFan(
|
||||
triangle,
|
||||
Matrix4x4.Identity,
|
||||
ReadOnlySpan<Vector4>.Empty,
|
||||
forceFarZ: false);
|
||||
}
|
||||
frames.EndFrame();
|
||||
|
||||
Assert.Equal(
|
||||
2,
|
||||
device.Calls.OfType<GpuRecordedPipelineBind>()
|
||||
.Count(call => call.PipelineName == "portal-depth-write"));
|
||||
Assert.Equal(2, device.Calls.OfType<GpuRecordedDraw>().Count());
|
||||
Assert.Collection(
|
||||
device.Calls.OfType<GpuRecordedPushConstants>(),
|
||||
punch => Assert.Equal(1, punch.Constants.RenderPass),
|
||||
seal => Assert.Equal(0, seal.Constants.RenderPass));
|
||||
Assert.Empty(device.Calls.OfType<GpuRecordedStencil>());
|
||||
}
|
||||
}
|
||||
|
|
@ -45,16 +45,16 @@ public sealed class WalkFrameDriverTests
|
|||
RetailAlphaQueue? alpha = null) : IWalkFrameLeafRenderer
|
||||
{
|
||||
public readonly List<WalkPolygon> Punches = new();
|
||||
public readonly List<(uint CellId, uint ClipSlot)> Shells = new();
|
||||
public readonly List<uint> Shells = new();
|
||||
public readonly List<int> AlphaPendingAtBarrier = new();
|
||||
|
||||
public void DrawSky() => log.Add("SKY");
|
||||
|
||||
public void DrawTerrainSlice(int sliceIndex) => log.Add($"TERRAIN:{sliceIndex}");
|
||||
|
||||
public void DrawCellShell(uint cellId, uint clipSlot)
|
||||
public void DrawCellShell(uint cellId)
|
||||
{
|
||||
Shells.Add((cellId, clipSlot));
|
||||
Shells.Add(cellId);
|
||||
log.Add($"SHELL:{cellId:x8}");
|
||||
}
|
||||
|
||||
|
|
@ -267,7 +267,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();
|
||||
// A minimal, no-op landscape (1x1 window, the one slot unpublished)
|
||||
// — matches RetailFrameWalkTests' own exit-view fixture. LScape::draw
|
||||
|
|
@ -296,6 +298,79 @@ public sealed class WalkFrameDriverTests
|
|||
Assert.All(mdiCalls, c => Assert.Equal(1u, c.DrawCount));
|
||||
// Nothing dropped: every populated record reached exactly one indirect draw.
|
||||
Assert.Equal(2, mdiCalls.Sum(c => (int)c.DrawCount));
|
||||
|
||||
// Exit seals replay through the exact portal_view slices captured by
|
||||
// this walk. The legacy PortalVisibilityFrame is not consulted.
|
||||
Assert.Equal(1, driver.InteriorFloodViewSliceCountAt(0));
|
||||
Assert.Equal(1, driver.InteriorFloodViewSliceCountAt(1));
|
||||
Assert.Equal(4, driver.InteriorFloodViewClipPlanesAt(0, 0).Length);
|
||||
Assert.Equal(4, driver.InteriorFloodViewClipPlanesAt(1, 0).Length);
|
||||
Assert.True(clipFrame.SlotCount >= 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InteriorFloodAfterLandscape_RearmsRetailPartStampForPostClearCellRepaint()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
var log = new List<string>();
|
||||
const ulong gfxObj = 0x0200_0021UL;
|
||||
const uint outdoorCellId = 0xF4180009u;
|
||||
const uint interiorCellId = 0xF4180112u;
|
||||
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08100021u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
||||
|
||||
RenderProjectionRecord sharedPart = MakeRecord(
|
||||
0x4F41806Cu,
|
||||
0,
|
||||
Vector3.Zero,
|
||||
[new MeshRef((uint)gfxObj, Matrix4x4.Identity)]);
|
||||
var worldData = new FakeWorldData();
|
||||
worldData.OutdoorStaticsByCell[outdoorCellId] =
|
||||
new WalkFrameStaticRecords(new[] { sharedPart }, 0xF418u);
|
||||
worldData.CellStaticsByCell[interiorCellId] =
|
||||
new WalkFrameStaticRecords(new[] { sharedPart }, 0xF418u);
|
||||
|
||||
var ctx = new TestContext();
|
||||
var interiorCell = new WalkCell { CellId = interiorCellId };
|
||||
interiorCell.PushView();
|
||||
WalkCopyView.AppendFullViewportQuad(
|
||||
interiorCell.TopView,
|
||||
ctx.Rays,
|
||||
ctx.WorldViewpoint,
|
||||
ctx.ViewportWidth,
|
||||
ctx.ViewportHeight);
|
||||
ctx.Cells[interiorCellId] = interiorCell;
|
||||
|
||||
var driver = new WalkFrameDriver(
|
||||
fx.Dispatcher,
|
||||
new RecordingLeafRenderer(log),
|
||||
worldData,
|
||||
new RecordingTrace(log));
|
||||
IWalkEventSink sink = driver;
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
|
||||
var landscapeViews = new WalkPortalView();
|
||||
WalkCopyView.AppendFullViewportQuad(
|
||||
landscapeViews,
|
||||
ctx.Rays,
|
||||
ctx.WorldViewpoint,
|
||||
ctx.ViewportWidth,
|
||||
ctx.ViewportHeight);
|
||||
sink.OnLandscapeViews(landscapeViews);
|
||||
sink.OnLandscapeCellTurn(outdoorCellId);
|
||||
sink.OnInteriorFloodDrawTurn([interiorCellId]);
|
||||
driver.EndFrame();
|
||||
driver.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
List<GpuRecordedMultiDrawIndirect> mdiCalls =
|
||||
[.. fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
|
||||
Assert.Equal(2, mdiCalls.Count);
|
||||
Assert.All(mdiCalls, call => Assert.Equal(1u, call.DrawCount));
|
||||
Assert.Equal(2, mdiCalls.Sum(call => (int)call.DrawCount));
|
||||
Assert.Equal(2, log.Count(entry => entry == "FLUSH:1:OutdoorStatic"
|
||||
|| entry == "FLUSH:1:CellStatic"));
|
||||
}
|
||||
|
||||
// ── Deliverable: the ov==0 interior case — no exit view survives, so
|
||||
|
|
@ -479,11 +554,7 @@ public sealed class WalkFrameDriverTests
|
|||
Assert.Equal([0x104u], driver.LookInCells);
|
||||
Assert.Collection(
|
||||
leaf.Shells,
|
||||
shell =>
|
||||
{
|
||||
Assert.Equal(0x104u, shell.CellId);
|
||||
Assert.NotEqual(0u, shell.ClipSlot);
|
||||
});
|
||||
shell => Assert.Equal(0x104u, shell));
|
||||
WalkPortalView capturedView = ctx.Cells[0x104].PortalViews[0];
|
||||
WalkViewPoly capturedPoly = Assert.Single(capturedView.View.Polys);
|
||||
Vector2 capturedCenter = Vector2.Zero;
|
||||
|
|
@ -515,6 +586,101 @@ public sealed class WalkFrameDriverTests
|
|||
Assert.Equal(3, mdiCalls.Sum(c => (int)c.DrawCount));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RepeatedFloodTurns_DrawEnvCellShellWholeOncePerRetailFrameStamp()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
var log = new List<string>();
|
||||
var leaf = new RecordingLeafRenderer(log);
|
||||
var ctx = new TestContext();
|
||||
const uint cellId = 0xF4180112u;
|
||||
var cell = new WalkCell { CellId = cellId };
|
||||
cell.PushView();
|
||||
WalkCopyView.AppendFullViewportQuad(
|
||||
cell.TopView,
|
||||
ctx.Rays,
|
||||
ctx.WorldViewpoint,
|
||||
ctx.ViewportWidth,
|
||||
ctx.ViewportHeight);
|
||||
ctx.Cells[cellId] = cell;
|
||||
|
||||
var driver = new WalkFrameDriver(
|
||||
fx.Dispatcher,
|
||||
leaf,
|
||||
new FakeWorldData());
|
||||
IWalkEventSink sink = driver;
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
sink.OnInteriorFloodDrawTurn([cellId]);
|
||||
sink.OnInteriorFloodDrawTurn([cellId]);
|
||||
driver.EndFrame();
|
||||
driver.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
Assert.Equal([cellId], leaf.Shells);
|
||||
Assert.Equal(1, log.Count(entry => entry == "SHELL:f4180112"));
|
||||
Assert.Equal(2, log.Count(entry => entry == "CLEAR"));
|
||||
Assert.Equal(2, log.Count(entry => entry == "SEALS"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LandscapeStampBoundary_RearmsWholeShellForPostClearRootRepaint()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
var log = new List<string>();
|
||||
var leaf = new RecordingLeafRenderer(log);
|
||||
var ctx = new TestContext();
|
||||
const uint cellId = 0xF4180112u;
|
||||
var cell = new WalkCell { CellId = cellId };
|
||||
cell.PushView();
|
||||
WalkCopyView.AppendFullViewportQuad(
|
||||
cell.TopView,
|
||||
ctx.Rays,
|
||||
ctx.WorldViewpoint,
|
||||
ctx.ViewportWidth,
|
||||
ctx.ViewportHeight);
|
||||
ctx.Cells[cellId] = cell;
|
||||
|
||||
var driver = new WalkFrameDriver(
|
||||
fx.Dispatcher,
|
||||
leaf,
|
||||
new FakeWorldData());
|
||||
IWalkEventSink sink = driver;
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
|
||||
// LScape::draw has begun. A building look-in reached this cell before
|
||||
// PView::DrawCells advances m_nFrameStamp and clears interior depth.
|
||||
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
|
||||
var landscapeViews = new WalkPortalView();
|
||||
WalkCopyView.AppendFullViewportQuad(
|
||||
landscapeViews,
|
||||
ctx.Rays,
|
||||
ctx.WorldViewpoint,
|
||||
ctx.ViewportWidth,
|
||||
ctx.ViewportHeight);
|
||||
sink.OnLandscapeViews(landscapeViews);
|
||||
sink.OnBuildingTurn(new WalkBuilding());
|
||||
sink.Emit(WalkEvent.DrawCells(outsideViewCount: 0, [cellId]));
|
||||
|
||||
// The same shell must draw again after the retail stamp increment and
|
||||
// full depth clear; otherwise the pre-clear color survives unpaired
|
||||
// with depth and bleeds through the root's walls.
|
||||
sink.OnInteriorFloodDrawTurn([cellId]);
|
||||
driver.EndFrame();
|
||||
driver.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
Assert.Equal([cellId, cellId], leaf.Shells);
|
||||
Assert.Equal(2, log.Count(entry => entry == "SHELL:f4180112"));
|
||||
Assert.True(
|
||||
log.IndexOf("SHELL:f4180112") < log.IndexOf("CLEAR"),
|
||||
"The look-in shell must precede the interior clear.");
|
||||
Assert.True(
|
||||
log.LastIndexOf("SHELL:f4180112") > log.IndexOf("SEALS"),
|
||||
"The rearmed root shell must repaint after the clear and seals.");
|
||||
}
|
||||
|
||||
// ── Fail-loud: a DrawCells turn with no preceding DrawInside/Building
|
||||
// turn is a walk/driver desync, not a silent skip. ─────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ public sealed class WalkProductionWorldConformanceTests
|
|||
[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(
|
||||
public void Cathedral_transition_keeps_south_hall_admission_bounded_for_depth_occlusion(
|
||||
uint cameraCellId,
|
||||
float x,
|
||||
float y)
|
||||
|
|
@ -244,11 +244,12 @@ public sealed class WalkProductionWorldConformanceTests
|
|||
// 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.
|
||||
// legitimately reaches 0x112 through one authored building aperture.
|
||||
// Retail uses that cone only for coarse sphere admission, then draws
|
||||
// each accepted shell/object whole; the complete intervening wall
|
||||
// shell hides the remote actors through ordinary depth. Preserve the
|
||||
// installed-DAT fact this 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),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,77 @@ namespace AcDream.App.Tests.Rendering.Walk;
|
|||
|
||||
public sealed class WalkProductionWorldDataTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildingShellBucketCellId_PortalLessBuildingUsesLandscapePositionCell()
|
||||
{
|
||||
const uint positionCellId = 0xF4180011u;
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0xCF418060u,
|
||||
position: new Vector3(53.57f, 13.874f, 160f)) with
|
||||
{
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = 0xCF418060u,
|
||||
SourceId = 0x01001FD3u,
|
||||
EffectCellId = positionCellId,
|
||||
BuildingShellAnchorCellId = 0,
|
||||
},
|
||||
EntityPayload = new RenderEntityPayload() with
|
||||
{
|
||||
IsBuildingShell = true,
|
||||
},
|
||||
};
|
||||
var building = new WalkBuilding
|
||||
{
|
||||
PositionCellId = positionCellId,
|
||||
Portals = [],
|
||||
};
|
||||
|
||||
Assert.Equal(
|
||||
positionCellId,
|
||||
WalkProductionWorldData.BuildingShellBucketCellId(in record));
|
||||
Assert.Equal(
|
||||
positionCellId,
|
||||
WalkProductionWorldData.BuildingShellBucketCellId(building));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildingShellBucketCellId_PortalBearingBuildingKeepsInteriorAnchor()
|
||||
{
|
||||
const uint anchorCellId = 0xF4180112u;
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0xCF41805Fu,
|
||||
position: new Vector3(36f, 13.8349f, 160f)) with
|
||||
{
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = 0xCF41805Fu,
|
||||
SourceId = 0x01001FB7u,
|
||||
EffectCellId = 0xF4180009u,
|
||||
BuildingShellAnchorCellId = anchorCellId,
|
||||
},
|
||||
EntityPayload = new RenderEntityPayload() with
|
||||
{
|
||||
IsBuildingShell = true,
|
||||
},
|
||||
};
|
||||
var building = new WalkBuilding
|
||||
{
|
||||
PositionCellId = 0xF4180009u,
|
||||
Portals =
|
||||
[
|
||||
new WalkBldPortal { OtherCellId = anchorCellId },
|
||||
],
|
||||
};
|
||||
|
||||
Assert.Equal(
|
||||
anchorCellId,
|
||||
WalkProductionWorldData.BuildingShellBucketCellId(in record));
|
||||
Assert.Equal(
|
||||
anchorCellId,
|
||||
WalkProductionWorldData.BuildingShellBucketCellId(building));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BucketOutdoorRecord_UsesEveryOutdoorPhysicsShadowCellAcrossLandblockEdge()
|
||||
{
|
||||
|
|
@ -44,6 +115,160 @@ public sealed class WalkProductionWorldDataTests
|
|||
Assert.Equal(record, Assert.Single(buckets[0xF07F0002u]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BucketIndoorRecord_InstallsCrossCellPartInBothInteriorCells()
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0x48A02035u,
|
||||
position: new Vector3(55.25f, -46.47f, -3f)) with
|
||||
{
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = 0x48A02035u,
|
||||
ParentCellId = 0x8A02015Fu,
|
||||
},
|
||||
};
|
||||
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
|
||||
WalkProductionWorldData.BucketIndoorRecord(
|
||||
in record,
|
||||
[0x8A02015Fu, 0x8A02015Eu],
|
||||
indoor,
|
||||
outdoor);
|
||||
|
||||
Assert.Equal([0x8A02015Eu, 0x8A02015Fu], indoor.Keys.Order());
|
||||
Assert.All(indoor.Values, bucket => Assert.Equal(record, Assert.Single(bucket)));
|
||||
Assert.Empty(outdoor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BucketIndoorRecord_CanCrossAnExitIntoLandscapeCell()
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0x48A02035u,
|
||||
position: Vector3.Zero) with
|
||||
{
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = 0x48A02035u,
|
||||
ParentCellId = 0x8A02015Fu,
|
||||
},
|
||||
};
|
||||
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
|
||||
WalkProductionWorldData.BucketIndoorRecord(
|
||||
in record,
|
||||
[0x8A02015Fu, 0x8A020021u],
|
||||
indoor,
|
||||
outdoor);
|
||||
|
||||
Assert.Equal(record, Assert.Single(indoor[0x8A02015Fu]));
|
||||
Assert.Equal(record, Assert.Single(outdoor[0x8A020021u]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BucketDynamicRecord_InstallsMultipartPlayerInEveryCrossedInteriorCell()
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0x000F4243u,
|
||||
position: new Vector3(58.81f, -49.42f, -0.85f)) with
|
||||
{
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = 0x000F4243u,
|
||||
ParentCellId = 0x8A02015Eu,
|
||||
},
|
||||
};
|
||||
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
|
||||
WalkProductionWorldData.BucketDynamicRecord(
|
||||
in record,
|
||||
[0x8A02015Eu, 0x8A02015Fu, 0x8A0201C1u],
|
||||
indoor,
|
||||
outdoor,
|
||||
renderCenterLbX: 0x8A,
|
||||
renderCenterLbY: 0x02);
|
||||
|
||||
Assert.Equal(
|
||||
[0x8A02015Eu, 0x8A02015Fu, 0x8A0201C1u],
|
||||
indoor.Keys.Order());
|
||||
Assert.All(
|
||||
indoor.Values,
|
||||
bucket => Assert.Equal(record, Assert.Single(bucket)));
|
||||
Assert.Empty(outdoor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BucketDynamicRecord_UnregisteredInteriorEffectFallsBackToParentCell()
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0x00001234u,
|
||||
position: Vector3.Zero) with
|
||||
{
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = 0x00001234u,
|
||||
ParentCellId = 0x8A02015Fu,
|
||||
},
|
||||
};
|
||||
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
|
||||
WalkProductionWorldData.BucketDynamicRecord(
|
||||
in record,
|
||||
Array.Empty<uint>(),
|
||||
indoor,
|
||||
outdoor,
|
||||
renderCenterLbX: 0x8A,
|
||||
renderCenterLbY: 0x02);
|
||||
|
||||
Assert.Equal(record, Assert.Single(indoor[0x8A02015Fu]));
|
||||
Assert.Empty(outdoor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveDynamicRenderCells_EquippedDescendantInheritsRootCellArray()
|
||||
{
|
||||
RenderProjectionRecord wand = Record(
|
||||
id: 0x800045EEu,
|
||||
position: Vector3.Zero) with
|
||||
{
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = 0x800045EEu,
|
||||
ParentCellId = 0xF4180104u,
|
||||
},
|
||||
EntityPayload = new RenderEntityPayload() with
|
||||
{
|
||||
CasterIdentity = RenderCasterIdentityKind.EquippedChild,
|
||||
},
|
||||
};
|
||||
var owners = new Dictionary<uint, IReadOnlyList<uint>>
|
||||
{
|
||||
[0x5000000Au] = [0xF4180106u, 0xF4180104u],
|
||||
};
|
||||
var parents = new Dictionary<uint, uint>
|
||||
{
|
||||
[0x800045EEu] = 0x80000461u,
|
||||
[0x80000461u] = 0x5000000Au,
|
||||
};
|
||||
|
||||
IReadOnlyList<uint> cells =
|
||||
WalkProductionWorldData.ResolveDynamicRenderCells(
|
||||
in wand,
|
||||
id => owners.TryGetValue(id, out IReadOnlyList<uint>? value)
|
||||
? value
|
||||
: Array.Empty<uint>(),
|
||||
id => parents.TryGetValue(id, out uint value)
|
||||
? value
|
||||
: null);
|
||||
|
||||
Assert.Equal([0xF4180106u, 0xF4180104u], cells);
|
||||
}
|
||||
|
||||
private static RenderProjectionRecord Record(uint id, Vector3 position) =>
|
||||
new RenderProjectionRecord() with
|
||||
{
|
||||
|
|
|
|||
|
|
@ -69,7 +69,9 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
Vector3 position,
|
||||
IReadOnlyList<MeshRef> meshRefs,
|
||||
bool isBuildingShell = false,
|
||||
uint parentCellId = 0u) =>
|
||||
uint parentCellId = 0u,
|
||||
RenderCasterIdentityKind casterIdentity =
|
||||
RenderCasterIdentityKind.Unclassified) =>
|
||||
new(
|
||||
Id: RenderProjectionId.FromRaw(localEntityId),
|
||||
ProjectionClass: RenderProjectionClass.OutdoorStatic,
|
||||
|
|
@ -97,7 +99,8 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
EntityPayload: new RenderEntityPayload(
|
||||
MeshRefs: meshRefs,
|
||||
PaletteOverride: null,
|
||||
IsBuildingShell: isBuildingShell));
|
||||
IsBuildingShell: isBuildingShell,
|
||||
CasterIdentity: casterIdentity));
|
||||
|
||||
private static ObjectRenderBatch MakeBatch(
|
||||
uint surfaceId,
|
||||
|
|
@ -274,7 +277,161 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyEntityForWalk_EmitsOneGpuClippedInstancePerPortalViewSlice()
|
||||
public void ClassifyEntityForWalk_FrameScopeStampsEachAdmittedSetupPartOncePerRetailPass()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const ulong setupGfxObj = 0x1000_0020UL;
|
||||
const ulong headGfxObj = 0x0100_0021UL;
|
||||
const ulong torsoGfxObj = 0x0100_0022UL;
|
||||
|
||||
InjectRenderData(fx.Manager, headGfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08000021u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
||||
InjectRenderData(fx.Manager, torsoGfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08000022u, TranslucencyKind.Opaque, 3, 4, 6, 2)));
|
||||
InjectRenderData(fx.Manager, setupGfxObj, new ObjectRenderData
|
||||
{
|
||||
IsSetup = true,
|
||||
SetupParts = new List<(ulong GfxObjId, Matrix4x4 Transform)>
|
||||
{
|
||||
(headGfxObj, Matrix4x4.CreateTranslation(0, 0, 2)),
|
||||
(torsoGfxObj, Matrix4x4.CreateTranslation(0, 0, 1)),
|
||||
},
|
||||
});
|
||||
|
||||
RenderProjectionRecord record = MakeRecord(
|
||||
220,
|
||||
0,
|
||||
Vector3.Zero,
|
||||
[new MeshRef((uint)setupGfxObj, Matrix4x4.Identity)]);
|
||||
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
||||
var selectionParts = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
|
||||
|
||||
fx.Dispatcher.BeginWalkPartFrame();
|
||||
try
|
||||
{
|
||||
fx.Dispatcher.ClassifyEntityForWalk(
|
||||
in record, 0x8C04u, batches, selectionParts);
|
||||
Assert.Equal(2, batches.Count);
|
||||
Assert.Equal(2, selectionParts.Count);
|
||||
|
||||
batches.Clear();
|
||||
selectionParts.Clear();
|
||||
fx.Dispatcher.ClassifyEntityForWalk(
|
||||
in record, 0x8C04u, batches, selectionParts);
|
||||
Assert.Empty(batches);
|
||||
Assert.Empty(selectionParts);
|
||||
|
||||
// PView::DrawCells @0x005A4886 increments m_nFrameStamp after
|
||||
// the landscape pass and before its interior-cell repaint. The
|
||||
// same parts may therefore submit once again in the second pass.
|
||||
fx.Dispatcher.AdvanceWalkPartPassStamp();
|
||||
fx.Dispatcher.ClassifyEntityForWalk(
|
||||
in record, 0x8C04u, batches, selectionParts);
|
||||
Assert.Equal(2, batches.Count);
|
||||
Assert.Equal(2, selectionParts.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fx.Dispatcher.EndWalkPartFrame();
|
||||
}
|
||||
|
||||
// Direct classifier calls outside a production walk frame remain
|
||||
// independent, as the conformance/referee tests require.
|
||||
batches.Clear();
|
||||
selectionParts.Clear();
|
||||
fx.Dispatcher.ClassifyEntityForWalk(
|
||||
in record, 0x8C04u, batches, selectionParts);
|
||||
Assert.Equal(2, batches.Count);
|
||||
Assert.Equal(2, selectionParts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyEntityForWalk_FrameScopeDoesNotStampPortalRejectedPart()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const ulong gfxObj = 0x0100_0023UL;
|
||||
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08000023u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
||||
RenderProjectionRecord record = MakeRecord(
|
||||
221,
|
||||
0,
|
||||
Vector3.Zero,
|
||||
[new MeshRef((uint)gfxObj, Matrix4x4.Identity)]);
|
||||
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
||||
var selectionParts = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
|
||||
|
||||
fx.Dispatcher.BeginWalkPartFrame();
|
||||
try
|
||||
{
|
||||
fx.Dispatcher.ClassifyEntityForWalk(
|
||||
in record,
|
||||
0x8C04u,
|
||||
batches,
|
||||
selectionParts,
|
||||
liveDynamic: true,
|
||||
lookInViews: new FixedWalkViews(),
|
||||
lookInRouteIndex: 0);
|
||||
Assert.Empty(batches);
|
||||
Assert.Empty(selectionParts);
|
||||
|
||||
fx.Dispatcher.ClassifyEntityForWalk(
|
||||
in record,
|
||||
0x8C04u,
|
||||
batches,
|
||||
selectionParts,
|
||||
liveDynamic: true,
|
||||
lookInViews: new FixedWalkViews(7u),
|
||||
lookInRouteIndex: 1);
|
||||
Assert.Single(batches);
|
||||
Assert.Single(selectionParts);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fx.Dispatcher.EndWalkPartFrame();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyEntityForWalk_LocalPlayerBypassesDrawnPartStampLikeRetail()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const ulong gfxObj = 0x0100_0024UL;
|
||||
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08000024u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
||||
RenderProjectionRecord player = MakeRecord(
|
||||
222,
|
||||
0x5000_0001u,
|
||||
Vector3.Zero,
|
||||
[new MeshRef((uint)gfxObj, Matrix4x4.Identity)],
|
||||
casterIdentity: RenderCasterIdentityKind.LocalPlayer);
|
||||
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
||||
var selectionParts = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
|
||||
|
||||
fx.Dispatcher.BeginWalkPartFrame();
|
||||
try
|
||||
{
|
||||
fx.Dispatcher.ClassifyEntityForWalk(
|
||||
in player, 0xF418u, batches, selectionParts,
|
||||
liveDynamic: true);
|
||||
Assert.Single(batches);
|
||||
Assert.Single(selectionParts);
|
||||
|
||||
batches.Clear();
|
||||
selectionParts.Clear();
|
||||
fx.Dispatcher.ClassifyEntityForWalk(
|
||||
in player, 0xF418u, batches, selectionParts,
|
||||
liveDynamic: true);
|
||||
Assert.Single(batches);
|
||||
Assert.Single(selectionParts);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fx.Dispatcher.EndWalkPartFrame();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyEntityForWalk_PortalViewsAdmitOneCompleteUnclippedMesh()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const ulong gfxObj = 0x0100_0013UL;
|
||||
|
|
@ -298,7 +455,8 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
lookInRouteIndex: 0,
|
||||
lookInCellId: 0x8C040112u);
|
||||
|
||||
Assert.Equal([7u, 9u], batches.Select(static batch => batch.ClipSlot));
|
||||
WbDrawDispatcher.WalkClassifiedBatch batch = Assert.Single(batches);
|
||||
Assert.Equal(0u, batch.ClipSlot);
|
||||
Assert.Single(selectionParts);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ public sealed class WorldRenderFrameBuilderTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Runtime_environment_copies_visible_cells_and_clear_restores_unscoped_lights()
|
||||
public void Runtime_environment_uses_resident_lights_independent_of_drawable_cells()
|
||||
{
|
||||
const uint visibleCell = 0x01010100u;
|
||||
const uint hiddenCell = 0x01010101u;
|
||||
|
|
@ -291,7 +291,7 @@ public sealed class WorldRenderFrameBuilderTests
|
|||
environment.Prepare(in camera, in roots, in foundation, activeDayGroup: null);
|
||||
|
||||
Assert.Contains(visibleLight, lighting.PointSnapshot);
|
||||
Assert.DoesNotContain(hiddenLight, lighting.PointSnapshot);
|
||||
Assert.Contains(hiddenLight, lighting.PointSnapshot);
|
||||
|
||||
environment.ClearDrawableCells();
|
||||
environment.Prepare(in camera, in roots, in foundation, activeDayGroup: null);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue