checkpoint: preserve user-gated FW closeout fixes
This commit is contained in:
parent
a2f2eb7d78
commit
b8befded8b
22 changed files with 1005 additions and 198 deletions
|
|
@ -606,6 +606,57 @@ public sealed class WalkFrameDriverTests
|
|||
Assert.Equal(new[] { "SKY", "TERRAIN:0" }, log);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RebindFrame_ReusesTheDriverAndRoutesTheNextFrameToTheNewLeaf()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
var firstLog = new List<string>();
|
||||
var secondLog = new List<string>();
|
||||
var ctx = new TestContext();
|
||||
var driver = new WalkFrameDriver(
|
||||
fx.Dispatcher, new RecordingLeafRenderer(firstLog), new FakeWorldData());
|
||||
var walk = new RetailFrameWalk();
|
||||
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.Collect(
|
||||
walk, 0u, null, landscape, ctx,
|
||||
Matrix4x4.Identity, Vector3.Zero);
|
||||
driver.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
driver.RebindFrame(new RecordingLeafRenderer(secondLog), clipFrame: null);
|
||||
driver.Collect(
|
||||
walk, 0u, null, landscape, ctx,
|
||||
Matrix4x4.Identity, Vector3.Zero);
|
||||
driver.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
Assert.Equal(new[] { "SKY", "TERRAIN:0" }, firstLog);
|
||||
Assert.Equal(new[] { "SKY", "TERRAIN:0" }, secondLog);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RebindFrame_DiscardsAnIncompletePriorFrameAndRecovers()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
var log = new List<string>();
|
||||
var ctx = new TestContext();
|
||||
var driver = new WalkFrameDriver(
|
||||
fx.Dispatcher, new RecordingLeafRenderer(new List<string>()), new FakeWorldData());
|
||||
var walk = new RetailFrameWalk();
|
||||
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
||||
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
driver.RebindFrame(new RecordingLeafRenderer(log), clipFrame: null);
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.Collect(
|
||||
walk, 0u, null, landscape, ctx,
|
||||
Matrix4x4.Identity, Vector3.Zero);
|
||||
driver.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
Assert.Equal(new[] { "SKY", "TERRAIN:0" }, log);
|
||||
}
|
||||
|
||||
// ── Deliverable: an outdoor landscape-cell turn with no building appends
|
||||
// straight to the stream (no shell call — outdoor cells have no EnvCell
|
||||
// shell), and the accumulated content flushes at frame end. ───────────
|
||||
|
|
@ -650,12 +701,67 @@ public sealed class WalkFrameDriverTests
|
|||
Assert.Equal(
|
||||
new[] { "FLUSH:2:OutdoorStatic,Dynamic", "PARTICLES:12d,12e" },
|
||||
log);
|
||||
var visibleCells = new HashSet<uint> { 0xDEAD_BEEFu };
|
||||
driver.CopyVisibleCellsTo(visibleCells);
|
||||
Assert.Equal(new[] { 0x8C040005u }, visibleCells);
|
||||
GpuRecordedMultiDrawIndirect[] mdi =
|
||||
fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().ToArray();
|
||||
Assert.Equal(2, mdi.Length);
|
||||
Assert.Equal(2u, mdi.Aggregate(0u, static (sum, call) => sum + call.DrawCount));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnLandscapeCellTurn_MultiCellShadowAlias_DrawsMeshAndParticlesOnlyOnce()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
var log = new List<string>();
|
||||
const ulong gfxObj = 0x0200_0021UL;
|
||||
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08100021u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
||||
|
||||
var ctx = new TestContext();
|
||||
var worldData = new FakeWorldData();
|
||||
RenderProjectionRecord shadowAlias = MakeRecord(
|
||||
303,
|
||||
0x50000002,
|
||||
Vector3.Zero,
|
||||
[new MeshRef((uint)gfxObj, Matrix4x4.Identity)]);
|
||||
WalkFrameStaticRecords aliases = new(
|
||||
new[] { shadowAlias },
|
||||
0xF07Fu);
|
||||
worldData.OutdoorDynamicsByCell[0xF07F0040u] = aliases;
|
||||
worldData.OutdoorDynamicsByCell[0xF0800001u] = aliases;
|
||||
|
||||
var driver = new WalkFrameDriver(
|
||||
fx.Dispatcher,
|
||||
new RecordingLeafRenderer(log),
|
||||
worldData,
|
||||
new RecordingTrace(log));
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
var activeViews = new WalkPortalView();
|
||||
WalkCopyView.AppendFullViewportQuad(
|
||||
activeViews,
|
||||
ctx.Rays,
|
||||
ctx.WorldViewpoint,
|
||||
ctx.ViewportWidth,
|
||||
ctx.ViewportHeight);
|
||||
((IWalkEventSink)driver).OnLandscapeViews(activeViews);
|
||||
((IWalkEventSink)driver).OnLandscapeCellTurn(0xF07F0040u);
|
||||
((IWalkEventSink)driver).OnLandscapeCellTurn(0xF0800001u);
|
||||
driver.EndFrame();
|
||||
driver.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
Assert.Equal(
|
||||
new[] { "FLUSH:1:Dynamic", "PARTICLES:12f" },
|
||||
log);
|
||||
GpuRecordedMultiDrawIndirect[] mdi =
|
||||
fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().ToArray();
|
||||
Assert.Single(mdi);
|
||||
Assert.Equal(1u, mdi[0].DrawCount);
|
||||
}
|
||||
|
||||
// ── Fixture (mirrors WalkStaticStreamPopulatorTests' DispatcherFixture —
|
||||
// FW3.2a's own referee) ─────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Walk;
|
||||
|
|
@ -12,6 +13,15 @@ namespace AcDream.App.Tests.Rendering.Walk;
|
|||
/// SetViewer exactly once per fixture).</summary>
|
||||
public sealed class WalkLandscapeAssemblerTests
|
||||
{
|
||||
private sealed class LandscapeCellRecorder : IWalkEventSink
|
||||
{
|
||||
public HashSet<uint> Cells { get; } = [];
|
||||
|
||||
public void Emit(in WalkEvent walkEvent) { }
|
||||
|
||||
public void OnLandscapeCellTurn(uint cellId) => Cells.Add(cellId);
|
||||
}
|
||||
|
||||
private const uint LandblockId = 0xA9B4FFFFu; // block (0xA9, 0xB4)
|
||||
private const uint CameraCellId = 0xA9B40001u; // same block, outdoor landcell 1
|
||||
|
||||
|
|
@ -123,6 +133,19 @@ public sealed class WalkLandscapeAssemblerTests
|
|||
Assert.Equal(1, assembler.Landscape.ViewerCellY);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetViewer_OutdoorCellRecoversWholeBlockRenderCenterOffset()
|
||||
{
|
||||
var assembler = new WalkLandscapeAssembler();
|
||||
|
||||
assembler.SetViewer(0xF07F0022u, new Vector3(107.31238f, 222.21594f, 0f));
|
||||
|
||||
Assert.Equal(4, assembler.Landscape.ViewerCellX);
|
||||
Assert.Equal(1, assembler.Landscape.ViewerCellY);
|
||||
Assert.Equal(0f, assembler.Landscape.ViewerWorldOriginX);
|
||||
Assert.Equal(192f, assembler.Landscape.ViewerWorldOriginY);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetViewer_InteriorCameraDerivesViewerCellFromOrigin()
|
||||
{
|
||||
|
|
@ -132,6 +155,21 @@ public sealed class WalkLandscapeAssemblerTests
|
|||
|
||||
Assert.Equal(2, assembler.Landscape.ViewerCellX); // floor(50 / 24) = 2
|
||||
Assert.Equal(3, assembler.Landscape.ViewerCellY); // floor(74 / 24) = 3
|
||||
Assert.Equal(0f, assembler.Landscape.ViewerWorldOriginX);
|
||||
Assert.Equal(0f, assembler.Landscape.ViewerWorldOriginY);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetViewer_InteriorCameraUsesRenderCenterBlockAndPositiveLocalCell()
|
||||
{
|
||||
var assembler = new WalkLandscapeAssembler();
|
||||
|
||||
assembler.SetViewer(0xA9B40105u, new Vector3(-10f, 222f, 0f));
|
||||
|
||||
Assert.Equal(-192f, assembler.Landscape.ViewerWorldOriginX);
|
||||
Assert.Equal(192f, assembler.Landscape.ViewerWorldOriginY);
|
||||
Assert.Equal(7, assembler.Landscape.ViewerCellX);
|
||||
Assert.Equal(1, assembler.Landscape.ViewerCellY);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -145,4 +183,53 @@ public sealed class WalkLandscapeAssemblerTests
|
|||
|
||||
Assert.NotNull(assembler.Landscape.Blocks[CenterIndex()]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TuskerIsland_RenderCenterOffset_DoesNotCullTheViewerLandblock()
|
||||
{
|
||||
// Live regression capture 2026-08-31. Cell 0x22 is local cell
|
||||
// (x=4,y=1), hence the camera must be in x=[96,120], y=[24,48]
|
||||
// in the viewer landblock. The render center was one block south,
|
||||
// so the published eye was y=222.216 (= 30.216 + 192). The landscape
|
||||
// grid is viewer-block-local and must carry that 192 m basis offset
|
||||
// when classifying its blocks against the camera plane.
|
||||
const uint tuskerLandblock = 0xF07FFFFFu;
|
||||
const uint tuskerCell = 0xF07F0022u;
|
||||
var eye = new Vector3(107.31238f, 222.21594f, 16.352213f);
|
||||
var forward = Vector3.Normalize(new Vector3(-0.66922873f, 0.6854568f, -0.286918f));
|
||||
var viewProjection = new Matrix4x4(
|
||||
0.79605f, -0.39642528f, -0.66922873f, -0.6692153f,
|
||||
0.7772036f, 0.4060382f, 0.6854568f, 0.6854431f,
|
||||
0f, 1.8946906f, -0.286918f, -0.28691226f,
|
||||
-258.13306f, -78.669205f, -75.91117f, -75.809654f);
|
||||
|
||||
var assembler = new WalkLandscapeAssembler();
|
||||
assembler.PublishLandblock(
|
||||
tuskerLandblock,
|
||||
maxZ: 220f,
|
||||
minZ: -10f,
|
||||
Array.Empty<WalkBuildingFactory.Entry>());
|
||||
assembler.SetViewer(tuskerCell, eye);
|
||||
|
||||
var context = new WalkProductionFrameContext(
|
||||
new CellVisibility(),
|
||||
new WalkBuildingRegistry(),
|
||||
eye,
|
||||
forward,
|
||||
viewProjection,
|
||||
1760f,
|
||||
990f);
|
||||
var recorder = new LandscapeCellRecorder();
|
||||
|
||||
new RetailFrameWalk().WalkFrame(
|
||||
tuskerCell,
|
||||
cameraCell: null,
|
||||
assembler.Landscape,
|
||||
context,
|
||||
recorder);
|
||||
|
||||
Assert.Contains(
|
||||
recorder.Cells,
|
||||
cellId => (cellId & 0xFFFF0000u) == (tuskerLandblock & 0xFFFF0000u));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,4 +128,49 @@ public sealed class WalkProductionFrameContextTests
|
|||
new CellVisibility(), new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
||||
default, 1024f, 768f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_RebindsCameraValuesAndRetainsTheRayCaster()
|
||||
{
|
||||
Matrix4x4 firstProjection = SimpleViewProjection();
|
||||
var ctx = new WalkProductionFrameContext(
|
||||
new CellVisibility(), new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
||||
firstProjection, 1024f, 768f);
|
||||
IWalkRayCaster retainedRays = ctx.Rays;
|
||||
|
||||
var eye = new Vector3(4f, 5f, 6f);
|
||||
Vector3 forward = Vector3.UnitX;
|
||||
Matrix4x4 secondProjection =
|
||||
Matrix4x4.CreateLookAt(eye, eye + forward, Vector3.UnitZ)
|
||||
* Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 4f, 16f / 9f, 0.1f, 500f);
|
||||
|
||||
ctx.Reset(eye, forward, secondProjection, 1760f, 990f);
|
||||
|
||||
Assert.Same(retainedRays, ctx.Rays);
|
||||
Assert.Equal(eye, ctx.WorldViewpoint);
|
||||
Assert.Equal(1760f, ctx.ViewportWidth);
|
||||
Assert.Equal(990f, ctx.ViewportHeight);
|
||||
Assert.Equal(forward, ctx.CyPlane.Normal);
|
||||
Assert.Equal(-Vector3.Dot(eye, forward) - WalkProductionFrameContext.ZNear, ctx.CyPlane.D);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_WithBadProjectionLeavesThePreviousBindingUsable()
|
||||
{
|
||||
Matrix4x4 projection = SimpleViewProjection();
|
||||
var eye = new Vector3(1f, 2f, 3f);
|
||||
var ctx = new WalkProductionFrameContext(
|
||||
new CellVisibility(), new WalkBuildingRegistry(), eye, Vector3.UnitY,
|
||||
projection, 1024f, 768f);
|
||||
Vector3 rayBeforeFailure = ctx.Rays.RayThrough(200f, 300f);
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
ctx.Reset(new Vector3(9f), Vector3.UnitX, default, 1f, 1f));
|
||||
|
||||
Assert.Equal(eye, ctx.WorldViewpoint);
|
||||
Assert.Equal(1024f, ctx.ViewportWidth);
|
||||
Assert.Equal(768f, ctx.ViewportHeight);
|
||||
Assert.Equal(Vector3.UnitY, ctx.CyPlane.Normal);
|
||||
Assert.Equal(rayBeforeFailure, ctx.Rays.RayThrough(200f, 300f));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Walk;
|
||||
|
||||
public sealed class WalkProductionWorldDataTests
|
||||
{
|
||||
[Fact]
|
||||
public void BucketOutdoorRecord_UsesEveryOutdoorPhysicsShadowCellAcrossLandblockEdge()
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0x1234u,
|
||||
position: new Vector3(191f, 191f, 0f));
|
||||
var buckets = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
|
||||
WalkProductionWorldData.BucketOutdoorRecord(
|
||||
in record,
|
||||
[0xF07F0040u, 0xF0800001u, 0xF4180101u],
|
||||
buckets,
|
||||
renderCenterLbX: 0xF0,
|
||||
renderCenterLbY: 0x7F);
|
||||
|
||||
Assert.Equal([0xF07F0040u, 0xF0800001u], buckets.Keys.Order());
|
||||
Assert.All(buckets.Values, bucket => Assert.Equal(record, Assert.Single(bucket)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BucketOutdoorRecord_UnregisteredEffectFallsBackToRootPositionCell()
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0x5678u,
|
||||
position: new Vector3(193f, 25f, 0f));
|
||||
var buckets = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
|
||||
WalkProductionWorldData.BucketOutdoorRecord(
|
||||
in record,
|
||||
Array.Empty<uint>(),
|
||||
buckets,
|
||||
renderCenterLbX: 0xEF,
|
||||
renderCenterLbY: 0x7F);
|
||||
|
||||
Assert.Equal([0xF07F0002u], buckets.Keys);
|
||||
Assert.Equal(record, Assert.Single(buckets[0xF07F0002u]));
|
||||
}
|
||||
|
||||
private static RenderProjectionRecord Record(uint id, Vector3 position) =>
|
||||
new RenderProjectionRecord() with
|
||||
{
|
||||
Id = RenderProjectionId.FromRaw(id),
|
||||
Transform = new RenderTransform(Matrix4x4.CreateTranslation(position)),
|
||||
Source = new RenderSourceMetadata() with { LocalEntityId = id },
|
||||
};
|
||||
}
|
||||
|
|
@ -190,6 +190,51 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
Assert.Equal((uint)alphaGfxObj, selectionParts[1].GfxObjId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyEntityForWalk_NoDrawProjectileRecord_SubmitsNeitherMeshNorSelection()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const ulong projectileGfxObj = 0x0100_0003UL;
|
||||
InjectRenderData(fx.Manager, projectileGfxObj, MakeFlatMesh(
|
||||
MakeBatch(
|
||||
0x08000003u,
|
||||
TranslucencyKind.Opaque,
|
||||
firstIndex: 0,
|
||||
baseVertex: 0,
|
||||
indexCount: 3,
|
||||
textureSlotIndex: 1)));
|
||||
|
||||
RenderProjectionRecord projectile = MakeRecord(
|
||||
localEntityId: 101,
|
||||
serverGuid: 0x7000_0101u,
|
||||
position: Vector3.Zero,
|
||||
meshRefs: [new MeshRef((uint)projectileGfxObj, Matrix4x4.Identity)])
|
||||
with
|
||||
{
|
||||
// ACE's projectile-impact SetState sets NoDraw immediately;
|
||||
// the later DeleteObject intentionally arrives five seconds
|
||||
// afterward. The journal projects that state as resident and
|
||||
// hidden, with Draw cleared.
|
||||
Flags = RenderProjectionFlags.SpatiallyResident
|
||||
| RenderProjectionFlags.Selectable
|
||||
| RenderProjectionFlags.Hidden,
|
||||
};
|
||||
|
||||
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
||||
var selectionParts =
|
||||
new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
|
||||
|
||||
fx.Dispatcher.ClassifyEntityForWalk(
|
||||
in projectile,
|
||||
tupleLandblockId: 0x8C04u,
|
||||
batches,
|
||||
selectionParts,
|
||||
liveDynamic: true);
|
||||
|
||||
Assert.Empty(batches);
|
||||
Assert.Empty(selectionParts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyEntityForWalk_SetupComposite_EncodesPartAndSetupPartIndexLikePackedRoute()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue