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>
222 lines
7 KiB
C#
222 lines
7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using AcDream.App.Rendering.Wb;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
internal enum ParticleSubmissionKind
|
|
{
|
|
Billboard,
|
|
Mesh,
|
|
}
|
|
|
|
internal readonly record struct ParticleSubmission(
|
|
ParticleSubmissionKind Kind,
|
|
int DrawIndex,
|
|
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
|
|
/// deterministic tiebreak so a PES chain does not shuffle at equal distance.
|
|
/// </summary>
|
|
internal static class ParticleSubmissionOrdering
|
|
{
|
|
public static void Sort(List<ParticleSubmission> submissions)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(submissions);
|
|
submissions.Sort(static (left, right) =>
|
|
{
|
|
int distance = right.DistanceSq.CompareTo(left.DistanceSq);
|
|
return distance != 0
|
|
? distance
|
|
: left.Sequence.CompareTo(right.Sequence);
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Balances the modern mesh pipeline's reference count against one stable
|
|
/// particle-emitter handle. Registration and teardown are idempotent because
|
|
/// the renderer sees every live particle, not merely every live emitter.
|
|
/// </summary>
|
|
internal sealed class ParticleMeshReferenceTracker : IDisposable
|
|
{
|
|
private sealed class ReferenceState
|
|
{
|
|
public required uint GfxObjId { get; init; }
|
|
public bool Desired { get; set; }
|
|
public bool Held { get; set; }
|
|
public bool Reconciling { get; set; }
|
|
}
|
|
|
|
private readonly Action<uint> _increment;
|
|
private readonly Action<uint> _decrement;
|
|
private readonly Dictionary<int, ReferenceState> _referencesByEmitter = new();
|
|
private bool _disposeRequested;
|
|
private bool _disposed;
|
|
|
|
public ParticleMeshReferenceTracker(Action<uint> increment, Action<uint> decrement)
|
|
{
|
|
_increment = increment ?? throw new ArgumentNullException(nameof(increment));
|
|
_decrement = decrement ?? throw new ArgumentNullException(nameof(decrement));
|
|
}
|
|
|
|
public void Register(int emitterHandle, uint gfxObjId)
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposeRequested, this);
|
|
if (!_referencesByEmitter.TryGetValue(emitterHandle, out ReferenceState? state))
|
|
{
|
|
state = new ReferenceState { GfxObjId = gfxObjId };
|
|
_referencesByEmitter.Add(emitterHandle, state);
|
|
}
|
|
else if (state.GfxObjId != gfxObjId)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Particle emitter {emitterHandle} is already associated with " +
|
|
$"GfxObj 0x{state.GfxObjId:X8}, not 0x{gfxObjId:X8}.");
|
|
}
|
|
|
|
state.Desired = true;
|
|
Reconcile(emitterHandle, state);
|
|
}
|
|
|
|
public void Release(int emitterHandle)
|
|
{
|
|
if (_disposed || !_referencesByEmitter.TryGetValue(emitterHandle, out ReferenceState? state))
|
|
return;
|
|
|
|
state.Desired = false;
|
|
Reconcile(emitterHandle, state);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
_disposeRequested = true;
|
|
List<Exception>? failures = null;
|
|
int[] emitterHandles = [.. _referencesByEmitter.Keys];
|
|
for (int i = 0; i < emitterHandles.Length; i++)
|
|
{
|
|
int emitterHandle = emitterHandles[i];
|
|
if (!_referencesByEmitter.TryGetValue(emitterHandle, out ReferenceState? state))
|
|
continue;
|
|
|
|
state.Desired = false;
|
|
try
|
|
{
|
|
Reconcile(emitterHandle, state);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= []).Add(error);
|
|
}
|
|
}
|
|
|
|
if (_referencesByEmitter.Count == 0)
|
|
_disposed = true;
|
|
|
|
if (failures is not null)
|
|
throw new AggregateException(
|
|
"One or more particle mesh references failed to release.",
|
|
failures);
|
|
}
|
|
|
|
private void Reconcile(int emitterHandle, ReferenceState state)
|
|
{
|
|
if (state.Reconciling)
|
|
return;
|
|
|
|
state.Reconciling = true;
|
|
Exception? failure = null;
|
|
try
|
|
{
|
|
while (state.Desired != state.Held)
|
|
{
|
|
if (state.Desired)
|
|
{
|
|
try
|
|
{
|
|
_increment(state.GfxObjId);
|
|
state.Held = true;
|
|
}
|
|
catch (MeshReferenceMutationException error)
|
|
{
|
|
if (error.MutationCommitted)
|
|
{
|
|
state.Held = true;
|
|
failure ??= error;
|
|
continue;
|
|
}
|
|
failure = error;
|
|
break;
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
failure = error;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
_decrement(state.GfxObjId);
|
|
state.Held = false;
|
|
}
|
|
catch (MeshReferenceMutationException error)
|
|
{
|
|
if (error.MutationCommitted)
|
|
{
|
|
state.Held = false;
|
|
failure ??= error;
|
|
continue;
|
|
}
|
|
failure = error;
|
|
break;
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
failure = error;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
state.Reconciling = false;
|
|
if (!state.Desired && !state.Held)
|
|
_referencesByEmitter.Remove(emitterHandle);
|
|
if (_disposeRequested && _referencesByEmitter.Count == 0)
|
|
_disposed = true;
|
|
}
|
|
|
|
if (failure is not null)
|
|
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(failure).Throw();
|
|
}
|
|
}
|