fix(render): restore landscape objects and walk alpha order

This commit is contained in:
Erik 2026-08-31 10:49:33 +02:00
parent 4683ac6f72
commit e880860291
5 changed files with 275 additions and 21 deletions

View file

@ -40,10 +40,13 @@ public sealed class WalkFrameDriverTests
// interleave (stream flushes interleaved with sky/terrain/shell/punch/
// alpha-barrier), not just each half in isolation. ─────────────────────
private sealed class RecordingLeafRenderer(List<string> log) : IWalkFrameLeafRenderer
private sealed class RecordingLeafRenderer(
List<string> log,
RetailAlphaQueue? alpha = null) : IWalkFrameLeafRenderer
{
public readonly List<WalkPolygon> Punches = new();
public readonly List<(uint CellId, uint ClipSlot)> Shells = new();
public readonly List<int> AlphaPendingAtBarrier = new();
public void DrawSky() => log.Add("SKY");
@ -65,7 +68,18 @@ public sealed class WalkFrameDriverTests
log.Add($"PUNCH:{worldPolygon.Vertices.Length}@v{activeViewIndex}");
}
public void AlphaBarrier() => log.Add("ALPHA");
public void AlphaBarrier()
{
if (alpha is null)
{
log.Add("ALPHA");
return;
}
AlphaPendingAtBarrier.Add(alpha.PendingCount);
log.Add($"ALPHA:{alpha.PendingCount}");
alpha.Flush();
}
public void DrawStaticParticles(IReadOnlySet<uint> ownerIds)
{
@ -762,6 +776,89 @@ public sealed class WalkFrameDriverTests
Assert.Equal(1u, mdi[0].DrawCount);
}
[Fact]
public void CoarseLandscapeCellTurn_ExpandsToEveryCoveredAuthoritativeOwnerCell()
{
using var fx = new DispatcherFixture();
const ulong gfxObj = 0x0200_0022UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08100022u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
var worldData = new FakeWorldData();
worldData.OutdoorStaticsByCell[0xE43D0040u] = new WalkFrameStaticRecords(
new[] { MakeRecord(304, 0, Vector3.Zero, [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]) },
0xE43Du);
var driver = new WalkFrameDriver(
fx.Dispatcher,
new RecordingLeafRenderer(new List<string>()),
worldData);
var ctx = new TestContext();
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);
// A 2x2 coarse grid's cell 3 covers authoritative x/y cells 4..7,
// including original cell 64. The old cellIndex+1 lookup only asked
// for ids 1..4 and silently lost this record.
((IWalkEventSink)driver).OnLandscapeCellTurn(0xE43DFFFFu, 2, 3);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Contains(0xE43D0040u, driver.VisitedLandscapeCellIds);
GpuRecordedMultiDrawIndirect call = Assert.Single(
fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
Assert.Equal(1u, call.DrawCount);
}
[Fact]
public void Collect_DoesNotExposeNearAlphaToEarlierBuildingBarrier()
{
using var fx = new DispatcherFixture();
const ulong gfxObj = 0x0200_0023UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08100023u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1)));
var worldData = new FakeWorldData();
worldData.OutdoorStaticsByCell[0xE43D0001u] = new WalkFrameStaticRecords(
new[] { MakeRecord(305, 0, new Vector3(0, 0, -20), [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]) },
0xE43Du);
worldData.OutdoorStaticsByCell[0xE43D0002u] = new WalkFrameStaticRecords(
new[] { MakeRecord(306, 0, new Vector3(0, 0, -2), [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]) },
0xE43Du);
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log, fx.AlphaQueue);
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
var ctx = new TestContext();
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
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(0xE43D0001u);
((IWalkEventSink)driver).OnBuildingTurn(new WalkBuilding());
((IWalkEventSink)driver).OnLandscapeCellTurn(0xE43D0002u);
driver.EndFrame();
Assert.Equal(0, fx.AlphaQueue.PendingCount);
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(new[] { 1 }, leaf.AlphaPendingAtBarrier);
Assert.Equal(1, fx.AlphaQueue.PendingCount);
}
// ── Fixture (mirrors WalkStaticStreamPopulatorTests' DispatcherFixture —
// FW3.2a's own referee) ─────────────────────────────────────────────────
@ -838,12 +935,18 @@ public sealed class WalkFrameDriverTests
{
private readonly IDisposable _publication;
private readonly IGpuPassEncoder _pass;
private readonly RetailAlphaQueue? _alpha;
public DrawScope(IGpuFrame frame, IGpuPassEncoder pass, IDisposable publication)
public DrawScope(
IGpuFrame frame,
IGpuPassEncoder pass,
IDisposable publication,
RetailAlphaQueue? alpha = null)
{
Frame = frame;
_pass = pass;
_publication = publication;
_alpha = alpha;
}
public IGpuFrame Frame { get; }
@ -852,6 +955,8 @@ public sealed class WalkFrameDriverTests
public void Dispose()
{
if (_alpha?.IsCollecting == true)
_alpha.EndFrame();
_publication.Dispose();
_pass.Dispose();
}
@ -886,7 +991,8 @@ public sealed class WalkFrameDriverTests
_meshAdapter,
entitySpawnAdapter,
new EntityClassificationCache(),
new AcDream.Core.Rendering.TranslucencyFadeManager());
new AcDream.Core.Rendering.TranslucencyFadeManager(),
alphaQueue: AlphaQueue);
}
public RecordingGpuDevice Device { get; }
@ -897,10 +1003,17 @@ public sealed class WalkFrameDriverTests
public WbDrawDispatcher Dispatcher { get; }
public RetailAlphaQueue AlphaQueue { get; } = new();
public ObjectMeshManager Manager => _meshAdapter.MeshManager!;
public DrawScope BeginDraw()
public DrawScope BeginDraw(bool beginAlpha = false)
{
if (beginAlpha)
{
Dispatcher.BeginFrame(frameSlot: 0);
AlphaQueue.BeginFrame();
}
FrameLifetime.BeginFrame();
IGpuFrame frame = FrameLifetime.CurrentFrame!;
IGpuPassEncoder pass = frame.BeginPass(
@ -908,11 +1021,17 @@ public sealed class WalkFrameDriverTests
"fw3-2b-1-walk-frame-driver-test", Vector4.Zero, sampleCount: 1));
IDisposable publication = Scope.Publish(pass);
Device.Clear();
return new DrawScope(frame, pass, publication);
return new DrawScope(
frame,
pass,
publication,
beginAlpha ? AlphaQueue : null);
}
public void Dispose()
{
if (AlphaQueue.IsCollecting)
AlphaQueue.AbortFrame();
Dispatcher.Dispose();
_meshAdapter.Dispose();
_textures.Dispose();