fix(render): restore retail per-cell alpha order
Reconstruct one combined static/dynamic object-part stream for each ordinary outdoor or interior cell, compute authored SortCenter CYpt keys, and stable-sort far to near before projecting opaque and delayed subsets. Prepare real cell-particle records at the leaf, preserve every S4-c2 router outcome, and merge object and particle delayed records by retained key before either source appends to the unchanged CLIP/ALPHA FIFO lists. Cell turns remain cell-major; equal cross-source ties are deterministically object-first. File AP-241 and AP-242 for the remaining separate opaque/row-5 channels and unrepresented equal-key common ordinal. File AP-243 for the paired-binary correction: retail shares the cell CYpt/heading beyond 50 m while this bounded port always uses the more exact authored per-part center. Pin 162 active AP rows and correct world-alpha and AlphaFlushCounts prose. Lead-approved scope clarification: RetailPViewPassExecutor.WalkLeaf.cs and RetailPViewPassExecutor.cs are the minimum existing production leaf adapter and thin particle-prepare forwarder omitted by the literal Walk/Wb file list. They contain no router, queue, mask, state, depth, or flush behavior; relocating them would create an artificial seam. Gates: Release solution build 0W/0E; shader/manifest 32/32; focused production 210/210; real allocation 3/3 at 0 B; one-shot hermetic 16743/0/0 across 14 assemblies; InstalledDat 385 pass/10 documented fail/1 skip with all six AlphaFlushSites passing; git diff --check PASS. Initial no-restore solution build failed NETSDK1004 for 42 missing scratch assets; one solution restore preceded the official build. Mutation proof, each restored before final gates: 1. Reverse comparator: authored-center order expected [202,101], actual [101,202]. 2. Move ties left: multipart/subset order expected [11,12,21,22], actual [22,21,12,11]. 3. Restore static/dynamic blocks: expected [2,3,1], actual [3,1,2]. 4. Use entity origin: authored-center order expected [202,101], actual [101,202]. 5. Restore particle tail: expected [Wb,Particle,Wb,Particle], actual [Wb,Wb,Particle,Particle]. 6. Scope-global sort: first cell model X expected 5, actual 50. 7. Restore dead camera parameter: SubmitWalkAlphaInstance parameter count expected 2, actual 3. 8. Restore stale global-queue prose: exact Assert.DoesNotContain failure on distance-sorts one shared queue. 9. Remove AP-241 identity: Assert.Single found no matching row. 10. Allocate in real merge: expected 0 B, actual 3072 B. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
06b986622b
commit
a86ec73ece
17 changed files with 1067 additions and 197 deletions
|
|
@ -1135,8 +1135,11 @@ explicit Campaign OVERHAUL divergences, not accepted retail contracts:
|
|||
retail's `DrawLandCell` then `DrawSortCell` interleave for each land cell;
|
||||
- depth clear/seal events are emitted for zero-outside-view roots and do not
|
||||
consume retail's persistent previous-call `uint16 portalsDrawnCount` gate;
|
||||
- `RetailAlphaQueue` distance-sorts one shared queue, whereas retail owns two
|
||||
FIFO mesh lists and drains clip before alpha at exact barriers.
|
||||
- ordinary object and particle parts now receive retail's stable per-cell
|
||||
authored-CYpt order before entering the two FIFO mesh lists, which drain
|
||||
CLIP before ALPHA at exact barriers. The retained differences are AP-241
|
||||
(opaque/row-5 GPU channels), AP-242 (equal cross-source tie ordinal), and
|
||||
AP-243 (retail's >50 m shared-cell-key optimization).
|
||||
|
||||
The exact target and evidence are recorded in
|
||||
`docs/research/2026-09-01-overhaul/oh1-retail-world-contract.md`; OH5, OH6, and
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -400,11 +400,14 @@ renderers classify translucent mesh batches correctly, but they have no live
|
|||
retail `CPartCell`/`CShadowPart` list and render particles in a separate
|
||||
batcher. `src/AcDream.App/Rendering/RetailAlphaQueue.cs` is therefore an
|
||||
acdream-owned runtime seam above the extracted mesh pipeline. During the main
|
||||
world frame, `WbDrawDispatcher` and `ParticleRenderer` submit transparent
|
||||
GfxObj subsets and scene particles into one stable far-to-near stream keyed by
|
||||
the transformed DAT `SortCenter`; only adjacent compatible entries may batch.
|
||||
world frame, `WalkFrameDriver` reconstructs retail's stable far-to-near order
|
||||
inside each object-cell turn from transformed authored DAT `SortCenter`, merges
|
||||
the cell's `WbDrawDispatcher` GfxObj subsets with prepared `ParticleRenderer`
|
||||
records, then appends the result to retail's two FIFO lists (CLIP/ALPHA).
|
||||
It never performs one scope-global distance sort; only adjacent compatible
|
||||
entries may batch at drain.
|
||||
Billboard particle textures are resident bindless `sampler2DArray` handles in
|
||||
the per-instance vertex ABI, so different textures preserve that sorted order
|
||||
the per-instance vertex ABI, so different textures preserve that per-cell order
|
||||
inside one instanced draw; only a DAT blend-mode boundary splits the run. This
|
||||
keeps dense particle fields from becoming one Vulkan draw per alternating
|
||||
texture. `WalkFrameDriver` drains the landscape and world scopes at the exact
|
||||
|
|
|
|||
|
|
@ -1349,72 +1349,86 @@ register count, allowed files, and every gate claim. A failed lens gets one
|
|||
bounded fix contract; a third fix round stops and is written up. Dual PASS
|
||||
lands c3a; the lead then writes c3b's deletion-only contract from that code.
|
||||
|
||||
## 19. S4-c3a stop record — owner attempts 3–5 exhausted (2026-09-04)
|
||||
### 18.7 Implementation result (scratch implementation, 2026-09-04)
|
||||
|
||||
**STOPPED UNLANDED.** The clean scratch worktree
|
||||
`C:\Users\erikn\source\repos\acdream\.claude\worktrees\s4-c3a-impl`
|
||||
ends at `359061b8299dd77a2335935868b739b8295b9b0c`. Nothing from c3a was
|
||||
cherry-picked to the campaign branch; the campaign renderer remains the
|
||||
reviewed S4-c2 stack through `252886e84`, with this §18 contract at
|
||||
`5c9da6fc2`. c3b is locked and G3/G4 remain unpassed.
|
||||
The bounded c3a implementation now builds one combined registry view for each
|
||||
ordinary outdoor/interior object-cell turn, transforms each GfxObj's authored
|
||||
`SortCenter`, applies a stable descending per-cell insertion sort, and derives
|
||||
both opaque/cutout commands and delayed object records from that same order.
|
||||
The real particle leaf prepares but does not expose delayed records; the cell
|
||||
event merges their retained keys with the object's range before either source
|
||||
appends to the unchanged CLIP/ALPHA FIFO lists. Row-5 immediate routing,
|
||||
capacity-drop/source rollback, barriers, and building-shell ownership are
|
||||
unchanged. Exact equal object/particle keys use the disclosed object-first tie.
|
||||
|
||||
The complete unlanded scratch stack is:
|
||||
The paired-binary correction is recorded as AP-243: retail's
|
||||
`UpdateObjCell` greater-than-50 m branch at `0x005A06B7..0x005A0720` gives
|
||||
all parts one cell CYpt/heading through `CPhysicsObj::UpdateViewerDistance`
|
||||
and therefore retains insertion order, while this bounded port always uses
|
||||
the more exact per-part authored center. AP-241/AP-242 record the two other
|
||||
narrow residuals. The active AP header and physical table are pinned at 162
|
||||
rows, with exactly one row for each AP-241/AP-242/AP-243.
|
||||
|
||||
1. `44e2bc227b` — initial c3a implementation;
|
||||
2. `b6bf6c131` — lead fix-round-1 contract;
|
||||
3. `eea5793d2` — fix round 1;
|
||||
4. `14397b14c` — lead fix-round-2 contract, explicitly the last round;
|
||||
5. `359061b82` — fix round 2.
|
||||
Lead-approved process-scope clarification: the real-path integration also
|
||||
changes exactly `src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs`
|
||||
and `src/AcDream.App/Rendering/RetailPViewPassExecutor.cs`. The literal
|
||||
`{Walk,Wb}/` production-file list omitted the existing leaf adapter and the
|
||||
thin `PrepareCellParticleAlpha` forwarder required to reach the expressly
|
||||
allowed `ParticleRenderer`; relocating that bridge would create an artificial
|
||||
seam. These two files contain only the interface adaptation and forwarder—no
|
||||
router, queue, mask, pipeline, reference, depth, or flush behavior. The lead
|
||||
approved this minimum real-path reachability delta before commit. It is a
|
||||
contract-scope clarification, not a retail behavior deviation, so it files no
|
||||
additional AP row.
|
||||
|
||||
Attempt 3 failed the retail lens because billboard/mode-2–5 particles keyed
|
||||
CYpt from their rendered/AABB center instead of the GfxObj-authored
|
||||
`sort_center`; the same lens found one stale static-record-order comment.
|
||||
Fix round 1 separated authored and visual centers, applied particle scale and
|
||||
orientation to the authored point, made prepared payload reservation lazy,
|
||||
rolled back false/exception append exactly once, bounded preparation to 3,000
|
||||
CLIP plus 3,000 ALPHA candidates, and corrected the prose.
|
||||
Clean-state return matrix (no graphical client was launched):
|
||||
|
||||
Attempt 4 failed the retail re-review on one universal edge: a successfully
|
||||
decoded real GfxObj with no Surface DID was replaced by
|
||||
`ParticleGfxInfo.Default`, discarding its authored center. The installed scan
|
||||
found zero practical exposure (2,051 emitter records / 360 referenced GfxObjs,
|
||||
all with a nonzero first Surface DID), but retail's CYpt read is surface-
|
||||
independent. Fix round 2 preserved the decoded record and made only texture
|
||||
acquisition conditional; it also corrected the synthetic-fixture evidence
|
||||
wording.
|
||||
- Initial solution `--no-restore` build was invalid setup evidence: 42
|
||||
projects lacked `project.assets.json` (`NETSDK1004`). One solution restore
|
||||
followed; the official Release solution build then succeeded with 0
|
||||
warnings and 0 errors.
|
||||
- Shader descriptor/manifest/SPIR-V validation: 32/32. No shader source,
|
||||
checked-in SPIR-V, manifest, Content model, serializer, or package format
|
||||
changed.
|
||||
- Focused queue/router/particle/Wb/walk/EnvCell/PView production filter:
|
||||
210/210. The three real warmed routes (EnvCell whole-leaf SubmitRhi,
|
||||
immediate mesh particle RHI, and the new driver/particle/dispatcher merge)
|
||||
pass 3/3 and each asserts 0 managed bytes.
|
||||
- Official one-shot 14-assembly hermetic gate: 16,743 passed / 0 skipped /
|
||||
0 failed. Evidence:
|
||||
`%TEMP%/acdream-s4-c3a-impl-20260904-2015`.
|
||||
- InstalledDat with explicit `C:\Turbine\Asheron's Call`: 385 passed / 10
|
||||
failed / 1 skipped. The failures are exactly TowerAscent, the two #383
|
||||
layout sweeps, #458 `Oh_doorway_still_first_frame_diff`, and the six
|
||||
retained `AlphaFlushCounts_*`; all six paired `AlphaFlushSites_*` pass.
|
||||
Evidence: `%TEMP%/acdream-s4-c3a-installed-20260904-2017`.
|
||||
- `git diff --check`: pass.
|
||||
|
||||
Attempt 5's final retail lens **PASSED**. It reconfirmed the paired executable
|
||||
and PDB, `CPhysicsPart::UpdateViewerDistance @0x0050E030`, stable descending
|
||||
per-cell order, combined static/dynamic membership, the real object/particle
|
||||
merge, building separation, unchanged two-list FIFO/router/row-5 behavior,
|
||||
and AP-241/AP-242/AP-243 truth/count in the scratch stack. Its focused
|
||||
reproduction passed 117/117.
|
||||
Observed mutation first failures (every mutation was reversed with a patch
|
||||
before the return matrix):
|
||||
|
||||
The final production/gate-honesty lens found no production, lifecycle,
|
||||
allocation, scope, register, or retained-memory defect. It nevertheless
|
||||
returned **FAIL** on one mandatory evidence error. The initial candidate's
|
||||
mutation ledger says renaming AP-241 first fails `Assert.Single`. The unchanged
|
||||
pin actually computes `rowIndex == -1` and first fails the earlier AP-section
|
||||
boundary `Assert.True` in `WalkStaticStreamPopulatorTests.cs`; `Assert.Single`
|
||||
is reached later. The other eighteen mutation claims match their actual first
|
||||
assertions. Correcting that sentence would be a third c3a fix round, forbidden
|
||||
by plan §5, packet §18.6, and the owner's explicit attempt limit. Therefore no
|
||||
evidence-only correction, landing, graphical gate, or c3b contract follows.
|
||||
|
||||
Final read-only evidence on `359061b82`:
|
||||
|
||||
- Release solution: 0 warnings / 0 errors;
|
||||
- focused §19 lane: 95/95; no-surface target: 1/1; final retail superset:
|
||||
117/117;
|
||||
- AP boundary/count pin: 1/1; physical and unique AP counts: 162;
|
||||
- real allocation pins: 2/2 at 0 managed bytes;
|
||||
- shader/manifest/SPIR-V: 32/32;
|
||||
- one-shot hermetic artifact: 16,751 passed / 0 skipped / 0 failed at
|
||||
`%TEMP%\acdream-s4-c3a-fix1-20260904-2215`;
|
||||
- inclusive InstalledDat artifact: 385 passed / 10 documented failures / 1
|
||||
skip at `%TEMP%\acdream-s4-c3a-fix1-installed-inclusive-20260904-2222`;
|
||||
- `git diff --check`: PASS; scratch HEAD clean; no graphical client launched.
|
||||
|
||||
Resume only after an explicit owner process change. The minimum mechanical
|
||||
correction is known, but the stop rule exists precisely to prevent a green
|
||||
renderer from bypassing a failed evidence contract.
|
||||
1. Reverse the CYpt comparator: `PopulateCellObjects_UsesAuthoredSortCenterAndStableFarToNearOrder`
|
||||
expected `[202,101]`, actual `[101,202]` at the first sequence assertion.
|
||||
2. Move equal keys left (`>=`): `PopulateCellObjects_EqualCyptRetainsEntityPartAndSubsetOrder`
|
||||
expected `[11,12,21,22]`, actual `[22,21,12,11]`.
|
||||
3. Restore static/dynamic blocks: `PopulateCellObjects_InterleavesStaticAndDynamicOpaquePartsByCypt`
|
||||
expected `[2,3,1]`, actual `[3,1,2]`.
|
||||
4. Replace authored SortCenter with entity translation: the authored-center
|
||||
test first expected `[202,101]`, actual `[101,202]`.
|
||||
5. Restore the old object-then-particle tail: `CellTurn_RealParticlePreparationMergesWithObjectAlphaByCypt`
|
||||
expected source order `[Wb,Particle,Wb,Particle]`, actual
|
||||
`[Wb,Wb,Particle,Particle]` at position 1.
|
||||
6. Sort all staged objects scope-globally before replay:
|
||||
`SeparateCellTurnsRemainCellMajorWhenLaterCellIsFarther` first expected
|
||||
model X `5`, actual `50`.
|
||||
7. Restore a dead optional camera parameter on `SubmitWalkAlphaInstance`:
|
||||
`WorldAlphaCyptContract_RetainsTheKeyWithoutDeadCameraSubmitThreading`
|
||||
first expected parameter count 2, actual 3.
|
||||
8. Reinsert “distance-sorts one shared queue” into the inventory:
|
||||
`WorldAlphaCyptDocumentationAndRegister_PinPerCellTruthAndThreeResiduals`
|
||||
first failed `Assert.DoesNotContain` on that exact phrase.
|
||||
9. Rename AP-241 away: that same truth pin first failed `Assert.Single`
|
||||
because no AP-241 row matched.
|
||||
10. Allocate one object in the real cell-merge path:
|
||||
`ProductionCellObjectParticleMerge_WarmedPathAllocatesZeroBytes` first
|
||||
expected 0, actual 3,072 managed bytes.
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ namespace AcDream.App.Rendering;
|
|||
/// <summary>
|
||||
/// Instanced renderer for retail particle emitters. Scene particles submit to
|
||||
/// <see cref="RetailAlphaQueue"/> while a world frame is active so their
|
||||
/// compositing order is shared with ordinary translucent GfxObj parts. Sky and
|
||||
/// sealed off-screen passes retain their independent immediate path.
|
||||
/// compositing order is merged by authored CYpt with ordinary translucent
|
||||
/// GfxObj parts at each owning cell turn before either source enters the two
|
||||
/// FIFO lists. Sky and sealed off-screen passes retain their independent
|
||||
/// immediate path.
|
||||
/// </summary>
|
||||
public sealed unsafe partial class ParticleRenderer : IDisposable
|
||||
{
|
||||
|
|
@ -163,6 +165,7 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
private readonly List<MeshParticleDraw> _meshDrawListScratch = new(64);
|
||||
private readonly List<MeshParticleInstance> _meshRunScratch = new(64);
|
||||
private readonly List<ParticleSubmission> _submissionScratch = new(128);
|
||||
private readonly List<PreparedParticleAlphaSubmission> _preparedCellAlphaScratch = new(128);
|
||||
private readonly List<RuntimeParticleEmitter> _scopedEmitterScratch = new(64);
|
||||
private readonly List<DeferredParticleDraw> _deferredAlpha = new(128);
|
||||
private DeferredParticleDraw[] _preparedAlpha = new DeferredParticleDraw[256];
|
||||
|
|
@ -315,6 +318,105 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
FinishDraw(camera, renderPass);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S4-c3a production cell-shadow seam. Builds the same real billboard and
|
||||
/// mesh records as <see cref="DrawForCell"/>, orders particle parts by
|
||||
/// their authored CYpt key, executes row-5 immediate draws at this exact
|
||||
/// cell turn, and returns only delayed records without appending them.
|
||||
/// The walk driver merges those retained keys with ordinary object parts
|
||||
/// before calling <see cref="PreparedParticleAlphaSubmission.Append"/>.
|
||||
/// </summary>
|
||||
internal ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareForCellAlpha(
|
||||
ICamera camera,
|
||||
Vector3 cameraWorldPos,
|
||||
ParticleRenderPass renderPass,
|
||||
uint cellId,
|
||||
uint clipSlot = 0)
|
||||
{
|
||||
_preparedCellAlphaScratch.Clear();
|
||||
if (camera is null)
|
||||
return CollectionsMarshal.AsSpan(_preparedCellAlphaScratch);
|
||||
|
||||
_particles.CopyRenderableEmittersInCell(renderPass, cellId, _scopedEmitterScratch);
|
||||
if (_scopedEmitterScratch.Count == 0)
|
||||
return CollectionsMarshal.AsSpan(_preparedCellAlphaScratch);
|
||||
|
||||
Matrix4x4.Invert(camera.View, out Matrix4x4 invView);
|
||||
Vector3 cameraRight = Vector3.Normalize(new Vector3(invView.M11, invView.M12, invView.M13));
|
||||
Vector3 cameraUp = Vector3.Normalize(new Vector3(invView.M21, invView.M22, invView.M23));
|
||||
BuildDrawLists(
|
||||
cameraWorldPos,
|
||||
renderPass,
|
||||
cameraRight,
|
||||
cameraUp,
|
||||
emitterFilter: null,
|
||||
_scopedEmitterScratch,
|
||||
clipSlot);
|
||||
|
||||
if (_submissionScratch.Count == 0)
|
||||
return CollectionsMarshal.AsSpan(_preparedCellAlphaScratch);
|
||||
|
||||
bool defers = renderPass == ParticleRenderPass.Scene && _alphaQueue?.IsCollecting == true;
|
||||
if (!defers)
|
||||
{
|
||||
DrawOrdered(camera);
|
||||
return CollectionsMarshal.AsSpan(_preparedCellAlphaScratch);
|
||||
}
|
||||
|
||||
ParticleSubmissionOrdering.Sort(_submissionScratch);
|
||||
Matrix4x4 viewProjection = camera.View * camera.Projection;
|
||||
RetailAlphaQueue queue = _alphaQueue!;
|
||||
for (int i = 0; i < _submissionScratch.Count; i++)
|
||||
{
|
||||
ParticleSubmission submission = _submissionScratch[i];
|
||||
TranslucencyKind translucency = submission.Kind == ParticleSubmissionKind.Mesh
|
||||
? _meshDrawListScratch[submission.DrawIndex].Batch.Translucency
|
||||
: default;
|
||||
uint colorArgb = submission.Kind == ParticleSubmissionKind.Mesh
|
||||
? _meshDrawListScratch[submission.DrawIndex].Instance.ColorArgb
|
||||
: default;
|
||||
RetailAlphaMeshDecision decision = RouteParticleSubmission(
|
||||
submission.Kind, translucency, colorArgb);
|
||||
|
||||
if (decision.Action is RetailAlphaMeshAction.Append
|
||||
or RetailAlphaMeshAction.AppendClipAndImmediate)
|
||||
{
|
||||
_dispatchDeferredParticle = submission.Kind == ParticleSubmissionKind.Billboard
|
||||
? new DeferredParticleDraw(
|
||||
submission.Kind,
|
||||
_drawListScratch[submission.DrawIndex],
|
||||
default,
|
||||
viewProjection)
|
||||
: new DeferredParticleDraw(
|
||||
submission.Kind,
|
||||
default,
|
||||
_meshDrawListScratch[submission.DrawIndex],
|
||||
viewProjection);
|
||||
int token = ReserveDispatchDeferredParticle();
|
||||
_preparedCellAlphaScratch.Add(new PreparedParticleAlphaSubmission(
|
||||
queue,
|
||||
decision.List,
|
||||
_alphaSource,
|
||||
token,
|
||||
decision.OverrideClipmap,
|
||||
submission.DistanceSq,
|
||||
submission.Sequence));
|
||||
}
|
||||
|
||||
if (decision.Action is RetailAlphaMeshAction.Immediate
|
||||
or RetailAlphaMeshAction.AppendClipAndImmediate)
|
||||
{
|
||||
_drawImmediateParticle(
|
||||
viewProjection,
|
||||
submission.Kind,
|
||||
submission.DrawIndex,
|
||||
opaqueDepthState: decision.Action == RetailAlphaMeshAction.Immediate);
|
||||
}
|
||||
}
|
||||
|
||||
return CollectionsMarshal.AsSpan(_preparedCellAlphaScratch);
|
||||
}
|
||||
|
||||
private void FinishDraw(ICamera camera, ParticleRenderPass renderPass)
|
||||
{
|
||||
if (_submissionScratch.Count == 0)
|
||||
|
|
@ -738,11 +840,10 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
Matrix4x4 model = Matrix4x4.CreateScale(particle.Size)
|
||||
* Matrix4x4.CreateFromQuaternion(orientation)
|
||||
* Matrix4x4.CreateTranslation(particle.Position);
|
||||
// S4-c2: RetailAlphaOrdering.ComputeViewerDistance is deleted (the
|
||||
// alpha queue is FIFO now), but this distanceSq is still needed by
|
||||
// the UNRELATED non-deferred immediate-draw path's own local sort
|
||||
// (ParticleSubmissionOrdering.Sort, ParticleRenderer.Rhi.cs) —
|
||||
// inlined here rather than resurrecting the deleted helper.
|
||||
// The queue remains FIFO. S4-c3a retains this authored part-center
|
||||
// distance for the owning cell's object/particle pre-append merge;
|
||||
// the unrelated non-deferred immediate path uses the same key for its
|
||||
// own local ParticleSubmissionOrdering.Sort.
|
||||
Vector3 worldSortCenter = Vector3.Transform(renderData.SortCenter, model);
|
||||
float distanceSq = Vector3.DistanceSquared(worldSortCenter, cameraWorldPosition);
|
||||
var instance = new MeshParticleInstance(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,27 @@ internal readonly record struct ParticleSubmission(
|
|||
float DistanceSq,
|
||||
int Sequence);
|
||||
|
||||
/// <summary>
|
||||
/// One scene-particle alpha record prepared at its owning cell turn but not
|
||||
/// yet appended. S4-c3a lets <c>WalkFrameDriver</c> merge this retained CYpt
|
||||
/// key with ordinary object parts before either source enters retail's FIFO
|
||||
/// list. The source payload is reserved during preparation; <see
|
||||
/// cref="RetailAlphaQueue.TryAppend"/> remains the sole visibility edge and
|
||||
/// still owns capacity-drop cleanup by registering the source before a drop.
|
||||
/// </summary>
|
||||
internal readonly record struct PreparedParticleAlphaSubmission(
|
||||
RetailAlphaQueue Queue,
|
||||
RetailAlphaList List,
|
||||
IRetailAlphaDrawSource Source,
|
||||
int Token,
|
||||
bool OverrideClipmap,
|
||||
float DistanceSq,
|
||||
int Sequence)
|
||||
{
|
||||
internal void Append() =>
|
||||
Queue.TryAppend(List, Source, Token, OverrideClipmap);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared ordering for the two retail particle geometry paths. Transparent
|
||||
/// particles are submitted back-to-front; creation/enumeration order is the
|
||||
|
|
|
|||
|
|
@ -89,9 +89,9 @@ internal sealed partial class RetailPViewPassExecutor
|
|||
|
||||
/// <summary>S3 chunk 3 fix round 1 (F2): the production
|
||||
/// <c>IWalkFrameLeafRenderer.HasRenderableEmittersInCell</c> wiring —
|
||||
/// both <see cref="DrawLandscapeStaticParticles"/> and
|
||||
/// <see cref="DrawCellParticles"/> draw through
|
||||
/// <c>ParticleRenderPass.Scene</c>, so this asks that SAME pass.</summary>
|
||||
/// both walk particle-prepare leaves use
|
||||
/// <c>ParticleRenderPass.Scene</c>, so this asks that SAME pass before
|
||||
/// either leaf constructs its real delayed records.</summary>
|
||||
internal bool HasWalkRenderableEmittersInCell(uint cellId) =>
|
||||
_particles?.HasRenderableEmittersInCell(ParticleRenderPass.Scene, cellId) ?? false;
|
||||
|
||||
|
|
@ -271,11 +271,11 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
|||
|
||||
public void ClearInteriorDepth() => _clearInteriorDepth();
|
||||
|
||||
public void DrawStaticParticles(uint cellId) =>
|
||||
_passes.DrawLandscapeStaticParticles(_frame, cellId);
|
||||
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareStaticParticles(uint cellId) =>
|
||||
_passes.PrepareCellParticleAlpha(_frame, cellId);
|
||||
|
||||
public void DrawCellParticles(uint cellId) =>
|
||||
_passes.DrawCellParticles(_frame, cellId);
|
||||
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareCellParticles(uint cellId) =>
|
||||
_passes.PrepareCellParticleAlpha(_frame, cellId);
|
||||
|
||||
public int DrawExitSeals() => _drawExitSeals();
|
||||
|
||||
|
|
|
|||
|
|
@ -555,6 +555,21 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
}
|
||||
}
|
||||
|
||||
internal ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareCellParticleAlpha(
|
||||
RetailPViewFrameInput frame,
|
||||
uint cellId)
|
||||
{
|
||||
if (_particles is null || _particleRenderer is null)
|
||||
return ReadOnlySpan<PreparedParticleAlphaSubmission>.Empty;
|
||||
|
||||
return _particleRenderer.PrepareForCellAlpha(
|
||||
frame.Camera,
|
||||
frame.CameraWorldPosition,
|
||||
ParticleRenderPass.Scene,
|
||||
cellId,
|
||||
clipSlot: 0);
|
||||
}
|
||||
|
||||
public void ClearInteriorDepth()
|
||||
{
|
||||
_surface.ClearInteriorDepth();
|
||||
|
|
|
|||
|
|
@ -42,6 +42,15 @@ internal readonly record struct WalkFrameStaticRecords(
|
|||
/// </summary>
|
||||
internal interface IWalkFrameWorldData
|
||||
{
|
||||
/// <summary>The exact combined ordinary-object membership for one indoor
|
||||
/// cell, preserving the registry's single static/dynamic insertion
|
||||
/// sequence for S4-c3a's per-part stable CYpt sort.</summary>
|
||||
WalkFrameStaticRecords GetCellObjects(uint cellId);
|
||||
|
||||
/// <summary>The exact combined ordinary-object membership for one
|
||||
/// landscape cell; see <see cref="GetCellObjects"/>.</summary>
|
||||
WalkFrameStaticRecords GetOutdoorObjects(uint cellId);
|
||||
|
||||
/// <summary>An indoor <c>PView::DrawCells</c> flood cell's (or a building
|
||||
/// look-in's) static content — <c>RenderProjectionClass.IndoorCellStatic</c>.</summary>
|
||||
WalkFrameStaticRecords GetCellStatics(uint cellId);
|
||||
|
|
@ -113,8 +122,8 @@ internal interface IWalkFrameLeafRenderer
|
|||
/// pipeline-state citation. A <see cref="WalkFrameEventKind.StreamMark"/>
|
||||
/// is NOT one of them (F10: the ordered stream it draws is opaque-only,
|
||||
/// depth test AND write on, no blending); the F2 particle-turn exception
|
||||
/// still holds (a genuinely empty <see cref="DrawStaticParticles"/>/<see
|
||||
/// cref="DrawCellParticles"/> turn neither submits nor flushes). This is
|
||||
/// still holds (a genuinely empty <see cref="PrepareStaticParticles"/>/<see
|
||||
/// cref="PrepareCellParticles"/> turn neither submits nor flushes). This is
|
||||
/// order-preserving by construction: the batch's GPU submission point is
|
||||
/// always the SAME point the unbatched terrain draws would have occupied
|
||||
/// (immediately before the next real submission), so pixel output is
|
||||
|
|
@ -133,10 +142,10 @@ internal interface IWalkFrameLeafRenderer
|
|||
/// name="cellId"/> has any renderable emitter right now — production
|
||||
/// wires this to <c>ParticleSystem.HasRenderableEmittersInCell</c> with
|
||||
/// the SAME <c>ParticleRenderPass.Scene</c> pass both <see
|
||||
/// cref="DrawStaticParticles"/> and <see cref="DrawCellParticles"/>
|
||||
/// already draw through. <see cref="WalkFrameDriver.Replay"/> asks this
|
||||
/// BEFORE flushing the pending terrain batch (or calling either particle
|
||||
/// leaf) so a genuinely empty particle turn submits nothing and does not
|
||||
/// cref="PrepareStaticParticles"/> and <see cref="PrepareCellParticles"/>
|
||||
/// prepare through. <see cref="WalkFrameDriver.Replay"/> asks this BEFORE
|
||||
/// flushing the pending terrain batch (or preparing either particle leaf)
|
||||
/// so a genuinely empty particle turn submits nothing and does not
|
||||
/// force an otherwise-unneeded terrain flush.</summary>
|
||||
bool HasRenderableEmittersInCell(uint cellId);
|
||||
|
||||
|
|
@ -157,12 +166,12 @@ internal interface IWalkFrameLeafRenderer
|
|||
/// (0x00514a70) draws an emitter at its own current cell's object turn
|
||||
/// regardless of its attached owner's registry membership, so this is a
|
||||
/// cell lookup, not an owner union.</summary>
|
||||
void DrawStaticParticles(uint cellId);
|
||||
ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareStaticParticles(uint cellId);
|
||||
|
||||
/// <summary>Submits one indoor cell's own-cell particle emitters at that
|
||||
/// cell's own object-list turn. Meshes have already entered the ordered
|
||||
/// stream before this leaf event.</summary>
|
||||
void DrawCellParticles(uint cellId);
|
||||
ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareCellParticles(uint cellId);
|
||||
|
||||
/// <summary><c>PView::DrawCells</c> @0x005a4840's gated full depth clear
|
||||
/// (pc:432731-432732) between the outside stage and the interior root's
|
||||
|
|
@ -347,7 +356,7 @@ internal enum WalkFrameEventKind : byte
|
|||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawExitSeals"/>.</summary>
|
||||
ExitSeals,
|
||||
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawStaticParticles"/> —
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.PrepareStaticParticles"/> —
|
||||
/// ONE landscape (land) cell's (<see cref="WalkFrameEvent.CellId"/>)
|
||||
/// own-cell particle emitters, emitted AT ITS OWN WALK TURN. Retail's
|
||||
/// falls containment is positional: an outdoor emitter's polys join the
|
||||
|
|
@ -368,7 +377,7 @@ internal enum WalkFrameEventKind : byte
|
|||
/// owns one shadow in its OWN current cell, never its parent's).</summary>
|
||||
StaticParticles,
|
||||
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawCellParticles"/> —
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.PrepareCellParticles"/> —
|
||||
/// <see cref="WalkFrameEvent.CellId"/> is the cell. Fires at EVERY
|
||||
/// visited interior cell's own object-list turn regardless of whether
|
||||
/// that cell has any visible static/dynamic owner record: an emitter
|
||||
|
|
@ -481,11 +490,23 @@ internal readonly struct WalkFrameEvent
|
|||
internal static WalkFrameEvent AlphaBarrier() =>
|
||||
new(WalkFrameEventKind.AlphaBarrier, 0, 0, 0f, null);
|
||||
|
||||
internal static WalkFrameEvent LandscapeCellParticles(uint cellId) =>
|
||||
new(WalkFrameEventKind.StaticParticles, 0, cellId, 0f, null);
|
||||
internal static WalkFrameEvent LandscapeCellParticles(
|
||||
uint cellId, int alphaExclusiveEnd, bool includeParticles) =>
|
||||
new(
|
||||
WalkFrameEventKind.StaticParticles,
|
||||
alphaExclusiveEnd,
|
||||
cellId,
|
||||
includeParticles ? 1f : 0f,
|
||||
null);
|
||||
|
||||
internal static WalkFrameEvent CellParticles(uint cellId) =>
|
||||
new(WalkFrameEventKind.CellParticles, 0, cellId, 0f, null);
|
||||
internal static WalkFrameEvent CellParticles(
|
||||
uint cellId, int alphaExclusiveEnd, bool includeParticles) =>
|
||||
new(
|
||||
WalkFrameEventKind.CellParticles,
|
||||
alphaExclusiveEnd,
|
||||
cellId,
|
||||
includeParticles ? 1f : 0f,
|
||||
null);
|
||||
|
||||
internal static WalkFrameEvent LandscapeFlush() =>
|
||||
new(WalkFrameEventKind.LandscapeFlush, 0, 0, 0f, null);
|
||||
|
|
@ -1138,7 +1159,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_alphaSubmissions[alphaCursor];
|
||||
_dispatcher.SubmitWalkAlphaInstance(
|
||||
in batch,
|
||||
_cameraWorldPosition,
|
||||
_viewProjection);
|
||||
}
|
||||
break;
|
||||
|
|
@ -1205,18 +1225,16 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
// current cell, drawn at that cell's object turn
|
||||
// regardless of any owner's registry membership —
|
||||
// this is a cell lookup, not an owner union.
|
||||
if (_leafRenderer.HasRenderableEmittersInCell(e.CellId))
|
||||
{
|
||||
FlushPendingTerrainBatch();
|
||||
_leafRenderer.DrawStaticParticles(e.CellId);
|
||||
}
|
||||
SubmitCellAlpha(
|
||||
e,
|
||||
ref alphaCursor,
|
||||
staticParticleTurn: true);
|
||||
break;
|
||||
case WalkFrameEventKind.CellParticles:
|
||||
if (_leafRenderer.HasRenderableEmittersInCell(e.CellId))
|
||||
{
|
||||
FlushPendingTerrainBatch();
|
||||
_leafRenderer.DrawCellParticles(e.CellId);
|
||||
}
|
||||
SubmitCellAlpha(
|
||||
e,
|
||||
ref alphaCursor,
|
||||
staticParticleTurn: false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -1231,13 +1249,62 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_events.Clear();
|
||||
_markPositions.Clear();
|
||||
_alphaSubmissions.Clear();
|
||||
_pendingTerrainBatch.Clear();
|
||||
_pendingTerrainBatch.Clear();
|
||||
_alphaSubmitMark = 0;
|
||||
_readyToReplay = false;
|
||||
_ctx = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S4-c3a: completes one retail object-cell turn. Object delayed subsets
|
||||
/// are already in stable far→near part order. Real particle records are
|
||||
/// prepared only now, then merged by their retained CYpt before either
|
||||
/// source appends to the two untouched FIFO queue lists. Object wins an
|
||||
/// exactly equal cross-source key; AP-242 records that deterministic
|
||||
/// source tie because acdream has no shared physical registration
|
||||
/// ordinal. The event always drains its object range, including a cell
|
||||
/// with no particle emitters.
|
||||
/// </summary>
|
||||
private void SubmitCellAlpha(
|
||||
in WalkFrameEvent e,
|
||||
ref int alphaCursor,
|
||||
bool staticParticleTurn)
|
||||
{
|
||||
ReadOnlySpan<PreparedParticleAlphaSubmission> particles =
|
||||
ReadOnlySpan<PreparedParticleAlphaSubmission>.Empty;
|
||||
bool includeParticles = e.FloatArg != 0f;
|
||||
if (includeParticles && _leafRenderer.HasRenderableEmittersInCell(e.CellId))
|
||||
{
|
||||
// Preserve the existing particle-turn terrain boundary: a row-5
|
||||
// immediate mesh may draw during preparation.
|
||||
FlushPendingTerrainBatch();
|
||||
particles = staticParticleTurn
|
||||
? _leafRenderer.PrepareStaticParticles(e.CellId)
|
||||
: _leafRenderer.PrepareCellParticles(e.CellId);
|
||||
}
|
||||
|
||||
int objectEnd = e.IntArg;
|
||||
int particleIndex = 0;
|
||||
while (alphaCursor < objectEnd || particleIndex < particles.Length)
|
||||
{
|
||||
bool takeParticle = particleIndex < particles.Length
|
||||
&& (alphaCursor >= objectEnd
|
||||
|| particles[particleIndex].DistanceSq
|
||||
> _alphaSubmissions[alphaCursor].SortDistanceSq);
|
||||
if (takeParticle)
|
||||
{
|
||||
particles[particleIndex++].Append();
|
||||
}
|
||||
else
|
||||
{
|
||||
WbDrawDispatcher.WalkClassifiedBatch batch =
|
||||
_alphaSubmissions[alphaCursor++];
|
||||
_dispatcher.SubmitWalkAlphaInstance(in batch, _viewProjection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>S3 chunk 3 fix round 1 (F2), round 2 (F10): submits the
|
||||
/// pending terrain batch as ONE <see cref="IWalkFrameLeafRenderer.DrawLandCellBatch"/>
|
||||
/// call, if it holds anything, and clears it. Called immediately before
|
||||
|
|
@ -1446,23 +1513,18 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
+ "view set — the walk and draw driver are desynchronized.");
|
||||
}
|
||||
VisitedLandscapeCellIds.Add(cellId);
|
||||
WalkFrameStaticRecords records = _worldData.GetOutdoorStatics(cellId);
|
||||
WalkFrameStaticRecords dynamics = _worldData.GetOutdoorDynamics(cellId);
|
||||
_populator.PopulateOutdoorStatics(
|
||||
_stream, cellId, records.Records, records.TupleLandblockId,
|
||||
_cameraWorldPosition, _viewProjection,
|
||||
this, _landscapeViewRouteIndex,
|
||||
alphaSubmissions: _alphaSubmissions);
|
||||
_populator.PopulateCellDynamics(
|
||||
_stream, cellId, dynamics.Records, dynamics.TupleLandblockId,
|
||||
_cameraWorldPosition, _viewProjection,
|
||||
this, _landscapeViewRouteIndex,
|
||||
alphaSubmissions: _alphaSubmissions);
|
||||
if (_alphaSubmissions.Count != _alphaSubmitMark)
|
||||
{
|
||||
MarkIfGrown();
|
||||
MarkAlphaIfGrown();
|
||||
}
|
||||
WalkFrameStaticRecords records = _worldData.GetOutdoorObjects(cellId);
|
||||
_populator.PopulateCellObjects(
|
||||
_stream,
|
||||
WalkDrawStage.OutdoorStatic,
|
||||
cellId,
|
||||
records.Records,
|
||||
records.TupleLandblockId,
|
||||
_cameraWorldPosition,
|
||||
_viewProjection,
|
||||
this,
|
||||
_landscapeViewRouteIndex,
|
||||
_alphaSubmissions);
|
||||
// FW4 (the #132 positional invariant): this cell's emitters submit
|
||||
// AT THIS TURN, so nearer buildings' pre-punch barriers drain them
|
||||
// against still-true depth — see WalkFrameEventKind.StaticParticles.
|
||||
|
|
@ -1473,7 +1535,12 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
// has any visible static/dynamic owner record (Campaign OVERHAUL S2
|
||||
// chunk 6 — a suspended/hidden owner's emitter must still show).
|
||||
MarkIfGrown();
|
||||
_events.Add(WalkFrameEvent.LandscapeCellParticles(cellId));
|
||||
bool includeParticles = _cellParticleTurnsDrawnThisFrame.Add(cellId);
|
||||
_alphaSubmitMark = _alphaSubmissions.Count;
|
||||
_events.Add(WalkFrameEvent.LandscapeCellParticles(
|
||||
cellId,
|
||||
_alphaSubmitMark,
|
||||
includeParticles));
|
||||
}
|
||||
|
||||
/// <summary>S4-c2: retail <c>RenderDeviceD3D::DrawBlock</c> @0x005a17c0's
|
||||
|
|
@ -1837,13 +1904,19 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
// The shell loop captured the exact live portal_view for this cell.
|
||||
// Static and dynamic object lists reuse that same route: retail's
|
||||
// DrawEnvCell and DrawObjCell consume the same PortalList.
|
||||
WalkFrameStaticRecords records = _worldData.GetCellStatics(cellId);
|
||||
_populator.PopulateCell(
|
||||
_stream, stage, cellId, records.Records, records.TupleLandblockId,
|
||||
_cameraWorldPosition, _viewProjection,
|
||||
this, viewRouteIndex, _alphaSubmissions);
|
||||
WalkFrameStaticRecords records = _worldData.GetCellObjects(cellId);
|
||||
_populator.PopulateCellObjects(
|
||||
_stream,
|
||||
stage,
|
||||
cellId,
|
||||
records.Records,
|
||||
records.TupleLandblockId,
|
||||
_cameraWorldPosition,
|
||||
_viewProjection,
|
||||
this,
|
||||
viewRouteIndex,
|
||||
_alphaSubmissions);
|
||||
MarkIfGrown();
|
||||
MarkAlphaIfGrown();
|
||||
|
||||
if (stage == WalkDrawStage.LookInStatic)
|
||||
{
|
||||
|
|
@ -1851,19 +1924,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
LookInCells.Add(cellId);
|
||||
}
|
||||
|
||||
WalkFrameStaticRecords dynamics = _worldData.GetCellDynamics(cellId);
|
||||
_populator.PopulateCellDynamics(
|
||||
_stream,
|
||||
cellId,
|
||||
dynamics.Records,
|
||||
dynamics.TupleLandblockId,
|
||||
_cameraWorldPosition,
|
||||
_viewProjection,
|
||||
this,
|
||||
viewRouteIndex,
|
||||
alphaSubmissions: _alphaSubmissions);
|
||||
MarkIfGrown();
|
||||
MarkAlphaIfGrown();
|
||||
// Fires for every cell turn regardless of whether that cell has any
|
||||
// visible static/dynamic owner record (Campaign OVERHAUL S2 chunk 6 —
|
||||
// a suspended/hidden owner's emitter must still show), but ONCE per
|
||||
|
|
@ -1873,8 +1933,12 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
// frame (a chamber reached through two portals). The same frame-scoped
|
||||
// set that dedupes the cell SHELL dedupes the particle turn (chunk 6
|
||||
// review F1).
|
||||
if (_cellParticleTurnsDrawnThisFrame.Add(cellId))
|
||||
_events.Add(WalkFrameEvent.CellParticles(cellId));
|
||||
bool includeParticles = _cellParticleTurnsDrawnThisFrame.Add(cellId);
|
||||
_alphaSubmitMark = _alphaSubmissions.Count;
|
||||
_events.Add(WalkFrameEvent.CellParticles(
|
||||
cellId,
|
||||
_alphaSubmitMark,
|
||||
includeParticles));
|
||||
}
|
||||
|
||||
private void CaptureCellViews(uint cellId)
|
||||
|
|
|
|||
|
|
@ -83,10 +83,12 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellCache = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellDynamicCache = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellObjectsCache = new();
|
||||
private readonly Dictionary<string, string> _facilityShadowProbeSignatures = new();
|
||||
private readonly Dictionary<uint, List<RenderProjectionRecord>> _shellsByAnchor = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _outdoorMaterialized = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _outdoorDynamicsMaterialized = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _outdoorObjectsMaterialized = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _shellMaterialized = new();
|
||||
|
||||
// Campaign OVERHAUL S2 chunk 5: every distinct entity id counted into
|
||||
|
|
@ -179,8 +181,10 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
_tupleLandblockId = tupleLandblockId;
|
||||
_cellCache.Clear();
|
||||
_cellDynamicCache.Clear();
|
||||
_cellObjectsCache.Clear();
|
||||
_outdoorMaterialized.Clear();
|
||||
_outdoorDynamicsMaterialized.Clear();
|
||||
_outdoorObjectsMaterialized.Clear();
|
||||
_shellMaterialized.Clear();
|
||||
_arenaLength = 0;
|
||||
UnregisteredRenderMembershipCount = 0;
|
||||
|
|
@ -252,6 +256,16 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
return records;
|
||||
}
|
||||
|
||||
public WalkFrameStaticRecords GetCellObjects(uint cellId)
|
||||
{
|
||||
if (_cellObjectsCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
|
||||
return cached;
|
||||
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: null);
|
||||
EmitFacilityShadowProbe(records.Records, cellId, "combined");
|
||||
_cellObjectsCache[cellId] = records;
|
||||
return records;
|
||||
}
|
||||
|
||||
public WalkFrameStaticRecords GetCellDynamics(uint cellId)
|
||||
{
|
||||
if (_cellDynamicCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
|
||||
|
|
@ -285,6 +299,20 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
return records;
|
||||
}
|
||||
|
||||
public WalkFrameStaticRecords GetOutdoorObjects(uint cellId)
|
||||
{
|
||||
if (_outdoorObjectsMaterialized.TryGetValue(
|
||||
cellId,
|
||||
out WalkFrameStaticRecords cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: null);
|
||||
_outdoorObjectsMaterialized[cellId] = records;
|
||||
return records;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S2 chunk 5: one cell id's borrowed retail render
|
||||
/// membership. Reads <see cref="ShadowObjectRegistry.GetRetailPartEntriesInCell"/>
|
||||
|
|
@ -303,7 +331,7 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
/// entity into every crossed cell under its own true id, so no
|
||||
/// additional indoor/outdoor dispatch is needed here.
|
||||
/// </summary>
|
||||
private WalkFrameStaticRecords ResolveCellView(uint cellId, bool dynamic)
|
||||
private WalkFrameStaticRecords ResolveCellView(uint cellId, bool? dynamic)
|
||||
{
|
||||
IReadOnlyList<RetailPartEntry> entries =
|
||||
_shadows.GetRetailPartEntriesInCell(cellId);
|
||||
|
|
@ -335,7 +363,8 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
}
|
||||
if (record.EntityPayload.IsBuildingShell)
|
||||
continue; // buildings draw at their own shell turn.
|
||||
if (IsDynamicProjectionClass(record.ProjectionClass) != dynamic)
|
||||
if (dynamic.HasValue
|
||||
&& IsDynamicProjectionClass(record.ProjectionClass) != dynamic.Value)
|
||||
continue;
|
||||
|
||||
if (written == _cellViewScratch.Length)
|
||||
|
|
|
|||
|
|
@ -18,14 +18,11 @@ namespace AcDream.App.Rendering.Walk;
|
|||
/// shell over terrain) — and retail resolves those by first-drawn-wins in
|
||||
/// ITS walk/cell-content order, not by any texture/material grouping. So
|
||||
/// this populator appends one <see cref="OrderedDrawCommand"/> per (entity,
|
||||
/// opaque batch) in the SAME order the caller's <c>records</c> span presents
|
||||
/// them — never material-grouped, never re-sorted. Translucent batches go to
|
||||
/// the SAME <see cref="RetailAlphaQueue"/> the classic/packed paths already
|
||||
/// use (global far→near re-sort still applies; walk-order submission only
|
||||
/// improves retail's submission-order tie-break fidelity for coincident
|
||||
/// distances — <c>WbDrawDispatcher.DeferTransparentGroups</c>'s own doc
|
||||
/// comment cites the same <c>CShadowPart::insertion_sort</c> stability this
|
||||
/// preserves).</para>
|
||||
/// opaque batch) in the per-cell stable far→near part order reconstructed
|
||||
/// from authored GfxObj SortCenter. Opaque/cutout and delayed projections
|
||||
/// derive from that same order. Translucent batches then enter the SAME two
|
||||
/// FIFO <see cref="RetailAlphaQueue"/> lists as particles; the queues never
|
||||
/// perform a scope-global sort.</para>
|
||||
///
|
||||
/// <para>Reads no retained scene state itself: every method takes the target
|
||||
/// <see cref="OrderedDrawStream"/> and a caller-supplied span of already
|
||||
|
|
@ -40,6 +37,12 @@ internal sealed class WalkStaticStreamPopulator
|
|||
private readonly WbDrawDispatcher _dispatcher;
|
||||
private readonly List<WbDrawDispatcher.WalkClassifiedBatch> _batchScratch = new();
|
||||
private readonly List<WbDrawDispatcher.WalkClassifiedSelectionPart> _selectionScratch = new();
|
||||
private readonly List<CellBatch> _cellBatchScratch = new();
|
||||
|
||||
private readonly record struct CellBatch(
|
||||
WbDrawDispatcher.WalkClassifiedBatch Batch,
|
||||
WalkDrawStage Stage,
|
||||
uint LocalEntityId);
|
||||
|
||||
internal WalkStaticStreamPopulator(WbDrawDispatcher dispatcher)
|
||||
{
|
||||
|
|
@ -147,6 +150,120 @@ internal sealed class WalkStaticStreamPopulator
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S4-c3a: classifies the cell's combined static/dynamic registry view,
|
||||
/// computes every subset's CYpt from its authored part sort center,
|
||||
/// performs retail's stable descending insertion sort, and projects the
|
||||
/// same order to opaque commands and delayed records. Equal keys retain
|
||||
/// registry/part/subset order because only a strictly farther record is
|
||||
/// moved left.
|
||||
///
|
||||
/// <para>AP-243: this always-authored per-part key is deliberately more
|
||||
/// exact than retail's greater-than-50 m branch. Retail
|
||||
/// <c>UpdateObjCell</c> @0x005A06B7..0x005A0720 gives every part the
|
||||
/// shared cell CYpt/heading there, preserving insertion order; acquiring
|
||||
/// that cell-distance/heading LOD input is outside this bounded repair.</para>
|
||||
/// </summary>
|
||||
internal void PopulateCellObjects(
|
||||
OrderedDrawStream stream,
|
||||
WalkDrawStage staticStage,
|
||||
uint cellId,
|
||||
ReadOnlySpan<RenderProjectionRecord> records,
|
||||
uint tupleLandblockId,
|
||||
Vector3 cameraWorldPosition,
|
||||
Matrix4x4 viewProjection,
|
||||
IWalkLookInViewSource? views,
|
||||
int viewRouteIndex,
|
||||
List<WbDrawDispatcher.WalkClassifiedBatch> alphaSubmissions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
ArgumentNullException.ThrowIfNull(alphaSubmissions);
|
||||
_cellBatchScratch.Clear();
|
||||
|
||||
for (int recordIndex = 0; recordIndex < records.Length; recordIndex++)
|
||||
{
|
||||
ref readonly RenderProjectionRecord record = ref records[recordIndex];
|
||||
bool liveDynamic = record.ProjectionClass is RenderProjectionClass.LiveDynamicRoot
|
||||
or RenderProjectionClass.EquippedChild;
|
||||
WalkDrawStage stage = liveDynamic ? WalkDrawStage.Dynamic : staticStage;
|
||||
_batchScratch.Clear();
|
||||
_selectionScratch.Clear();
|
||||
_dispatcher.ClassifyEntityForWalk(
|
||||
in record,
|
||||
tupleLandblockId,
|
||||
_batchScratch,
|
||||
_selectionScratch,
|
||||
liveDynamic,
|
||||
views,
|
||||
viewRouteIndex,
|
||||
cellId,
|
||||
diagnosticViewProjection: viewProjection);
|
||||
|
||||
for (int batchIndex = 0; batchIndex < _batchScratch.Count; batchIndex++)
|
||||
{
|
||||
WbDrawDispatcher.WalkClassifiedBatch batch = _batchScratch[batchIndex];
|
||||
Vector3 worldSortCenter = Vector3.Transform(batch.LocalSortCenter, batch.Transform);
|
||||
batch = batch with
|
||||
{
|
||||
SortDistanceSq = Vector3.DistanceSquared(
|
||||
worldSortCenter, cameraWorldPosition),
|
||||
};
|
||||
_cellBatchScratch.Add(new CellBatch(
|
||||
batch,
|
||||
stage,
|
||||
record.Source.LocalEntityId));
|
||||
}
|
||||
|
||||
for (int selectionIndex = 0; selectionIndex < _selectionScratch.Count; selectionIndex++)
|
||||
{
|
||||
WbDrawDispatcher.WalkClassifiedSelectionPart part =
|
||||
_selectionScratch[selectionIndex];
|
||||
_dispatcher.PublishWalkSelectionPart(in part);
|
||||
}
|
||||
}
|
||||
|
||||
StableSortCellBatches(_cellBatchScratch);
|
||||
for (int i = 0; i < _cellBatchScratch.Count; i++)
|
||||
{
|
||||
CellBatch item = _cellBatchScratch[i];
|
||||
WbDrawDispatcher.WalkClassifiedBatch batch = item.Batch;
|
||||
if (batch.IsOpaque)
|
||||
{
|
||||
int commandIndex = stream.Count;
|
||||
stream.Append(new OrderedDrawCommand(
|
||||
batch.Key, batch.Transform, item.Stage, cellId, batch.ClipSlot,
|
||||
batch.Lights, batch.IndoorFlag, batch.Alpha,
|
||||
batch.SelectionLighting, batch.DetailCategory));
|
||||
_dispatcher.ProbeFacilityStairCommandAppended(
|
||||
commandIndex,
|
||||
item.LocalEntityId,
|
||||
cellId,
|
||||
item.Stage,
|
||||
in batch);
|
||||
}
|
||||
else
|
||||
{
|
||||
alphaSubmissions.Add(batch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void StableSortCellBatches(List<CellBatch> batches)
|
||||
{
|
||||
for (int i = 1; i < batches.Count; i++)
|
||||
{
|
||||
CellBatch value = batches[i];
|
||||
int insertion = i;
|
||||
while (insertion > 0
|
||||
&& value.Batch.SortDistanceSq > batches[insertion - 1].Batch.SortDistanceSq)
|
||||
{
|
||||
batches[insertion] = batches[insertion - 1];
|
||||
insertion--;
|
||||
}
|
||||
batches[insertion] = value;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClassifyAndAppend(
|
||||
OrderedDrawStream stream,
|
||||
WalkDrawStage stage,
|
||||
|
|
@ -195,7 +312,7 @@ internal sealed class WalkStaticStreamPopulator
|
|||
if (alphaSubmissions is null)
|
||||
{
|
||||
_dispatcher.SubmitWalkAlphaInstance(
|
||||
in batch, cameraWorldPosition, viewProjection);
|
||||
in batch, viewProjection);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// key's VALUE never orders anything: groups are enumerated in the dictionary's
|
||||
/// insertion order and sorted by cull mode then camera distance
|
||||
/// (<c>CompareOpaqueSubmissionOrder</c> / <c>CompareTransparentSubmissionOrder</c>),
|
||||
/// and the delayed-alpha path sorts by viewer distance then submission ordinal.
|
||||
/// while the normal-world delayed-alpha path orders authored part centers
|
||||
/// stably within each cell before appending to two FIFO lists. The grouped
|
||||
/// fallback restores submission ordinal after material grouping; it is not a
|
||||
/// scope-global world-alpha distance sort.
|
||||
/// The key reaches only equality, hashing, and the scene-digest fingerprints.</para>
|
||||
///
|
||||
/// <para>Campaign VM VM6 review fix round: <see cref="FoliageFlags"/> joined
|
||||
|
|
|
|||
|
|
@ -146,8 +146,10 @@ public sealed partial class WbDrawDispatcher
|
|||
/// (the authored GfxObj sort center — the same value
|
||||
/// <c>InstanceGroup.LocalSortCenters</c> carries per instance today; S4-c2
|
||||
/// deleted its one alpha-ordering consumer, <c>RetailAlphaOrdering.ComputeViewerDistance</c>,
|
||||
/// but the field itself stays: <c>InstanceGroup.LocalSortCenters</c> is a
|
||||
/// general per-instance record this type mirrors, not an alpha-only one).
|
||||
/// but S4-c3a restores its retail use: <c>WalkStaticStreamPopulator</c>
|
||||
/// transforms it through the part model and retains the resulting
|
||||
/// squared CYpt key in <see cref="SortDistanceSq"/> before the per-cell
|
||||
/// stable sort. The key feeds two FIFO alpha lists; neither queue sorts.
|
||||
/// </summary>
|
||||
internal readonly record struct WalkClassifiedBatch(
|
||||
GroupKey Key,
|
||||
|
|
@ -159,7 +161,8 @@ public sealed partial class WbDrawDispatcher
|
|||
Vector2 SelectionLighting,
|
||||
uint DetailCategory,
|
||||
bool IsOpaque,
|
||||
Vector3 LocalSortCenter);
|
||||
Vector3 LocalSortCenter,
|
||||
float SortDistanceSq = 0f);
|
||||
|
||||
/// <summary>
|
||||
/// One retail-picking part surfaced by <see cref="ClassifyEntityForWalk"/>
|
||||
|
|
@ -889,10 +892,8 @@ public sealed partial class WbDrawDispatcher
|
|||
/// </summary>
|
||||
internal void SubmitWalkAlphaInstance(
|
||||
in WalkClassifiedBatch batch,
|
||||
Vector3 cameraWorldPosition,
|
||||
Matrix4x4 viewProjection)
|
||||
{
|
||||
_ = cameraWorldPosition; // S4-c2: retail's queues are FIFO, not distance-sorted.
|
||||
RetailAlphaQueue queue = _alphaQueue
|
||||
?? throw new InvalidOperationException(
|
||||
"SubmitWalkAlphaInstance requires an active RetailAlphaQueue.");
|
||||
|
|
|
|||
|
|
@ -47,9 +47,12 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// <c>DrawElementsIndirectCommand</c>. Per-frame ring allocations carry instance
|
||||
/// matrices (binding 0), per-group batch metadata/texture-table slots (binding
|
||||
/// 1), and the indirect draw commands. Opaque world groups remain MDI-batched.
|
||||
/// Transparent world instances enter <see cref="RetailAlphaQueue"/> so ordinary
|
||||
/// GfxObj parts and particles share retail's stable far-to-near stream; sealed
|
||||
/// off-screen consumers retain the immediate transparent MDI path.
|
||||
/// Transparent world instances enter <see cref="RetailAlphaQueue"/> after
|
||||
/// <c>WalkFrameDriver</c> reconstructs retail's stable far-to-near order
|
||||
/// independently for each object-cell turn from authored part SortCenter.
|
||||
/// Object and particle records are merged before they append to retail's two
|
||||
/// FIFO lists; sealed off-screen consumers retain the immediate transparent
|
||||
/// MDI path.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -2419,14 +2422,11 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
throw new InvalidOperationException(
|
||||
"One retail alpha scope cannot combine different view-projection matrices.");
|
||||
|
||||
// Retail CShadowPart::insertion_sort (0x006B5130) is stable:
|
||||
// equal-CYpt parts keep the order in which the cell submitted them.
|
||||
// Material grouping is an acdream batching detail and must not become
|
||||
// that tiebreak. Reconstruct the original draw-local instance order
|
||||
// before handing entries to the queue — S4-c2: retail's own queue is
|
||||
// FIFO (D3DPolyRender::AddMeshToAlphaList never sorts), so preserving
|
||||
// this walk-order sequence IS the ordering, not a sort key for a
|
||||
// later comparator.
|
||||
// a tiebreak. Reconstruct original draw-local submission order before
|
||||
// handing entries to retail's FIFO queue. The normal-world walk path
|
||||
// does not use this grouped route: WalkStaticStreamPopulator performs
|
||||
// the per-cell stable CYpt sort before its per-instance submissions.
|
||||
_alphaFingerprintScratch.Clear();
|
||||
foreach (InstanceGroup group in _translucentDraws)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
|
|
@ -12,6 +13,7 @@ using AcDream.App.Rendering.Walk;
|
|||
using AcDream.App.Tests.Rendering.Gpu;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Vfx;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
|
@ -130,9 +132,17 @@ public sealed partial class WalkFrameDriverTests
|
|||
alpha.Flush(RetailAlphaFlushSite.SortCellExit, 0.75f);
|
||||
}
|
||||
|
||||
public void DrawStaticParticles(uint cellId) => log.Add($"PARTICLES:{cellId:x8}");
|
||||
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareStaticParticles(uint cellId)
|
||||
{
|
||||
log.Add($"PARTICLES:{cellId:x8}");
|
||||
return ReadOnlySpan<PreparedParticleAlphaSubmission>.Empty;
|
||||
}
|
||||
|
||||
public void DrawCellParticles(uint cellId) => log.Add($"CELL-PARTICLES:{cellId:x8}");
|
||||
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareCellParticles(uint cellId)
|
||||
{
|
||||
log.Add($"CELL-PARTICLES:{cellId:x8}");
|
||||
return ReadOnlySpan<PreparedParticleAlphaSubmission>.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingTrace(List<string> log) : IWalkFrameDriverTrace
|
||||
|
|
@ -141,6 +151,39 @@ public sealed partial class WalkFrameDriverTests
|
|||
log.Add($"FLUSH:{commandCount}:{string.Join(',', stages.Distinct())}");
|
||||
}
|
||||
|
||||
private sealed class ProductionParticleLeaf(
|
||||
ParticleSystem particles,
|
||||
ParticleRenderer renderer,
|
||||
ICamera camera,
|
||||
Vector3 cameraWorldPosition) : IWalkFrameLeafRenderer
|
||||
{
|
||||
public void DrawSky() { }
|
||||
public void DrawLandCellBatch(
|
||||
IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells) { }
|
||||
public bool HasRenderableEmittersInCell(uint cellId) =>
|
||||
particles.HasRenderableEmittersInCell(ParticleRenderPass.Scene, cellId);
|
||||
public void DrawCellShell(uint cellId) { }
|
||||
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareStaticParticles(uint cellId) =>
|
||||
renderer.PrepareForCellAlpha(
|
||||
camera, cameraWorldPosition, ParticleRenderPass.Scene, cellId);
|
||||
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareCellParticles(uint cellId) =>
|
||||
renderer.PrepareForCellAlpha(
|
||||
camera, cameraWorldPosition, ParticleRenderPass.Scene, cellId);
|
||||
public void ClearInteriorDepth() { }
|
||||
public void FlushLandscape() { }
|
||||
public int DrawExitSeals() => 0;
|
||||
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) { }
|
||||
public void AlphaBarrier() { }
|
||||
public void FlushSortCellExit() { }
|
||||
}
|
||||
|
||||
private sealed class IdentityCamera : ICamera
|
||||
{
|
||||
public Matrix4x4 View => Matrix4x4.Identity;
|
||||
public Matrix4x4 Projection => Matrix4x4.Identity;
|
||||
public float Aspect { get; set; } = 1f;
|
||||
}
|
||||
|
||||
private sealed class FakeWorldData : IWalkFrameWorldData
|
||||
{
|
||||
public readonly Dictionary<uint, WalkFrameStaticRecords> CellStaticsByCell = new();
|
||||
|
|
@ -153,12 +196,22 @@ public sealed partial class WalkFrameDriverTests
|
|||
public WalkFrameStaticRecords GetCellStatics(uint cellId) =>
|
||||
CellStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
|
||||
|
||||
public WalkFrameStaticRecords GetCellObjects(uint cellId) =>
|
||||
Combine(
|
||||
CellStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty),
|
||||
CellDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty));
|
||||
|
||||
public WalkFrameStaticRecords GetCellDynamics(uint cellId) =>
|
||||
CellDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
|
||||
|
||||
public WalkFrameStaticRecords GetOutdoorStatics(uint cellId) =>
|
||||
OutdoorStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
|
||||
|
||||
public WalkFrameStaticRecords GetOutdoorObjects(uint cellId) =>
|
||||
Combine(
|
||||
OutdoorStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty),
|
||||
OutdoorDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty));
|
||||
|
||||
public WalkFrameStaticRecords GetOutdoorDynamics(uint cellId) =>
|
||||
OutdoorDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
|
||||
|
||||
|
|
@ -167,6 +220,25 @@ public sealed partial class WalkFrameDriverTests
|
|||
|
||||
public Matrix4x4 GetBuildingWorldTransform(WalkBuilding building) =>
|
||||
WorldTransformByBuilding.GetValueOrDefault(building, Matrix4x4.Identity);
|
||||
|
||||
private static WalkFrameStaticRecords Combine(
|
||||
WalkFrameStaticRecords first,
|
||||
WalkFrameStaticRecords second)
|
||||
{
|
||||
if (second.Records.Count == 0)
|
||||
return first;
|
||||
RenderProjectionRecord[] dynamicRecords = second.Records
|
||||
.Select(record => record with
|
||||
{
|
||||
ProjectionClass = RenderProjectionClass.LiveDynamicRoot,
|
||||
})
|
||||
.ToArray();
|
||||
if (first.Records.Count == 0)
|
||||
return new WalkFrameStaticRecords(dynamicRecords, second.TupleLandblockId);
|
||||
return new WalkFrameStaticRecords(
|
||||
first.Records.Concat(dynamicRecords).ToArray(),
|
||||
first.TupleLandblockId);
|
||||
}
|
||||
}
|
||||
|
||||
// ── The walk-level test context (interior flood + building portal pass) ─
|
||||
|
|
@ -1001,7 +1073,9 @@ public sealed partial class WalkFrameDriverTests
|
|||
new[]
|
||||
{
|
||||
"ALPHA", "PUNCH:4@v0", "SHELL:00000104",
|
||||
"FLUSH:1:LookInStatic", "FLUSH:1:Dynamic",
|
||||
// S4-c3a: one cell shadow list, so static + dynamic are one
|
||||
// sorted stream segment rather than two artificial blocks.
|
||||
"FLUSH:2:LookInStatic,Dynamic",
|
||||
"CELL-PARTICLES:00000104",
|
||||
"FLUSH:1:BuildingShell",
|
||||
},
|
||||
|
|
@ -1805,6 +1879,205 @@ public sealed partial class WalkFrameDriverTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellTurn_RealParticlePreparationMergesWithObjectAlphaByCypt()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const uint cellId = 0x8C040005u;
|
||||
const ulong objectGfx = 0x0200_0C31UL;
|
||||
InjectRenderData(fx.Manager, objectGfx, MakeFlatMesh(
|
||||
MakeBatch(0x08100C31u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1)));
|
||||
var worldData = new FakeWorldData();
|
||||
worldData.OutdoorStaticsByCell[cellId] = new WalkFrameStaticRecords(
|
||||
new[]
|
||||
{
|
||||
MakeRecord(1, 0, new Vector3(20, 0, 0),
|
||||
[new MeshRef((uint)objectGfx, Matrix4x4.Identity)]),
|
||||
MakeRecord(2, 0, new Vector3(40, 0, 0),
|
||||
[new MeshRef((uint)objectGfx, Matrix4x4.Identity)]),
|
||||
},
|
||||
0x8C04u);
|
||||
|
||||
var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(42));
|
||||
EmitterDesc desc = new()
|
||||
{
|
||||
DatId = 0x32000C31u,
|
||||
Type = AcDream.Core.Vfx.ParticleType.Still,
|
||||
MaxParticles = 1,
|
||||
InitialParticles = 1,
|
||||
LifetimeMin = 100f,
|
||||
LifetimeMax = 100f,
|
||||
StartAlpha = 1f,
|
||||
EndAlpha = 1f,
|
||||
};
|
||||
int particle30 = particles.SpawnEmitter(desc, new Vector3(30, 0, 0));
|
||||
// Equal to object 1: AP-242's deterministic source tie is object-first.
|
||||
int particle20 = particles.SpawnEmitter(desc, new Vector3(20, 0, 0));
|
||||
particles.UpdateEmitterOwnerCell(particle30, cellId);
|
||||
particles.UpdateEmitterOwnerCell(particle20, cellId);
|
||||
using var renderer = new ParticleRenderer(
|
||||
fx.Device,
|
||||
fx.FrameLifetime,
|
||||
fx.Scope,
|
||||
particles,
|
||||
meshAdapter: fx.MeshAdapter,
|
||||
alphaQueue: fx.AlphaQueue);
|
||||
var leaf = new ProductionParticleLeaf(
|
||||
particles, renderer, new IdentityCamera(), Vector3.Zero);
|
||||
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
|
||||
var ctx = new TestContext();
|
||||
IWalkEventSink sink = driver;
|
||||
|
||||
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
|
||||
renderer.BeginFrame(frameSlot: 0);
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
var activeViews = new WalkPortalView();
|
||||
WalkCopyView.AppendFullViewportQuad(
|
||||
activeViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
|
||||
sink.OnLandscapeViews(activeViews);
|
||||
sink.OnLandscapeCellTurn(cellId);
|
||||
driver.EndFrame();
|
||||
driver.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
List<RetailAlphaEntry> entries = QueueAlphaEntries(fx.AlphaQueue);
|
||||
Assert.Equal(4, entries.Count);
|
||||
Assert.Equal(
|
||||
[
|
||||
typeof(WbDrawDispatcher),
|
||||
typeof(ParticleRenderer),
|
||||
typeof(WbDrawDispatcher),
|
||||
typeof(ParticleRenderer),
|
||||
],
|
||||
entries.Select(static entry => entry.Source.GetType().DeclaringType));
|
||||
|
||||
fx.AlphaQueue.AbortFrame();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeparateCellTurnsRemainCellMajorWhenLaterCellIsFarther()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const uint firstCell = 0x8C040005u;
|
||||
const uint secondCell = 0x8C040006u;
|
||||
const ulong objectGfx = 0x0200_0C32UL;
|
||||
InjectRenderData(fx.Manager, objectGfx, MakeFlatMesh(
|
||||
MakeBatch(0x08100C32u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1)));
|
||||
var worldData = new FakeWorldData();
|
||||
worldData.OutdoorStaticsByCell[firstCell] = new WalkFrameStaticRecords(
|
||||
new[] { MakeRecord(1, 0, new Vector3(5, 0, 0),
|
||||
[new MeshRef((uint)objectGfx, Matrix4x4.Identity)]) },
|
||||
0x8C04u);
|
||||
worldData.OutdoorStaticsByCell[secondCell] = new WalkFrameStaticRecords(
|
||||
new[] { MakeRecord(2, 0, new Vector3(50, 0, 0),
|
||||
[new MeshRef((uint)objectGfx, Matrix4x4.Identity)]) },
|
||||
0x8C04u);
|
||||
var log = new List<string>();
|
||||
var leaf = new RecordingLeafRenderer(log, fx.AlphaQueue);
|
||||
leaf.CellsWithoutEmitters.UnionWith([firstCell, secondCell]);
|
||||
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
|
||||
var ctx = new TestContext();
|
||||
IWalkEventSink sink = driver;
|
||||
|
||||
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);
|
||||
sink.OnLandscapeViews(activeViews);
|
||||
sink.OnLandscapeCellTurn(firstCell);
|
||||
sink.OnLandscapeCellTurn(secondCell);
|
||||
driver.EndFrame();
|
||||
driver.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
Assert.Equal(2, QueueAlphaEntries(fx.AlphaQueue).Count);
|
||||
IList payload = (IList)typeof(WbDrawDispatcher).GetField(
|
||||
"_deferredAlpha", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(fx.Dispatcher)!;
|
||||
Matrix4x4 firstModel = (Matrix4x4)payload[0]!.GetType().GetProperty("Model")!
|
||||
.GetValue(payload[0])!;
|
||||
Matrix4x4 secondModel = (Matrix4x4)payload[1]!.GetType().GetProperty("Model")!
|
||||
.GetValue(payload[1])!;
|
||||
Assert.Equal(5f, firstModel.M41);
|
||||
Assert.Equal(50f, secondModel.M41);
|
||||
|
||||
fx.AlphaQueue.AbortFrame();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductionCellObjectParticleMerge_WarmedPathAllocatesZeroBytes()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const uint cellId = 0x8C040007u;
|
||||
const ulong objectGfx = 0x0200_0C33UL;
|
||||
InjectRenderData(fx.Manager, objectGfx, MakeFlatMesh(
|
||||
MakeBatch(0x08100C33u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1)));
|
||||
var worldData = new FakeWorldData();
|
||||
worldData.OutdoorStaticsByCell[cellId] = new WalkFrameStaticRecords(
|
||||
new[] { MakeRecord(1, 0, new Vector3(40, 0, 0),
|
||||
[new MeshRef((uint)objectGfx, Matrix4x4.Identity)]) },
|
||||
0x8C04u);
|
||||
|
||||
var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(42));
|
||||
int handle = particles.SpawnEmitter(
|
||||
new EmitterDesc
|
||||
{
|
||||
DatId = 0x32000C33u,
|
||||
Type = AcDream.Core.Vfx.ParticleType.Still,
|
||||
MaxParticles = 1,
|
||||
InitialParticles = 1,
|
||||
LifetimeMin = 100f,
|
||||
LifetimeMax = 100f,
|
||||
StartAlpha = 1f,
|
||||
EndAlpha = 1f,
|
||||
},
|
||||
new Vector3(20, 0, 0));
|
||||
particles.UpdateEmitterOwnerCell(handle, cellId);
|
||||
using var renderer = new ParticleRenderer(
|
||||
fx.Device,
|
||||
fx.FrameLifetime,
|
||||
fx.Scope,
|
||||
particles,
|
||||
meshAdapter: fx.MeshAdapter,
|
||||
alphaQueue: fx.AlphaQueue);
|
||||
var leaf = new ProductionParticleLeaf(
|
||||
particles, renderer, new IdentityCamera(), Vector3.Zero);
|
||||
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
|
||||
var ctx = new TestContext();
|
||||
IWalkEventSink sink = driver;
|
||||
var activeViews = new WalkPortalView();
|
||||
WalkCopyView.AppendFullViewportQuad(
|
||||
activeViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
fx.Dispatcher.BeginFrame(frameSlot: 0);
|
||||
renderer.BeginFrame(frameSlot: 0);
|
||||
fx.Device.RecordingEnabled = false;
|
||||
|
||||
void RunCellTurn()
|
||||
{
|
||||
fx.AlphaQueue.BeginFrame();
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
sink.OnLandscapeViews(activeViews);
|
||||
sink.OnLandscapeCellTurn(cellId);
|
||||
driver.EndFrame();
|
||||
driver.Replay(draw.Frame, draw.Pass);
|
||||
fx.AlphaQueue.AbortFrame();
|
||||
}
|
||||
|
||||
long allocated = ZeroAllocationProbe.MeasureWarmed(
|
||||
RunCellTurn,
|
||||
batchSize: 128,
|
||||
warmupBatches: 2,
|
||||
samples: 4);
|
||||
|
||||
Assert.Equal(0, allocated);
|
||||
}
|
||||
|
||||
private static List<RetailAlphaEntry> QueueAlphaEntries(RetailAlphaQueue queue) =>
|
||||
(List<RetailAlphaEntry>)typeof(RetailAlphaQueue).GetField(
|
||||
"_alpha", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(queue)!;
|
||||
|
||||
// ── F4(b) (S3 chunk 3 fix round 1 §9.6): an interior root with one
|
||||
// surviving exit view — the SAME fixture as
|
||||
// RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells
|
||||
|
|
@ -2217,6 +2490,8 @@ public sealed partial class WalkFrameDriverTests
|
|||
|
||||
public ObjectMeshManager Manager => _meshAdapter.MeshManager!;
|
||||
|
||||
public WbMeshAdapter MeshAdapter => _meshAdapter;
|
||||
|
||||
public DrawScope BeginDraw(bool beginAlpha = false)
|
||||
{
|
||||
if (beginAlpha)
|
||||
|
|
|
|||
|
|
@ -120,6 +120,11 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
private static ObjectRenderData MakeFlatMesh(params ObjectRenderBatch[] batches) =>
|
||||
new() { Batches = new List<ObjectRenderBatch>(batches) };
|
||||
|
||||
private static ObjectRenderData MakeSortedMesh(
|
||||
Vector3 sortCenter,
|
||||
params ObjectRenderBatch[] batches) =>
|
||||
new() { SortCenter = sortCenter, 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,
|
||||
|
|
@ -135,6 +140,224 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
dict[id] = data;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PopulateCellObjects_UsesAuthoredSortCenterAndStableFarToNearOrder()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const ulong nearOrigin = 0x0100_0C01UL;
|
||||
const ulong farAuthoredCenter = 0x0100_0C02UL;
|
||||
InjectRenderData(fx.Manager, nearOrigin, MakeSortedMesh(
|
||||
Vector3.Zero,
|
||||
MakeBatch(101, TranslucencyKind.AlphaBlend, 101, 0, 3, 1)));
|
||||
InjectRenderData(fx.Manager, farAuthoredCenter, MakeSortedMesh(
|
||||
new Vector3(10, 0, 0),
|
||||
MakeBatch(202, TranslucencyKind.AlphaBlend, 202, 0, 3, 2)));
|
||||
|
||||
RenderProjectionRecord[] records =
|
||||
[
|
||||
MakeRecord(1, 0, new Vector3(5, 0, 0), [new MeshRef((uint)nearOrigin, Matrix4x4.Identity)]),
|
||||
MakeRecord(2, 0, Vector3.Zero, [new MeshRef((uint)farAuthoredCenter, Matrix4x4.Identity)]),
|
||||
];
|
||||
var alpha = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
||||
var stream = new OrderedDrawStream();
|
||||
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
|
||||
|
||||
populator.PopulateCellObjects(
|
||||
stream,
|
||||
WalkDrawStage.CellStatic,
|
||||
0x8C040112u,
|
||||
records,
|
||||
0x8C04u,
|
||||
Vector3.Zero,
|
||||
Matrix4x4.Identity,
|
||||
views: null,
|
||||
viewRouteIndex: -1,
|
||||
alpha);
|
||||
|
||||
Assert.Empty(stream.Keys);
|
||||
Assert.Equal([202u, 101u], alpha.Select(static batch => batch.Key.FirstIndex));
|
||||
Assert.Equal([100f, 25f], alpha.Select(static batch => batch.SortDistanceSq));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PopulateCellObjects_EqualCyptRetainsEntityPartAndSubsetOrder()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const ulong setup = 0x1000_0C10UL;
|
||||
const ulong firstPart = 0x0100_0C11UL;
|
||||
const ulong secondPart = 0x0100_0C12UL;
|
||||
InjectRenderData(fx.Manager, firstPart, MakeSortedMesh(
|
||||
Vector3.Zero,
|
||||
MakeBatch(11, TranslucencyKind.AlphaBlend, 11, 0, 3, 1),
|
||||
MakeBatch(12, TranslucencyKind.AlphaBlend, 12, 0, 3, 2)));
|
||||
InjectRenderData(fx.Manager, secondPart, MakeSortedMesh(
|
||||
Vector3.Zero,
|
||||
MakeBatch(21, TranslucencyKind.AlphaBlend, 21, 0, 3, 3),
|
||||
MakeBatch(22, TranslucencyKind.AlphaBlend, 22, 0, 3, 4)));
|
||||
InjectRenderData(fx.Manager, setup, new ObjectRenderData
|
||||
{
|
||||
IsSetup = true,
|
||||
SetupParts =
|
||||
[
|
||||
(firstPart, Matrix4x4.CreateTranslation(10, 0, 0)),
|
||||
(secondPart, Matrix4x4.CreateTranslation(0, 10, 0)),
|
||||
],
|
||||
});
|
||||
RenderProjectionRecord record = MakeRecord(
|
||||
10, 0, Vector3.Zero, [new MeshRef((uint)setup, Matrix4x4.Identity)]);
|
||||
var alpha = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
||||
|
||||
new WalkStaticStreamPopulator(fx.Dispatcher).PopulateCellObjects(
|
||||
new OrderedDrawStream(),
|
||||
WalkDrawStage.CellStatic,
|
||||
0x8C040112u,
|
||||
[record],
|
||||
0x8C04u,
|
||||
Vector3.Zero,
|
||||
Matrix4x4.Identity,
|
||||
views: null,
|
||||
viewRouteIndex: -1,
|
||||
alpha);
|
||||
|
||||
Assert.Equal([11u, 12u, 21u, 22u], alpha.Select(static batch => batch.Key.FirstIndex));
|
||||
Assert.All(alpha, static batch => Assert.Equal(100f, batch.SortDistanceSq));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PopulateCellObjects_InterleavesStaticAndDynamicOpaquePartsByCypt()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const ulong nearStatic = 0x0100_0C21UL;
|
||||
const ulong farDynamic = 0x0100_0C22UL;
|
||||
const ulong middleStatic = 0x0100_0C23UL;
|
||||
InjectRenderData(fx.Manager, nearStatic, MakeFlatMesh(
|
||||
MakeBatch(1, TranslucencyKind.Opaque, 1, 0, 3, 1)));
|
||||
InjectRenderData(fx.Manager, farDynamic, MakeFlatMesh(
|
||||
MakeBatch(2, TranslucencyKind.Opaque, 2, 0, 3, 2)));
|
||||
InjectRenderData(fx.Manager, middleStatic, MakeFlatMesh(
|
||||
MakeBatch(3, TranslucencyKind.Opaque, 3, 0, 3, 3)));
|
||||
RenderProjectionRecord dynamic = MakeRecord(
|
||||
2, 0, new Vector3(30, 0, 0), [new MeshRef((uint)farDynamic, Matrix4x4.Identity)])
|
||||
with { ProjectionClass = RenderProjectionClass.LiveDynamicRoot };
|
||||
RenderProjectionRecord[] records =
|
||||
[
|
||||
MakeRecord(1, 0, new Vector3(10, 0, 0), [new MeshRef((uint)nearStatic, Matrix4x4.Identity)]),
|
||||
dynamic,
|
||||
MakeRecord(3, 0, new Vector3(20, 0, 0), [new MeshRef((uint)middleStatic, Matrix4x4.Identity)]),
|
||||
];
|
||||
var stream = new OrderedDrawStream();
|
||||
|
||||
new WalkStaticStreamPopulator(fx.Dispatcher).PopulateCellObjects(
|
||||
stream,
|
||||
WalkDrawStage.CellStatic,
|
||||
0x8C040112u,
|
||||
records,
|
||||
0x8C04u,
|
||||
Vector3.Zero,
|
||||
Matrix4x4.Identity,
|
||||
views: null,
|
||||
viewRouteIndex: -1,
|
||||
[]);
|
||||
|
||||
Assert.Equal([2u, 3u, 1u], stream.Keys.Select(static key => key.FirstIndex));
|
||||
Assert.Equal(
|
||||
[WalkDrawStage.Dynamic, WalkDrawStage.CellStatic, WalkDrawStage.CellStatic],
|
||||
stream.Stages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorldAlphaCyptContract_RetainsTheKeyWithoutDeadCameraSubmitThreading()
|
||||
{
|
||||
Type batchType = typeof(WbDrawDispatcher).GetNestedType(
|
||||
"WalkClassifiedBatch", BindingFlags.NonPublic)
|
||||
?? throw new InvalidOperationException("WalkClassifiedBatch was not found.");
|
||||
PropertyInfo sortDistance = batchType.GetProperty("SortDistanceSq")
|
||||
?? throw new InvalidOperationException("SortDistanceSq was not retained.");
|
||||
Assert.Equal(typeof(float), sortDistance.PropertyType);
|
||||
|
||||
MethodInfo submit = typeof(WbDrawDispatcher).GetMethod(
|
||||
"SubmitWalkAlphaInstance", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException("SubmitWalkAlphaInstance was not found.");
|
||||
ParameterInfo[] parameters = submit.GetParameters();
|
||||
Assert.Equal(2, parameters.Length);
|
||||
Assert.Equal("batch", parameters[0].Name);
|
||||
Assert.Equal(typeof(Matrix4x4), parameters[1].ParameterType);
|
||||
Assert.DoesNotContain(parameters, static parameter => parameter.ParameterType == typeof(Vector3));
|
||||
|
||||
string root = FindRepoRoot();
|
||||
string populator = File.ReadAllText(Path.Combine(
|
||||
root, "src", "AcDream.App", "Rendering", "Walk", "WalkStaticStreamPopulator.cs"));
|
||||
Assert.Contains("batch.LocalSortCenter, batch.Transform", populator);
|
||||
Assert.Contains("SortDistanceSq = Vector3.DistanceSquared", populator);
|
||||
Assert.Contains("value.Batch.SortDistanceSq >", populator);
|
||||
Assert.Contains("AP-243", populator);
|
||||
Assert.Contains("greater-than-50 m", populator);
|
||||
Assert.Contains("0x005A06B7..0x005A0720", populator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorldAlphaCyptDocumentationAndRegister_PinPerCellTruthAndThreeResiduals()
|
||||
{
|
||||
string root = FindRepoRoot();
|
||||
string inventory = File.ReadAllText(Path.Combine(
|
||||
root, "docs", "architecture", "worldbuilder-inventory.md"));
|
||||
string architecture = File.ReadAllText(Path.Combine(
|
||||
root, "docs", "architecture", "acdream-architecture.md"));
|
||||
string registerPath = Path.Combine(
|
||||
root, "docs", "architecture", "retail-divergence-register.md");
|
||||
string register = File.ReadAllText(registerPath);
|
||||
|
||||
Assert.Contains("per-cell", inventory, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("two FIFO", inventory, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"distance-sorts one shared queue", inventory, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("per-cell", architecture, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("two FIFO", architecture, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
Assert.Contains("162 active rows", register, StringComparison.Ordinal);
|
||||
int apSectionStart = register.IndexOf(
|
||||
"## 3. Documented approximation (AP)", StringComparison.Ordinal);
|
||||
int apSectionEnd = register.IndexOf(
|
||||
"## 4. Temporary stopgap (TS)", StringComparison.Ordinal);
|
||||
Assert.True(apSectionStart >= 0, "AP section heading must exist.");
|
||||
Assert.True(apSectionEnd > apSectionStart, "TS heading must follow the AP section.");
|
||||
foreach (string id in new[] { "AP-241", "AP-242", "AP-243" })
|
||||
{
|
||||
int rowIndex = register.IndexOf($"| {id} |", StringComparison.Ordinal);
|
||||
Assert.True(
|
||||
rowIndex > apSectionStart && rowIndex < apSectionEnd,
|
||||
$"{id} row must be inside the AP table before the TS section; "
|
||||
+ $"rowIndex={rowIndex}, AP=[{apSectionStart},{apSectionEnd}).");
|
||||
}
|
||||
Assert.Single(File.ReadLines(registerPath), static line =>
|
||||
line.StartsWith("| AP-241 |", StringComparison.Ordinal));
|
||||
Assert.Single(File.ReadLines(registerPath), static line =>
|
||||
line.StartsWith("| AP-242 |", StringComparison.Ordinal));
|
||||
Assert.Single(File.ReadLines(registerPath), static line =>
|
||||
line.StartsWith("| AP-243 |", StringComparison.Ordinal));
|
||||
Assert.Equal(
|
||||
162,
|
||||
File.ReadLines(registerPath).Count(static line =>
|
||||
line.StartsWith("| AP-", StringComparison.Ordinal)));
|
||||
Assert.Contains("greater than 50 m", register, StringComparison.Ordinal);
|
||||
Assert.Contains("0x005A06B7..0x005A0720", register, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
string? directory = AppContext.BaseDirectory;
|
||||
while (directory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory, "AcDream.slnx")))
|
||||
return directory;
|
||||
directory = Directory.GetParent(directory)?.FullName;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Could not locate AcDream.slnx.");
|
||||
}
|
||||
|
||||
// ── Deliverable 1: ClassifyEntityForWalk data equivalence ─────────────
|
||||
|
||||
[Fact]
|
||||
|
|
@ -573,13 +796,12 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
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);
|
||||
fx.Dispatcher.SubmitWalkAlphaInstance(in batch, Matrix4x4.Identity);
|
||||
|
||||
Assert.Equal(1, fx.AlphaQueue.PendingCount);
|
||||
Assert.Equal(1, fx.AlphaQueue.AlphaCount);
|
||||
|
|
@ -614,7 +836,6 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(detailCategory: 1u);
|
||||
on.Dispatcher.SubmitWalkAlphaInstance(
|
||||
in building,
|
||||
Vector3.Zero,
|
||||
Matrix4x4.Identity);
|
||||
|
||||
Assert.Equal(0, on.AlphaQueue!.PendingCount);
|
||||
|
|
@ -636,7 +857,6 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(detailCategory: 1u);
|
||||
off.Dispatcher.SubmitWalkAlphaInstance(
|
||||
in building,
|
||||
Vector3.Zero,
|
||||
Matrix4x4.Identity);
|
||||
|
||||
Assert.Equal(1, off.AlphaQueue!.PendingCount);
|
||||
|
|
@ -656,7 +876,6 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
WbDrawDispatcher.WalkClassifiedBatch batch = AlphaWalkBatch(detailCategory: 0u);
|
||||
ordinary.Dispatcher.SubmitWalkAlphaInstance(
|
||||
in batch,
|
||||
Vector3.Zero,
|
||||
Matrix4x4.Identity);
|
||||
|
||||
Assert.Equal(1, ordinary.AlphaQueue!.PendingCount);
|
||||
|
|
@ -750,11 +969,11 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
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);
|
||||
fx.Dispatcher.SubmitWalkAlphaInstance(in batch, Matrix4x4.Identity);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
fx.Dispatcher.SubmitWalkAlphaInstance(
|
||||
in batch, Vector3.Zero, Matrix4x4.CreateTranslation(1, 0, 0)));
|
||||
in batch, Matrix4x4.CreateTranslation(1, 0, 0)));
|
||||
|
||||
fx.AlphaQueue.AbortFrame();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,8 +218,10 @@ public sealed partial class WalkTraceConformanceTests
|
|||
IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells) { }
|
||||
public bool HasRenderableEmittersInCell(uint cellId) => false;
|
||||
public void DrawCellShell(uint cellId) { }
|
||||
public void DrawStaticParticles(uint cellId) { }
|
||||
public void DrawCellParticles(uint cellId) { }
|
||||
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareStaticParticles(uint cellId) =>
|
||||
ReadOnlySpan<PreparedParticleAlphaSubmission>.Empty;
|
||||
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareCellParticles(uint cellId) =>
|
||||
ReadOnlySpan<PreparedParticleAlphaSubmission>.Empty;
|
||||
public void ClearInteriorDepth() { }
|
||||
public void FlushLandscape() => RecordFlush(RetailAlphaFlushSite.LandscapeFlush, 0f);
|
||||
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) { }
|
||||
|
|
@ -275,6 +277,8 @@ public sealed partial class WalkTraceConformanceTests
|
|||
/// query returns the shared empty record.</summary>
|
||||
private sealed class EmptyAlphaDepthWorldData : IWalkFrameWorldData
|
||||
{
|
||||
public WalkFrameStaticRecords GetCellObjects(uint cellId) => WalkFrameStaticRecords.Empty;
|
||||
public WalkFrameStaticRecords GetOutdoorObjects(uint cellId) => WalkFrameStaticRecords.Empty;
|
||||
public WalkFrameStaticRecords GetCellStatics(uint cellId) => WalkFrameStaticRecords.Empty;
|
||||
public WalkFrameStaticRecords GetCellDynamics(uint cellId) => WalkFrameStaticRecords.Empty;
|
||||
public WalkFrameStaticRecords GetOutdoorStatics(uint cellId) => WalkFrameStaticRecords.Empty;
|
||||
|
|
@ -475,16 +479,14 @@ public sealed partial class WalkTraceConformanceTests
|
|||
/// so every 0f-threshold flush observes count (0, 0) — always a real
|
||||
/// drain (0 is never <c>< 0</c>), matching retail's own always-drains
|
||||
/// behavior at threshold 0f, but with a different count than the
|
||||
/// capture's real content volume; (2) even WITH content, acdream appends
|
||||
/// one queue entry per INSTANCE, where retail's <c>AddMeshToAlphaList</c>
|
||||
/// appends one entry per SUBSET per <c>DrawMesh</c> call — a single
|
||||
/// multi-subset instance inflates retail's count relative to acdream's;
|
||||
/// (3) acdream's CLIP list is structurally empty for ordinary content
|
||||
/// (M2's new register row) — <c>WbDrawDispatcher.IsOpaque</c> filters
|
||||
/// clip-mapped subsets out before they ever reach a submit call, so
|
||||
/// acdream's CLIP count reads 0 wherever retail's capture shows nonzero
|
||||
/// CLIP entries. See the S4 packet's §9/§10/§11 subsections for the
|
||||
/// full per-pose sequences.
|
||||
/// capture's real content volume; (2) AP-238 coalesces transparent
|
||||
/// EnvCell work to one token per <c>(cell,list)</c>, whereas retail
|
||||
/// appends each contributing subset; (3) AP-239/AP-240 change
|
||||
/// CLIP-vs-ALPHA membership for the named ordinary-Wb surface cases.
|
||||
/// Ordinary Wb and particle paths otherwise append per real subset; the
|
||||
/// former broad "per instance versus per subset" explanation was false.
|
||||
/// See the S4 packet's §9/§10/§11 subsections for the full per-pose
|
||||
/// sequences.
|
||||
/// </summary>
|
||||
private void RunAlphaFlushCountsGate(string fixtureName)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue