Fix round 1 of the two the plan allows for S4-c2 (048d5b12f), addressing every item M1-M8/A1-A8 from the campaign branch's packet section 11. M1 (BLOCKING): ParticleRenderer.DeferToRetailAlphaQueue threw whenever a mesh-particle batch's TranslucencyKind.Opaque classification (mask 0x00) reached a non-Append DrawMesh row - a real, reachable outcome since TryAppendMeshDraws applies no IsOpaque-style pre-filter unlike WbDrawDispatcher's path. Deleted both throws (ParticleRenderer.cs, WbDrawDispatcher.cs SubmitToAlphaQueue), extracted the router-input derivation into internal static ParticleRenderer.RouteParticleSubmission (directly testable), and implemented rows 1/5's immediate draw by reusing the existing per-instance billboard/mesh draw code (new DrawImmediateParticleSubmissionRhi, no new renderer, no sort per A8). materialHasAlpha (row 4) is derived from CMaterial::CheckAlphaValues @0x005396a0 (Ghidra-verified 2026-09-04): has_alpha=0 only when Ambient.a==Diffuse.a==Specular.a==Emissive.a==1.0f, else 1 - and CMaterial::SetTranslucencySimple @0x005396f0 (reached from CPhysicsPart::SetTranslucency <- Particle::Update @0x0051c290's per-frame alpha-fade) sets all four channels to (1f - translucency) uniformly, so has_alpha == "this particle's current alpha isn't 1.0" - already carried in ColorArgb's top byte (Lerp(StartAlpha,EndAlpha,tLife), ParticleSystem.cs 1039-1041/1453). M3: SubmitToAlphaQueue now takes isBuildingShell + viewProjection and feeds Row 1's detailSurfaceActive gate for translucent building-shell instances (retail DrawBuilding @0x0059f2a0 installs building detail before its own shell subsets reach DrawMesh). Row 1 (detail on) draws the instance immediately at its own walk-stream AlphaSubmitMark via new DrawImmediateAlphaInstance/DrawImmediateAlphaInstanceRhi (reusing WriteDeferredAlphaEntrySlot + the drain's pipeline/detail code for slot 0); never touches _deferredAlpha/the queue for pure-Immediate submissions (fixes a latent leak: previously reserving a _deferredAlpha slot for every candidate meant a frame with ALL-immediate content never registered a source, so ResetAlphaSubmissions never ran). M3c: DrawPreparedAlphaBatchRhi's own per-command hasDetail/DrawBuildingDetailRangeRhi branch is deleted - detail-eligible content no longer reaches the drain (spec: delayed replay is always detailEnabled=0). New AD-120 register row for the "in place -> at the entity's own stream mark" granularity adaptation this introduces. M6: RetailPViewPassExecutor.SubmitOrDrawTransparentCellShell routes an EnvCell's transparent shell through the SAME RetailAlphaMeshRouter table (mask 0x02, detailSurfaceActive = EnvCellRenderer.TransparentDetailEnabled) instead of a hand-coded branch. Detail on: unchanged immediate draw. Detail off: appends ONE token per CELL via new EnvCellAlphaDrawSource (internal, delegate-based so it's unit-testable without a GPU harness), replayed through EnvCellRenderer.RenderTransparentOrdered at the next flush - RenderTransparentOrdered already replayed a cell's batch on demand, so no new deferred-replay abstraction was needed (the original landing's claim that one was required is corrected in the packet). M4: RetailAlphaEntry.IsFirstForList deleted. Retail's per-DrawMesh- invocation "first for this list" flag is trivially true for every subset retail ever appends (DrawMesh @0x0059d4a0 sets both flags true at entry, 0059d4cc/0059d4d0, clearing each independently after ITS OWN first append within that call, 0059d5ef) - the captures confirm it (new=1 on 9,685/10,556 AM lines, impossible under "first since last drain"). acdream's deleted isFirstForList = target.Count == 0 computed the wrong quantifier. One acdream append already IS one DrawMesh call, so the field carried no information and its own pin was vacuous (feedback_every_new_pin_must_be_ shown_to_fail) - deleted rather than kept as an always-true no-op. M5: split all six AlphaFlushTranscript_* KnownFailure Facts into live AlphaFlushSites_* (site+threshold sequence only, content-independent, unconditional pass) and AlphaFlushCounts_* (KnownFailure, full tuple incl. drained counts) sharing one RunAlphaFlushTranscriptReplay helper. Rewrote the KnownFailure reason honestly: THREE independent causes, not "harness content only" - (1) zero live mesh content, (2) acdream appends one entry per INSTANCE vs retail's one per SUBSET per DrawMesh call, (3) acdream's CLIP list is structurally empty for ordinary content (M2). Added ONE live valve pin (WalkFrameDriverTests.SortCellExit_ValveDrainsThroughReplayAtThe ExactBoundary, a Theory over 2250/2249) that goes through WalkFrameDriver. Replay's own SortCellExit arm, not RetailAlphaQueue.Flush called directly. M2/A2/M8c (register): ~~AP-34~~ restored as struck-through RETIRED (it had been deleted outright, not struck, breaking the register's own convention) citing residuals AP-238/AP-239/AP-240. The chunk-2 commit's "AP-236"/ "AP-237" collided with an id ALREADY filed-and-retired on main by an unrelated #132 fix (docs/ISSUES.md:19976) - ids are never reused, so both are renumbered: AP-236->AP-238 (EnvCell; rewritten per M6 - granularity-only residual now, "never through the shared queue" is retired), AP-237->AP-239 (mask-reconstruction gap; the dangling "AP-236b" citation in RetailAlphaMeshRouter.cs never matched either filed id and is corrected to AP-239). New AP-240 filed for M2: WbDrawDispatcher.IsOpaque filters ClipMap out upstream, so ordinary clip-mapped content never reaches retail's CLIP FIFO (draws immediately on the pre-existing OpaqueAlphaToCoverage pipeline instead) - no pipeline change this round, per the contract. AP section header recounted honestly: 159 active (was stale at 161); AD section 92 (was 91, +AD-120). M7: AP-239's only cited example (cloud GfxObj 0x01004C35, surface 0x08000023) is DISPROVEN - decoded Type=0x10114 has alpha-family bits that win in BOTH retail's real priority and acdream's reconstruction, so it never actually diverges. A 2026-09-04 scan of all 6,152 Portal-dat Surfaces found 27 real Translucent+Base1ClipMap-no-alpha-family instances (Type=0x14); cross-referencing GfxObjs found surface 0x08000015 on GfxObj 0x010001EC - cited in place of the disproven cloud example. M8: (a) RenderDeviceD3D::DrawBlock is @0x005a17c0 per symbols.json, not @0x005a18d0 (that is its per-land-cell loop head) - fixed across RetailAlphaQueue.cs, RetailPViewPassExecutor.cs, WalkEvents.cs, WalkFrameDriver.cs (x3), RetailFrameWalk.cs. (b) RetailAlphaQueue.cs's four FlushAlphaList addresses were labelled "return site" but are the CALL instruction's own address (x86 CALL rel32 is 5 bytes) - relabelled "call at X (returns to X+5)". (d) worldbuilder-inventory.md:420's AP-34 citation updated. A1: RetailPViewRenderer.cs's outdoor-root passes.FlushLandscapeAlpha() call (labelled LandscapeFlush) is deleted - retail's real LandscapeFlush (PView::DrawCells @0x005a4840's own FlushAlphaList(0f) @0x005a4872) sits strictly inside if (outside_view.view_count > 0) on the INTERIOR PView; an outdoor root never reaches it (terrace-edge capture: zero 005a4877 FL lines). Verified no opaque draw intervenes between this deletion point and RetailAlphaQueue.EndFrame()'s own RenderNormalMode pass-end flush (WorldSceneRenderer.cs: DrawPostWorldParticles's PView branch is an explicit no-op; only diagnostic wireframes follow) - #132's "drain after the finished opaque world" invariant is kept by that flush alone. A4: RetailAlphaQueue's ApplyScratchRetention call sites passed the ENTRY count as the SOURCE-count argument too (observedClip + observedAlpha twice), so _sources/_sourceDrawOffsets never shrank toward the real source count. Fixed to capture _sources.Count before Clear(). A5/A6/A7: WalkStaticStreamPopulatorTests.cs's stale "same viewer distance" banner corrected; two off-by-one entry counts in the packet's section 10 sequences corrected (foundry-deep x10->x11, holtburg-doorway-still x5->x6); WalkAlphaDepthTrace.cs's "new=" comment explains why IsNew is retained but never compared (M4's finding makes a per-subset comparison meaningless at acdream's per-instance granularity); RetailAlphaMeshRouterTests.cs's IndependentReferenceRoute (never actually independently-shaped) renamed to RestatedBranchTableRoute with an honest doc comment, and the unverified "flipped in turn and failed every time" claim deleted. A3 was resolved as a side effect of M1's SubmitToAlphaQueue doc-comment rewrite (the wrong "0x02/0x03, 0x04/0x05, 0x08/0x09" enumeration no longer exists); RetailAlphaMeshRouter.cs's own MaskFromTranslucencyKind comment updated in the same spirit (M1's particle-site Opaque-reachability finding). Mutation checks (each applied to a scratch-restored copy, confirmed failing, then byte-exact reverted - diffed against a /tmp backup after restore): - A1 (RetailPViewRendererTests.DrawInside_NeverCallsFlushLandscapeAlphaDirectly): restoring the deleted "if (ctx.RootCell.IsOutdoorNode) passes.FlushLandscapeAlpha();" line reintroduces a direct RetailPViewPassExecutor.FlushLandscapeAlpha call in DrawInside's compiled body. Actual failure: "Assert.DoesNotContain() Failure: Filter matched in collection ... CompiledCall { ... Target = Void FlushLandscapeAlpha() }". - A4 (RetailAlphaQueueTests.RetainedSourceCapacity_ConvergesToTheReal SourceCountNotTheEntryCount): reverting both ApplyScratchRetention calls to (observedClip + observedAlpha, observedClip + observedAlpha) - with 100 distinct sources in the spike frame (so _sources.Capacity grows to 128, past its initial 4) followed by three low-demand frames of 10 entries via ONE repeated source - makes sourceTarget compute from 10 (Math.Max(4,10*2)=20) instead of 1 (Math.Max(4,1*2)=4). Actual failure: "Expected the retained source-array capacity to converge toward the real source count (1), but it stayed at 20". (A single-source-only spike does NOT discriminate this bug - Math.Min(sourceTarget, _sources.Capacity) clamps both formulas to the same unchanged initial 4 - hence the 100-source setup.) - M5 valve (WalkFrameDriverTests.SortCellExit_ValveDrainsThroughReplayAt TheExactBoundary, 2250 case): flipping RetailAlphaQueue.Flush's no-op comparison from "<" to "<=" (S4-c1/c2's own boundary mutation) makes the 2250-entries case a no-op instead of draining. Actual failure: "Assert.Equal() Failure: Values differ Expected: 0 Actual: 2250". - M6 (EnvCellAlphaDrawSourceTests, both Facts): adding an immediate renderTransparentOrdered(new[]{cellId}) call inside EnvCellAlphaDrawSource.AddPendingCellId (simulating the M1/M3 "drew immediately instead of deferring" bug class) makes PendingCellToken_IsDrainedAtTheFlushAndNeverBeforeIt fail with "Assert.Empty() Failure: Collection was not empty Collection: [[4660]]" and ParticleAppendedBetweenTwoCellTokens_KeepsItsPositionInTheCombined Drain fail with 'Expected: string[] ["cell:256", "particle:7", "cell:512"] Actual: List<string> ["cell:256", "cell:512", "cell:256", "particle:7", "cell:512"]'. - M1 (ParticleRendererRouteTests.OpaqueClassifiedMeshBatch_WithMaterialAlpha _RoutesToAlphaAppend): hardcoding materialHasAlpha to false inside RouteParticleSubmission makes the Opaque-mask+has-alpha case route Immediate instead of Append. Actual failure: "Assert.Equal() Failure: Values differ Expected: Append Actual: Immediate". Gates (real output lines): - dotnet build AcDream.slnx -c Release -> "Build succeeded. 0 Warning(s) 0 Error(s)". - Hermetic lane (--filter "Lane!=InstalledDat&Lane!=PreparedPackage& Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux& Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure") -> "Passed! - Failed: 0, Passed: 6864, Skipped: 0, Total: 6864". - InstalledDat lane (ACDREAM_RUN_INSTALLED_DAT_TESTS=1, ACDREAM_DAT_DIR="C:/Users/erikn/Documents/Asheron's Call", --filter "Lane=InstalledDat") -> "Failed: 10, Passed: 255, Skipped: 1, Total: 266" - the exact 4 pre-existing failures (TowerAscent_StaircaseStaysConeVisible_EveryStep, LayoutImporterMediaBearingChildSweepTests, LayoutImporterInvisibleSweep Tests, Oh_doorway_still_first_frame_diff) plus the six AlphaFlushCounts_* KnownFailure Facts, nothing else; all six AlphaFlushSites_* Facts pass inside this same lane (the whole WalkTraceConformanceTests partial class carries Lane=InstalledDat, so they cannot run hermetically). - Shader classes (VulkanShaderDescriptorContractTests, VulkanShaderManifestTests, RenderPackSpirvValidatorTests) -> "Passed! - Failed: 0, Passed: 32, Skipped: 0, Total: 32". - RetailAlphaQueueTests, RetailAlphaMeshRouterTests, RetailFrameWalkTests, WalkFrameDriverTests, ParticleRenderer*Tests, EnvCellRenderer*Tests (combined filter) -> "Passed! - Failed: 0, Passed: 120, Skipped: 0, Total: 120". - Register collision grep: AP-238/AP-239/AP-240/AD-120 each count exactly 1; ~~AP-34~~ count exactly 1; live totals AP=159, AD=92. Not done / deferred: none - all M1-M8/A1-A8 items landed; A8 was a no-action note. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
887 lines
35 KiB
C#
887 lines
35 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Collections.ObjectModel;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Numerics;
|
|
using System.Reflection;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Gpu;
|
|
using AcDream.App.Rendering.Gpu.Vk;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.App.Rendering.Selection;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Rendering.Walk;
|
|
using AcDream.App.Tests.Rendering.Gpu;
|
|
using AcDream.Content;
|
|
using AcDream.Core.Meshing;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Enums;
|
|
using DatReaderWriter.Lib.IO;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Walk;
|
|
|
|
/// <summary>
|
|
/// Campaign FW stage FW3.2a: the walk→draw population layer's data-
|
|
/// equivalence referee. Covers <see cref="WbDrawDispatcher.ClassifyEntityForWalk"/>
|
|
/// (the shared per-entity classify seam), <see cref="WalkStaticStreamPopulator"/>
|
|
/// (opaque → <see cref="OrderedDrawStream"/>, translucent → the alpha queue,
|
|
/// selection publish), and the FW3.2a own-cull-scratch fix to the ordered
|
|
/// submitter (<c>PrepareOrderedStream</c>/<c>DrawOrderedRange</c> as of
|
|
/// Campaign FW stage FW3.4a).
|
|
/// </summary>
|
|
public sealed class WalkStaticStreamPopulatorTests
|
|
{
|
|
// ── Test doubles ────────────────────────────────────────────────────────
|
|
|
|
private sealed class RecordingSelectionSink : IRetailSelectionRenderSink
|
|
{
|
|
public readonly List<(uint ServerGuid, uint LocalEntityId, int PartIndex, uint GfxObjId, Matrix4x4 LocalToWorld)>
|
|
Calls = new();
|
|
|
|
public void AddVisiblePart(
|
|
uint serverGuid, uint localEntityId, int partIndex, uint gfxObjId, Matrix4x4 partWorld) =>
|
|
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,
|
|
bool testSphere = true) => slots.Length != 0;
|
|
}
|
|
|
|
// ── Synthetic RenderProjectionRecord construction ──────────────────────
|
|
|
|
private static RenderProjectionRecord MakeRecord(
|
|
uint localEntityId,
|
|
uint serverGuid,
|
|
Vector3 position,
|
|
IReadOnlyList<MeshRef> meshRefs,
|
|
bool isBuildingShell = false,
|
|
uint parentCellId = 0u,
|
|
RenderCasterIdentityKind casterIdentity =
|
|
RenderCasterIdentityKind.Unclassified) =>
|
|
new(
|
|
Id: RenderProjectionId.FromRaw(localEntityId),
|
|
ProjectionClass: RenderProjectionClass.OutdoorStatic,
|
|
OwnerIncarnation: RenderOwnerIncarnation.FromRaw(1),
|
|
Transform: new RenderTransform(Matrix4x4.CreateTranslation(position)),
|
|
PreviousTransform: default,
|
|
MeshSet: default,
|
|
Material: default,
|
|
Residency: default,
|
|
Bounds: default,
|
|
Flags: RenderProjectionFlags.Draw,
|
|
DegradeState: default,
|
|
SortKey: new RenderSortKey(0),
|
|
DirtyMask: default,
|
|
Source: new RenderSourceMetadata(
|
|
LocalEntityId: localEntityId,
|
|
ServerGuid: serverGuid,
|
|
SourceId: 0,
|
|
ParentCellId: parentCellId,
|
|
EffectCellId: 0,
|
|
BuildingShellAnchorCellId: 0,
|
|
TransformFingerprint: default,
|
|
GeometryFingerprint: default,
|
|
AppearanceFingerprint: default),
|
|
EntityPayload: new RenderEntityPayload(
|
|
MeshRefs: meshRefs,
|
|
PaletteOverride: null,
|
|
IsBuildingShell: isBuildingShell,
|
|
CasterIdentity: casterIdentity));
|
|
|
|
private static ObjectRenderBatch MakeBatch(
|
|
uint surfaceId,
|
|
TranslucencyKind translucency,
|
|
uint firstIndex,
|
|
int baseVertex,
|
|
int indexCount,
|
|
uint textureSlotIndex,
|
|
uint textureLayer = 0,
|
|
CullMode cullMode = CullMode.CounterClockwise) =>
|
|
new()
|
|
{
|
|
Key = new TextureKey { SurfaceId = surfaceId, IsSolid = false },
|
|
Translucency = translucency,
|
|
FirstIndex = firstIndex,
|
|
BaseVertex = (uint)baseVertex,
|
|
IndexCount = indexCount,
|
|
TextureSlot = new GpuTextureSlot(textureSlotIndex),
|
|
TextureIndex = (int)textureLayer,
|
|
};
|
|
|
|
private static ObjectRenderData MakeFlatMesh(params ObjectRenderBatch[] batches) =>
|
|
new() { Batches = new List<ObjectRenderBatch>(batches) };
|
|
|
|
// ── Reflection seam: ObjectMeshManager owns no test-injection API, and
|
|
// driving real GPU/GfxObj upload for a unit test is out of this stage's
|
|
// scope — ObjectRenderData/ObjectRenderBatch are plain settable classes,
|
|
// so this seeds the manager's private cache directly. ──────────────────
|
|
|
|
private static void InjectRenderData(ObjectMeshManager manager, ulong id, ObjectRenderData data)
|
|
{
|
|
FieldInfo field = typeof(ObjectMeshManager).GetField(
|
|
"_renderData", BindingFlags.NonPublic | BindingFlags.Instance)
|
|
?? throw new InvalidOperationException(
|
|
"ObjectMeshManager._renderData field not found — test relies on this exact name.");
|
|
var dict = (ConcurrentDictionary<ulong, ObjectRenderData>)field.GetValue(manager)!;
|
|
dict[id] = data;
|
|
}
|
|
|
|
// ── Deliverable 1: ClassifyEntityForWalk data equivalence ─────────────
|
|
|
|
[Fact]
|
|
public void ClassifyEntityForWalk_OneOpaqueAndOneTranslucentPart_YieldsBatchesInRecordOrderWithCorrectIsOpaque()
|
|
{
|
|
using var fx = new DispatcherFixture();
|
|
const ulong opaqueGfxObj = 0x0100_0001UL;
|
|
const ulong alphaGfxObj = 0x0100_0002UL;
|
|
InjectRenderData(fx.Manager, opaqueGfxObj, MakeFlatMesh(
|
|
MakeBatch(0x08000001u, TranslucencyKind.Opaque, firstIndex: 0, baseVertex: 0, indexCount: 3, textureSlotIndex: 1)));
|
|
InjectRenderData(fx.Manager, alphaGfxObj, MakeFlatMesh(
|
|
MakeBatch(0x08000002u, TranslucencyKind.AlphaBlend, firstIndex: 3, baseVertex: 4, indexCount: 6, textureSlotIndex: 2)));
|
|
|
|
var meshRefs = new[]
|
|
{
|
|
new MeshRef((uint)opaqueGfxObj, Matrix4x4.CreateTranslation(1, 0, 0)),
|
|
new MeshRef((uint)alphaGfxObj, Matrix4x4.CreateTranslation(0, 1, 0)),
|
|
};
|
|
RenderProjectionRecord record = MakeRecord(
|
|
localEntityId: 100, serverGuid: 0, position: new Vector3(5, 6, 7), meshRefs);
|
|
|
|
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
|
var selectionParts = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
|
|
fx.Dispatcher.ClassifyEntityForWalk(in record, tupleLandblockId: 0x8C04u, batches, selectionParts);
|
|
|
|
Assert.Equal(2, batches.Count);
|
|
|
|
WbDrawDispatcher.WalkClassifiedBatch opaque = batches[0];
|
|
Assert.True(opaque.IsOpaque);
|
|
Assert.Equal(TranslucencyKind.Opaque, opaque.Key.Translucency);
|
|
Assert.Equal(0u, opaque.Key.FirstIndex);
|
|
Assert.Equal(3, opaque.Key.IndexCount);
|
|
Assert.Equal(1u, opaque.Key.TextureSlot.Index);
|
|
Assert.Equal(1f, opaque.Alpha);
|
|
Assert.Equal(meshRefs[0].PartTransform * record.Transform.LocalToWorld, opaque.Transform);
|
|
|
|
WbDrawDispatcher.WalkClassifiedBatch translucent = batches[1];
|
|
Assert.False(translucent.IsOpaque);
|
|
Assert.Equal(TranslucencyKind.AlphaBlend, translucent.Key.Translucency);
|
|
Assert.Equal(3u, translucent.Key.FirstIndex);
|
|
Assert.Equal(6, translucent.Key.IndexCount);
|
|
Assert.Equal(2u, translucent.Key.TextureSlot.Index);
|
|
Assert.Equal(meshRefs[1].PartTransform * record.Transform.LocalToWorld, translucent.Transform);
|
|
|
|
Assert.Equal(2, selectionParts.Count);
|
|
Assert.Equal(100u, selectionParts[0].LocalEntityId);
|
|
Assert.Equal(0, selectionParts[0].PartIndex);
|
|
Assert.Equal((uint)opaqueGfxObj, selectionParts[0].GfxObjId);
|
|
Assert.Equal(opaque.Transform, selectionParts[0].LocalToWorld);
|
|
Assert.Equal(1, selectionParts[1].PartIndex);
|
|
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()
|
|
{
|
|
using var fx = new DispatcherFixture();
|
|
const ulong setupGfxObj = 0x1000_0010UL;
|
|
const ulong trunkGfxObj = 0x0100_0011UL;
|
|
const ulong leavesGfxObj = 0x0100_0012UL;
|
|
|
|
InjectRenderData(fx.Manager, trunkGfxObj, MakeFlatMesh(
|
|
MakeBatch(0x08000011u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
|
InjectRenderData(fx.Manager, leavesGfxObj, MakeFlatMesh(
|
|
MakeBatch(0x08000012u, TranslucencyKind.ClipMap, 3, 4, 6, 2)));
|
|
InjectRenderData(fx.Manager, setupGfxObj, new ObjectRenderData
|
|
{
|
|
IsSetup = true,
|
|
SetupParts = new List<(ulong GfxObjId, Matrix4x4 Transform)>
|
|
{
|
|
(trunkGfxObj, Matrix4x4.CreateTranslation(0, 0, 1)),
|
|
(leavesGfxObj, Matrix4x4.CreateTranslation(0, 0, 2)),
|
|
},
|
|
});
|
|
|
|
var meshRefs = new[] { new MeshRef((uint)setupGfxObj, Matrix4x4.Identity) };
|
|
RenderProjectionRecord record = MakeRecord(200, 0, Vector3.Zero, meshRefs);
|
|
|
|
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
|
var selectionParts = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
|
|
fx.Dispatcher.ClassifyEntityForWalk(in record, 0x8C04u, batches, selectionParts);
|
|
|
|
Assert.Equal(2, batches.Count);
|
|
Assert.Equal(2, selectionParts.Count);
|
|
// partIndex=0 (the entity's single top-level MeshRef) << 16 | setupPartIndex.
|
|
Assert.Equal(0, selectionParts[0].PartIndex);
|
|
Assert.Equal(1, selectionParts[1].PartIndex);
|
|
Assert.Equal((uint)trunkGfxObj, selectionParts[0].GfxObjId);
|
|
Assert.Equal((uint)leavesGfxObj, selectionParts[1].GfxObjId);
|
|
}
|
|
|
|
[Fact]
|
|
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;
|
|
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);
|
|
|
|
WbDrawDispatcher.WalkClassifiedBatch batch = Assert.Single(batches);
|
|
Assert.Equal(0u, batch.ClipSlot);
|
|
Assert.Single(selectionParts);
|
|
}
|
|
|
|
// ── Deliverable 2: WalkStaticStreamPopulator routing ───────────────────
|
|
|
|
[Fact]
|
|
public void PopulateCell_OpaqueBatchAppendsOrderedDrawCommandInRecordOrderWithStageAndCellProvenance()
|
|
{
|
|
using var fx = new DispatcherFixture();
|
|
const ulong gfxObj = 0x0100_0003UL;
|
|
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
|
|
MakeBatch(0x08000003u, TranslucencyKind.Opaque, 10, 2, 12, 5)));
|
|
|
|
var record = MakeRecord(300, 0, new Vector3(1, 2, 3), new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) });
|
|
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
|
|
var stream = new OrderedDrawStream();
|
|
|
|
populator.PopulateCell(
|
|
stream, WalkDrawStage.CellStatic, cellId: 0x8C040100u,
|
|
new[] { record }, tupleLandblockId: 0x8C04u,
|
|
cameraWorldPosition: Vector3.Zero, viewProjection: Matrix4x4.Identity);
|
|
|
|
Assert.Equal(1, stream.Count);
|
|
Assert.Equal(WalkDrawStage.CellStatic, stream.Stages[0]);
|
|
Assert.Equal(0x8C040100u, stream.CellIds[0]);
|
|
Assert.Equal(10u, stream.Keys[0].FirstIndex);
|
|
Assert.Equal(12, stream.Keys[0].IndexCount);
|
|
Assert.Equal(1f, stream.Alphas[0]);
|
|
Assert.Equal(record.Transform.LocalToWorld, stream.Transforms[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void PopulateOutdoorStatics_UsesTheOutdoorStaticStage()
|
|
{
|
|
using var fx = new DispatcherFixture();
|
|
const ulong gfxObj = 0x0100_0004UL;
|
|
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
|
|
MakeBatch(0x08000004u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
|
|
|
var record = MakeRecord(400, 0, Vector3.Zero, new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) });
|
|
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
|
|
var stream = new OrderedDrawStream();
|
|
|
|
populator.PopulateOutdoorStatics(
|
|
stream, cellId: 0x8C040000u, new[] { record }, tupleLandblockId: 0x8C04u,
|
|
cameraWorldPosition: Vector3.Zero, viewProjection: Matrix4x4.Identity);
|
|
|
|
Assert.Equal(1, stream.Count);
|
|
Assert.Equal(WalkDrawStage.OutdoorStatic, stream.Stages[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void PopulateCell_TranslucentBatchDoesNotAppendToTheStreamAndReachesTheAlphaQueue()
|
|
{
|
|
using var fx = new DispatcherFixture(withAlphaQueue: true);
|
|
const ulong gfxObj = 0x0100_0005UL;
|
|
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
|
|
MakeBatch(0x08000005u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1)));
|
|
|
|
var record = MakeRecord(500, 0, new Vector3(0, 0, 10), new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) });
|
|
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
|
|
var stream = new OrderedDrawStream();
|
|
|
|
fx.AlphaQueue!.BeginFrame();
|
|
populator.PopulateCell(
|
|
stream, WalkDrawStage.CellStatic, 0x8C040100u, new[] { record }, 0x8C04u,
|
|
cameraWorldPosition: Vector3.Zero, viewProjection: Matrix4x4.Identity);
|
|
|
|
Assert.Equal(0, stream.Count);
|
|
Assert.Equal(1, fx.AlphaQueue.PendingCount);
|
|
fx.AlphaQueue.AbortFrame();
|
|
}
|
|
|
|
[Fact]
|
|
public void PopulateCell_PublishesSelectionPartsForEveryClassifiedEntity()
|
|
{
|
|
var sink = new RecordingSelectionSink();
|
|
using var fx = new DispatcherFixture(selectionSink: sink);
|
|
const ulong gfxObj = 0x0100_0006UL;
|
|
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
|
|
MakeBatch(0x08000006u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
|
|
|
var record = MakeRecord(600, serverGuid: 0x8000_0060u, new Vector3(1, 1, 1),
|
|
new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) });
|
|
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
|
|
var stream = new OrderedDrawStream();
|
|
|
|
populator.PopulateCell(
|
|
stream, WalkDrawStage.CellStatic, 0x8C040100u, new[] { record }, 0x8C04u,
|
|
Vector3.Zero, Matrix4x4.Identity);
|
|
|
|
var call = Assert.Single(sink.Calls);
|
|
Assert.Equal(0x8000_0060u, call.ServerGuid);
|
|
Assert.Equal(600u, call.LocalEntityId);
|
|
Assert.Equal(0, call.PartIndex);
|
|
Assert.Equal((uint)gfxObj, call.GfxObjId);
|
|
Assert.Equal(record.Transform.LocalToWorld, call.LocalToWorld);
|
|
}
|
|
|
|
// ── SubmitWalkAlphaInstance: same per-instance data and router decision
|
|
// as DeferTransparentGroups, through the REAL RetailAlphaQueue — S4-c2
|
|
// fix round 1 (A5): this banner's stale "same viewer distance" text is
|
|
// corrected; the FIFO cutover deleted viewer distance from the alpha
|
|
// path entirely. ────────────────────────────────────────────────────
|
|
|
|
/// <summary>S4-c2: retail's queues are FIFO, not distance-sorted — this
|
|
/// pins the routing decision instead (an AlphaBlend batch's constructed
|
|
/// mask is 0x02, which has no ClipMap bit, so
|
|
/// <see cref="RetailAlphaMeshRouter.Route"/> appends it to ALPHA, never
|
|
/// CLIP), plus the exact token. Mutation check: routing AlphaBlend to
|
|
/// CLIP instead makes <c>ClipCount</c> assert fail (1 instead of the
|
|
/// expected 0) and <c>AlphaCount</c> fail (0 instead of 1).</summary>
|
|
[Fact]
|
|
public void SubmitWalkAlphaInstance_RoutesAlphaBlendBatchIntoTheAlphaList()
|
|
{
|
|
using var fx = new DispatcherFixture(withAlphaQueue: true);
|
|
fx.AlphaQueue!.BeginFrame();
|
|
|
|
var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend, FoliageFlags: 0);
|
|
Vector3 localSortCenter = new(1, 2, 3);
|
|
Matrix4x4 model = Matrix4x4.CreateTranslation(4, 5, 6);
|
|
var cameraWorldPosition = Vector3.Zero;
|
|
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
|
|
key, model, ClipSlot: 7, WbDrawDispatcher.InstanceLightSet.Disabled, IndoorFlag: 1,
|
|
Alpha: 0.5f, SelectionLighting: new Vector2(0.25f, 0.75f), DetailCategory: 1,
|
|
IsOpaque: false, LocalSortCenter: localSortCenter);
|
|
|
|
fx.Dispatcher.SubmitWalkAlphaInstance(in batch, cameraWorldPosition, Matrix4x4.Identity);
|
|
|
|
Assert.Equal(1, fx.AlphaQueue.PendingCount);
|
|
Assert.Equal(1, fx.AlphaQueue.AlphaCount);
|
|
Assert.Equal(0, fx.AlphaQueue.ClipCount);
|
|
|
|
FieldInfo alphaListField = typeof(RetailAlphaQueue).GetField(
|
|
"_alpha", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
|
var alphaList = (List<RetailAlphaEntry>)alphaListField.GetValue(fx.AlphaQueue)!;
|
|
RetailAlphaEntry entry = Assert.Single(alphaList);
|
|
Assert.Equal(0, entry.Token);
|
|
|
|
fx.AlphaQueue.AbortFrame();
|
|
}
|
|
|
|
[Fact]
|
|
public void SubmitWalkAlphaInstance_RejectsAMismatchedViewProjectionInTheSameScope()
|
|
{
|
|
using var fx = new DispatcherFixture(withAlphaQueue: true);
|
|
fx.AlphaQueue!.BeginFrame();
|
|
|
|
var key = new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.AlphaBlend, FoliageFlags: 0);
|
|
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
|
|
key, Matrix4x4.Identity, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f,
|
|
Vector2.Zero, 0, IsOpaque: false, LocalSortCenter: new Vector3(0, 0, 10));
|
|
|
|
fx.Dispatcher.SubmitWalkAlphaInstance(in batch, Vector3.Zero, Matrix4x4.Identity);
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
fx.Dispatcher.SubmitWalkAlphaInstance(
|
|
in batch, Vector3.Zero, Matrix4x4.CreateTranslation(1, 0, 0)));
|
|
|
|
fx.AlphaQueue.AbortFrame();
|
|
}
|
|
|
|
// ── Deliverable 3: the ordered submitter's own cull scratch ────────────
|
|
|
|
[Fact]
|
|
public void PrepareThenDrawOrderedStream_DoesNotReadOrCorruptTheSharedAlphaCullScratch()
|
|
{
|
|
using var fx = new DispatcherFixture();
|
|
using DrawScope draw = fx.BeginDraw();
|
|
|
|
// Poison the SHARED _drawCullModes scratch the alpha path owns — the
|
|
// exact array the ordered submitter used to write into before FW3.2a.
|
|
// Under the OLD shared-scratch behavior this test's second assertion
|
|
// fails: the ordered path's own command overwrites index 0 with
|
|
// its own cull mode (Clockwise), destroying the alpha path's poison.
|
|
FieldInfo field = typeof(WbDrawDispatcher).GetField(
|
|
"_drawCullModes", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
|
var poisonModes = (CullMode[])field.GetValue(fx.Dispatcher)!;
|
|
poisonModes[0] = CullMode.None;
|
|
|
|
var stream = new OrderedDrawStream();
|
|
stream.Append(new OrderedDrawCommand(
|
|
new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.Opaque, FoliageFlags: 0, CullMode: CullMode.Clockwise),
|
|
Matrix4x4.Identity, WalkDrawStage.Terrain, 0, 0,
|
|
WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0));
|
|
|
|
fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity);
|
|
fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, stream.Count);
|
|
|
|
// (1) The ordered submission's OWN recorded cull call reflects the
|
|
// STREAM's cull mode (Clockwise -> GpuCullMode.Front), not the
|
|
// poisoned shared array's (None).
|
|
List<GpuCullMode> cullCalls = [.. fx.Device.Calls.OfType<GpuRecordedCullMode>().Select(c => c.CullMode)];
|
|
Assert.Equal([GpuCullMode.Front], cullCalls);
|
|
|
|
// (2) The shared _drawCullModes scratch is UNTOUCHED — the ordered
|
|
// path never wrote through it.
|
|
var afterModes = (CullMode[])field.GetValue(fx.Dispatcher)!;
|
|
Assert.Equal(CullMode.None, afterModes[0]);
|
|
}
|
|
|
|
// ── Fixture ─────────────────────────────────────────────────────────────
|
|
|
|
private readonly struct DrawScope : IDisposable
|
|
{
|
|
private readonly IDisposable _publication;
|
|
private readonly IGpuPassEncoder _pass;
|
|
|
|
public DrawScope(IGpuFrame frame, IGpuPassEncoder pass, IDisposable publication)
|
|
{
|
|
Frame = frame;
|
|
_pass = pass;
|
|
_publication = publication;
|
|
}
|
|
|
|
public IGpuFrame Frame { get; }
|
|
|
|
public IGpuPassEncoder Pass => _pass;
|
|
|
|
public void Dispose()
|
|
{
|
|
_publication.Dispose();
|
|
_pass.Dispose();
|
|
}
|
|
}
|
|
|
|
private sealed class DispatcherFixture : IDisposable
|
|
{
|
|
private readonly WbMeshAdapter _meshAdapter;
|
|
private readonly TextureCache _textures;
|
|
|
|
public DispatcherFixture(
|
|
bool withAlphaQueue = false,
|
|
IRetailSelectionRenderSink? selectionSink = null)
|
|
{
|
|
Device = new RecordingGpuDevice();
|
|
FrameLifetime = new GpuDeviceFrameLifetime(Device);
|
|
Scope = new VulkanWorldPassScope(sampleCount: 1);
|
|
_textures = new TextureCache(Device, new NoopDatReaderWriter());
|
|
_meshAdapter = new WbMeshAdapter(
|
|
Device,
|
|
new NoopDatReaderWriter(),
|
|
new NullPreparedAssetSource(),
|
|
NullLogger<WbMeshAdapter>.Instance,
|
|
Device.Retirement);
|
|
var entitySpawnAdapter = new EntitySpawnAdapter(
|
|
_textures,
|
|
_ => throw new NotSupportedException("Not exercised by these tests."));
|
|
AlphaQueue = withAlphaQueue ? new RetailAlphaQueue() : null;
|
|
|
|
Dispatcher = new WbDrawDispatcher(
|
|
Device,
|
|
FrameLifetime,
|
|
Scope,
|
|
_textures,
|
|
_meshAdapter,
|
|
entitySpawnAdapter,
|
|
new EntityClassificationCache(),
|
|
new AcDream.Core.Rendering.TranslucencyFadeManager(),
|
|
selectionSink: selectionSink,
|
|
alphaQueue: AlphaQueue);
|
|
}
|
|
|
|
public RecordingGpuDevice Device { get; }
|
|
|
|
public GpuDeviceFrameLifetime FrameLifetime { get; }
|
|
|
|
public VulkanWorldPassScope Scope { get; }
|
|
|
|
public WbDrawDispatcher Dispatcher { get; }
|
|
|
|
public RetailAlphaQueue? AlphaQueue { get; }
|
|
|
|
public ObjectMeshManager Manager => _meshAdapter.MeshManager!;
|
|
|
|
public DrawScope BeginDraw()
|
|
{
|
|
FrameLifetime.BeginFrame();
|
|
IGpuFrame frame = FrameLifetime.CurrentFrame!;
|
|
IGpuPassEncoder pass = frame.BeginPass(
|
|
GpuPassDescription.BackbufferClear(
|
|
"fw3-2a-walk-populator-test", Vector4.Zero, sampleCount: 1));
|
|
IDisposable publication = Scope.Publish(pass);
|
|
Device.Clear();
|
|
return new DrawScope(frame, pass, publication);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispatcher.Dispose();
|
|
_meshAdapter.Dispose();
|
|
_textures.Dispose();
|
|
Device.Dispose();
|
|
}
|
|
}
|
|
|
|
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
|
{
|
|
public PreparedAssetSourceStats Stats => default;
|
|
|
|
public CacheStats DecodedTextureCacheStats => default;
|
|
|
|
public PreparedAssetPresence Probe(
|
|
AcDream.Content.Pak.PakAssetType type,
|
|
uint sourceFileId) =>
|
|
PreparedAssetPresence.Missing;
|
|
|
|
public PreparedAssetReadResult Read(
|
|
in PreparedAssetRequest request,
|
|
CancellationToken cancellationToken = default) =>
|
|
PreparedAssetReadResult.Missing;
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class NoopDatReaderWriter : IDatReaderWriter
|
|
{
|
|
private readonly StubDatabase _portal = new();
|
|
private readonly StubDatabase _highRes = new();
|
|
private readonly StubDatabase _language = new();
|
|
private readonly StubDatabase _cell = new();
|
|
|
|
public string SourceDirectory => string.Empty;
|
|
|
|
public IDatDatabase Portal => _portal;
|
|
|
|
public IDatDatabase Cell => _cell;
|
|
|
|
public ReadOnlyDictionary<uint, IDatDatabase> CellRegions { get; } =
|
|
new(new Dictionary<uint, IDatDatabase>());
|
|
|
|
public IDatDatabase HighRes => _highRes;
|
|
|
|
public IDatDatabase Language => _language;
|
|
|
|
public IDatDatabase Local => _language;
|
|
|
|
public ReadOnlyDictionary<uint, uint> RegionFileMap { get; } =
|
|
new(new Dictionary<uint, uint>());
|
|
|
|
public int PortalIteration => 0;
|
|
|
|
public int CellIteration => 0;
|
|
|
|
public int HighResIteration => 0;
|
|
|
|
public int LanguageIteration => 0;
|
|
|
|
public bool TryGetFileBytes(
|
|
uint regionId,
|
|
uint fileId,
|
|
ref byte[] bytes,
|
|
out int bytesRead)
|
|
{
|
|
bytesRead = 0;
|
|
return false;
|
|
}
|
|
|
|
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj =>
|
|
Array.Empty<uint>();
|
|
|
|
public IEnumerable<IDatReaderWriter.IdResolution> ResolveId(uint id) =>
|
|
Array.Empty<IDatReaderWriter.IdResolution>();
|
|
|
|
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
|
|
throw new NotSupportedException();
|
|
|
|
public bool TrySave<T>(
|
|
uint regionId,
|
|
T obj,
|
|
int iteration = 0) where T : IDBObj =>
|
|
throw new NotSupportedException();
|
|
|
|
[return: MaybeNull]
|
|
public T Get<T>(uint fileId) where T : IDBObj => default;
|
|
|
|
public bool TryGet<T>(
|
|
uint fileId,
|
|
[MaybeNullWhen(false)] out T value) where T : IDBObj
|
|
{
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
|
|
private sealed class StubDatabase : IDatDatabase
|
|
{
|
|
public DatDatabase Db => throw new NotSupportedException();
|
|
|
|
public int Iteration => 0;
|
|
|
|
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj =>
|
|
Array.Empty<uint>();
|
|
|
|
public bool TryGet<T>(
|
|
uint fileId,
|
|
[MaybeNullWhen(false)] out T value) where T : IDBObj
|
|
{
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
public bool TryGetFileBytes(
|
|
uint fileId,
|
|
[MaybeNullWhen(false)] out byte[] value)
|
|
{
|
|
value = null;
|
|
return false;
|
|
}
|
|
|
|
public bool TryGetFileBytes(
|
|
uint fileId,
|
|
ref byte[] bytes,
|
|
out int bytesRead)
|
|
{
|
|
bytesRead = 0;
|
|
return false;
|
|
}
|
|
|
|
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
|
|
throw new NotSupportedException();
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|