checkpoint: preserve user-gated FW closeout fixes

This commit is contained in:
Erik 2026-08-31 08:27:37 +02:00
parent a2f2eb7d78
commit b8befded8b
22 changed files with 1005 additions and 198 deletions

View file

@ -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) ─────────────────────────────────────────────────

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -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 },
};
}

View file

@ -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()
{