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
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue