fix(rendering): port retail shared alpha list
Queue translucent world GfxObj batches and scene particles in one stable far-to-near stream using transformed DAT sort centers, then drain it at retail's landscape and final-world boundaries. Preserve authored blend, cull, lighting, opacity, and adjacent-only batching so particles behind lifestones are composited through the crystal instead of overpainting it. Release build succeeds and all 5,914 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
ec1bb19609
commit
6b0472ee32
14 changed files with 1083 additions and 34 deletions
|
|
@ -43,8 +43,10 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// each group becomes one <c>DrawElementsIndirectCommand</c>. Three GPU buffers
|
||||
/// are uploaded per frame: instance matrices (SSBO binding 0), per-group batch
|
||||
/// metadata/texture handles (SSBO binding 1), and the indirect draw commands.
|
||||
/// Two <c>glMultiDrawElementsIndirect</c> calls cover the opaque and transparent
|
||||
/// passes respectively — one GL call per pass regardless of group count.
|
||||
/// 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.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -88,6 +90,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
private readonly EntitySpawnAdapter _entitySpawnAdapter;
|
||||
private readonly IRetailSelectionRenderSink? _selectionSink;
|
||||
private readonly IRetailSelectionLightingSource? _selectionLighting;
|
||||
private readonly RetailAlphaQueue? _alphaQueue;
|
||||
private readonly AlphaDrawSource _alphaSource;
|
||||
|
||||
private readonly BindlessSupport _bindless;
|
||||
|
||||
|
|
@ -240,10 +244,43 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
public uint Flags;
|
||||
}
|
||||
|
||||
private readonly record struct DeferredAlphaInstance(
|
||||
GroupKey Key,
|
||||
Matrix4x4 Model,
|
||||
uint ClipSlot,
|
||||
AlphaLightSet Lights,
|
||||
uint Indoor,
|
||||
float Opacity,
|
||||
Vector2 SelectionLighting);
|
||||
|
||||
private readonly record struct AlphaLightSet(
|
||||
int L0, int L1, int L2, int L3,
|
||||
int L4, int L5, int L6, int L7)
|
||||
{
|
||||
public int this[int index] => index switch
|
||||
{
|
||||
0 => L0, 1 => L1, 2 => L2, 3 => L3,
|
||||
4 => L4, 5 => L5, 6 => L6, 7 => L7,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class AlphaDrawSource(WbDrawDispatcher owner) : IRetailAlphaDrawSource
|
||||
{
|
||||
public void DrawAlphaBatch(ReadOnlySpan<int> tokens)
|
||||
=> owner.DrawDeferredAlphaBatch(tokens);
|
||||
|
||||
public void ResetAlphaSubmissions()
|
||||
=> owner._deferredAlpha.Clear();
|
||||
}
|
||||
|
||||
// Per-frame scratch — reused across frames to avoid per-frame allocation.
|
||||
private readonly Dictionary<GroupKey, InstanceGroup> _groups = new();
|
||||
private readonly List<InstanceGroup> _opaqueDraws = new();
|
||||
private readonly List<InstanceGroup> _translucentDraws = new();
|
||||
private readonly List<DeferredAlphaInstance> _deferredAlpha = new(128);
|
||||
private TranslucencyKind[] _deferredAlphaKinds = new TranslucencyKind[128];
|
||||
private Matrix4x4 _deferredAlphaViewProjection;
|
||||
// A.5 T26 follow-up (Bug B): WalkEntities populates this scratch list
|
||||
// instead of allocating a fresh List<(WorldEntity, int)> per frame. At
|
||||
// ~10K entities × ~3 mesh refs = ~30K tuples × 16 bytes = ~480 KB / frame
|
||||
|
|
@ -363,7 +400,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
BindlessSupport bindless,
|
||||
EntityClassificationCache classificationCache,
|
||||
AcDream.Core.Rendering.TranslucencyFadeManager translucencyFades,
|
||||
IRetailSelectionRenderSink? selectionSink = null)
|
||||
IRetailSelectionRenderSink? selectionSink = null,
|
||||
RetailAlphaQueue? alphaQueue = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentNullException.ThrowIfNull(shader);
|
||||
|
|
@ -382,6 +420,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_translucencyFades = translucencyFades;
|
||||
_selectionSink = selectionSink;
|
||||
_selectionLighting = selectionSink as IRetailSelectionLightingSource;
|
||||
_alphaQueue = alphaQueue;
|
||||
_alphaSource = new AlphaDrawSource(this);
|
||||
|
||||
_bindless = bindless ?? throw new ArgumentNullException(nameof(bindless));
|
||||
_instanceSsbo = _gl.GenBuffer();
|
||||
|
|
@ -1579,12 +1619,23 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// stable 1k-draw live scene into hundreds of tiny MDI runs after a
|
||||
// landblock transition, which shows up as a GPU-command bottleneck
|
||||
// without a triangle-count spike.
|
||||
// Retail particles and ordinary object parts share CPartCell's
|
||||
// CShadowPart list before delayed alpha is flushed. During the world
|
||||
// alpha frame, preserve each transparent instance as an independent
|
||||
// submission so the shared queue can interleave it with particles.
|
||||
// Immediate mode remains for sealed off-screen consumers such as the
|
||||
// paperdoll and UI Studio render stack.
|
||||
bool deferTransparent = _alphaQueue?.IsCollecting == true;
|
||||
_opaqueDraws.Sort(CompareOpaqueSubmissionOrder);
|
||||
_translucentDraws.Sort(CompareTransparentSubmissionOrder);
|
||||
if (deferTransparent)
|
||||
DeferTransparentGroups(camPos, vp);
|
||||
else
|
||||
_translucentDraws.Sort(CompareTransparentSubmissionOrder);
|
||||
|
||||
// ── Phase 4: build IndirectGroupInput list (opaque sorted, then translucent),
|
||||
// fill via BuildIndirectArrays ──────────────────────────────────
|
||||
int totalDraws = _opaqueDraws.Count + _translucentDraws.Count;
|
||||
int immediateTransparentCount = deferTransparent ? 0 : _translucentDraws.Count;
|
||||
int totalDraws = _opaqueDraws.Count + immediateTransparentCount;
|
||||
if (_batchData.Length < totalDraws)
|
||||
_batchData = new BatchData[totalDraws + 64];
|
||||
if (_indirectCommands.Length < totalDraws)
|
||||
|
|
@ -1594,7 +1645,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
|
||||
var groupInputs = new List<IndirectGroupInput>(totalDraws);
|
||||
foreach (var g in _opaqueDraws) groupInputs.Add(ToInput(g));
|
||||
foreach (var g in _translucentDraws) groupInputs.Add(ToInput(g));
|
||||
if (!deferTransparent)
|
||||
foreach (var g in _translucentDraws) groupInputs.Add(ToInput(g));
|
||||
|
||||
// Cast _batchData (private BatchData) to public-mirror BatchDataPublic for BuildIndirectArrays.
|
||||
// Layout is asserted at test time (BatchDataPublic_LayoutMatchesPrivateBatchData test).
|
||||
|
|
@ -1907,6 +1959,200 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
Translucency: g.Translucency,
|
||||
CullMode: g.CullMode);
|
||||
|
||||
private static GroupKey ToKey(InstanceGroup g) => new(
|
||||
g.Ibo,
|
||||
g.FirstIndex,
|
||||
g.BaseVertex,
|
||||
g.IndexCount,
|
||||
g.BindlessTextureHandle,
|
||||
g.TextureLayer,
|
||||
g.Translucency,
|
||||
g.CullMode);
|
||||
|
||||
private void DeferTransparentGroups(Vector3 cameraWorldPosition, Matrix4x4 viewProjection)
|
||||
{
|
||||
RetailAlphaQueue queue = _alphaQueue!;
|
||||
if (_deferredAlpha.Count == 0)
|
||||
_deferredAlphaViewProjection = viewProjection;
|
||||
else if (_deferredAlphaViewProjection != viewProjection)
|
||||
throw new InvalidOperationException(
|
||||
"One retail alpha scope cannot combine different view-projection matrices.");
|
||||
|
||||
foreach (InstanceGroup group in _translucentDraws)
|
||||
{
|
||||
GroupKey key = ToKey(group);
|
||||
for (int i = 0; i < group.Matrices.Count; i++)
|
||||
{
|
||||
Matrix4x4 model = group.Matrices[i];
|
||||
Vector3 localSortCenter = group.LocalSortCenters[i];
|
||||
float viewerDistance = RetailAlphaOrdering.ComputeViewerDistance(
|
||||
localSortCenter,
|
||||
model,
|
||||
cameraWorldPosition);
|
||||
int lightOffset = i * LightManager.MaxLightsPerObject;
|
||||
var lights = new AlphaLightSet(
|
||||
group.LightSets[lightOffset + 0], group.LightSets[lightOffset + 1],
|
||||
group.LightSets[lightOffset + 2], group.LightSets[lightOffset + 3],
|
||||
group.LightSets[lightOffset + 4], group.LightSets[lightOffset + 5],
|
||||
group.LightSets[lightOffset + 6], group.LightSets[lightOffset + 7]);
|
||||
|
||||
int token = _deferredAlpha.Count;
|
||||
_deferredAlpha.Add(new DeferredAlphaInstance(
|
||||
key,
|
||||
model,
|
||||
group.Slots[i],
|
||||
lights,
|
||||
group.IndoorFlags[i],
|
||||
group.Opacities[i],
|
||||
group.SelectionLighting[i]));
|
||||
queue.Submit(_alphaSource, token, viewerDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDeferredAlphaBatch(ReadOnlySpan<int> tokens)
|
||||
{
|
||||
if (tokens.Length == 0)
|
||||
return;
|
||||
|
||||
GlobalMeshBuffer? global = _meshAdapter.MeshManager?.GlobalBuffer;
|
||||
if (global is null || global.VAO == 0)
|
||||
return;
|
||||
|
||||
int count = tokens.Length;
|
||||
EnsureDeferredAlphaCapacity(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
DeferredAlphaInstance entry = _deferredAlpha[tokens[i]];
|
||||
WriteMatrix(_instanceData, i * 16, entry.Model);
|
||||
_clipSlotData[i] = entry.ClipSlot;
|
||||
_indoorData[i] = entry.Indoor;
|
||||
_alphaData[i] = entry.Opacity;
|
||||
_selectionLightingData[i] = entry.SelectionLighting;
|
||||
int lightOffset = i * LightManager.MaxLightsPerObject;
|
||||
for (int light = 0; light < LightManager.MaxLightsPerObject; light++)
|
||||
_lightSetData[lightOffset + light] = entry.Lights[light];
|
||||
|
||||
GroupKey key = entry.Key;
|
||||
_batchData[i] = new BatchData
|
||||
{
|
||||
TextureHandle = key.BindlessTextureHandle,
|
||||
TextureLayer = key.TextureLayer,
|
||||
Flags = 0,
|
||||
};
|
||||
_indirectCommands[i] = new DrawElementsIndirectCommand
|
||||
{
|
||||
Count = (uint)key.IndexCount,
|
||||
InstanceCount = 1,
|
||||
FirstIndex = key.FirstIndex,
|
||||
BaseVertex = key.BaseVertex,
|
||||
BaseInstance = (uint)i,
|
||||
};
|
||||
_drawCullModes[i] = key.CullMode;
|
||||
_deferredAlphaKinds[i] = key.Translucency;
|
||||
}
|
||||
|
||||
UploadDeferredAlphaBuffers(count);
|
||||
|
||||
_shader.Use();
|
||||
_shader.SetMatrix4("uViewProjection", _deferredAlphaViewProjection);
|
||||
_shader.SetInt("uLightingMode", 0);
|
||||
_shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode);
|
||||
_shader.SetInt("uRenderPass", 1);
|
||||
BindClipRegionBinding2();
|
||||
_gl.BindVertexArray(global.VAO);
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
_gl.Enable(EnableCap.Blend);
|
||||
_gl.DepthMask(false);
|
||||
_gl.FrontFace(FrontFaceDirection.CW);
|
||||
|
||||
int runStart = 0;
|
||||
while (runStart < count)
|
||||
{
|
||||
TranslucencyKind blend = _deferredAlphaKinds[runStart];
|
||||
int runEnd = runStart + 1;
|
||||
while (runEnd < count && _deferredAlphaKinds[runEnd] == blend)
|
||||
runEnd++;
|
||||
|
||||
ApplyRetailBlend(blend);
|
||||
DrawIndirectRange(runStart, runEnd - runStart);
|
||||
runStart = runEnd;
|
||||
}
|
||||
|
||||
_gl.DepthMask(true);
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
private void EnsureDeferredAlphaCapacity(int count)
|
||||
{
|
||||
int neededMatrixFloats = count * 16;
|
||||
if (_instanceData.Length < neededMatrixFloats)
|
||||
_instanceData = new float[neededMatrixFloats + 256 * 16];
|
||||
if (_clipSlotData.Length < count)
|
||||
_clipSlotData = new uint[count + 256];
|
||||
if (_indoorData.Length < count)
|
||||
_indoorData = new uint[count + 256];
|
||||
if (_alphaData.Length < count)
|
||||
_alphaData = new float[count + 256];
|
||||
if (_selectionLightingData.Length < count)
|
||||
_selectionLightingData = new Vector2[count + 256];
|
||||
if (_lightSetData.Length < count * LightManager.MaxLightsPerObject)
|
||||
_lightSetData = new int[(count + 256) * LightManager.MaxLightsPerObject];
|
||||
if (_batchData.Length < count)
|
||||
_batchData = new BatchData[count + 64];
|
||||
if (_indirectCommands.Length < count)
|
||||
_indirectCommands = new DrawElementsIndirectCommand[count + 64];
|
||||
if (_drawCullModes.Length < count)
|
||||
_drawCullModes = new CullMode[count + 64];
|
||||
if (_deferredAlphaKinds.Length < count)
|
||||
_deferredAlphaKinds = new TranslucencyKind[count + 64];
|
||||
}
|
||||
|
||||
private void UploadDeferredAlphaBuffers(int count)
|
||||
{
|
||||
fixed (float* p = _instanceData)
|
||||
UploadSsbo(_instanceSsbo, 0, p, count * 16 * sizeof(float));
|
||||
fixed (BatchData* p = _batchData)
|
||||
UploadSsbo(_batchSsbo, 1, p, count * sizeof(BatchData));
|
||||
fixed (uint* p = _clipSlotData)
|
||||
UploadSsbo(_clipSlotSsbo, 3, p, count * sizeof(uint));
|
||||
fixed (int* p = _lightSetData)
|
||||
UploadSsbo(_instLightSetSsbo, 5, p, count * LightManager.MaxLightsPerObject * sizeof(int));
|
||||
fixed (uint* p = _indoorData)
|
||||
UploadSsbo(_instIndoorSsbo, 6, p, count * sizeof(uint));
|
||||
fixed (float* p = _alphaData)
|
||||
UploadSsbo(_instAlphaSsbo, 7, p, count * sizeof(float));
|
||||
fixed (Vector2* p = _selectionLightingData)
|
||||
UploadSsbo(_instSelectionLightingSsbo, 8, p, count * sizeof(float) * 2);
|
||||
UploadGlobalLights();
|
||||
|
||||
fixed (DrawElementsIndirectCommand* p = _indirectCommands)
|
||||
{
|
||||
_gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer);
|
||||
_gl.BufferData(
|
||||
BufferTargetARB.DrawIndirectBuffer,
|
||||
(nuint)(count * sizeof(DrawElementsIndirectCommand)),
|
||||
p,
|
||||
BufferUsageARB.DynamicDraw);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRetailBlend(TranslucencyKind blend)
|
||||
{
|
||||
_gl.BlendFunc(
|
||||
blend == TranslucencyKind.InvAlpha
|
||||
? BlendingFactor.OneMinusSrcAlpha
|
||||
: BlendingFactor.SrcAlpha,
|
||||
blend switch
|
||||
{
|
||||
TranslucencyKind.Additive => BlendingFactor.One,
|
||||
TranslucencyKind.InvAlpha => BlendingFactor.SrcAlpha,
|
||||
_ => BlendingFactor.OneMinusSrcAlpha,
|
||||
});
|
||||
}
|
||||
|
||||
private static int CompareOpaqueSubmissionOrder(InstanceGroup a, InstanceGroup b)
|
||||
{
|
||||
int cull = a.CullMode.CompareTo(b.CullMode);
|
||||
|
|
@ -2117,11 +2363,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
internal static void ApplyCacheHit(
|
||||
EntityCacheEntry entry,
|
||||
Matrix4x4 entityWorld,
|
||||
Action<GroupKey, Matrix4x4> appendInstance)
|
||||
Action<GroupKey, Matrix4x4, Vector3> appendInstance)
|
||||
{
|
||||
foreach (var cached in entry.Batches)
|
||||
{
|
||||
appendInstance(cached.Key, cached.RestPose * entityWorld);
|
||||
appendInstance(cached.Key, cached.RestPose * entityWorld, cached.LocalSortCenter);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2188,7 +2434,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
/// creates an <see cref="InstanceGroup"/> for the given key in
|
||||
/// <c>_groups</c> and appends the per-instance world matrix.
|
||||
/// </summary>
|
||||
private void AppendInstanceToGroup(GroupKey key, Matrix4x4 model)
|
||||
private void AppendInstanceToGroup(GroupKey key, Matrix4x4 model, Vector3 localSortCenter)
|
||||
{
|
||||
if (!_groups.TryGetValue(key, out var grp))
|
||||
{
|
||||
|
|
@ -2206,6 +2452,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_groups[key] = grp;
|
||||
}
|
||||
grp.Matrices.Add(model);
|
||||
grp.LocalSortCenters.Add(localSortCenter);
|
||||
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
|
||||
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
|
||||
// #188: cache-hit entities are always non-animated (the Tier-1 cache
|
||||
|
|
@ -2392,11 +2639,12 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_groups[key] = grp;
|
||||
}
|
||||
grp.Matrices.Add(model);
|
||||
grp.LocalSortCenters.Add(renderData.SortCenter);
|
||||
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
|
||||
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
|
||||
grp.Opacities.Add(opacityMultiplier); // #188 — parallel to Matrices
|
||||
grp.SelectionLighting.Add(_currentEntitySelectionLighting);
|
||||
collector?.Add(new CachedBatch(key, texHandle, restPose));
|
||||
collector?.Add(new CachedBatch(key, texHandle, restPose, renderData.SortCenter));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2659,6 +2907,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
public float SortDistance; // squared distance from camera to first instance, for opaque sort
|
||||
public readonly List<Matrix4x4> Matrices = new();
|
||||
|
||||
// Retail CPhysicsPart::CYpt uses the transformed GfxObj sort center,
|
||||
// not the entity origin. Parallel to Matrices so delayed-alpha
|
||||
// submissions retain the exact per-part key after material grouping.
|
||||
public readonly List<Vector3> LocalSortCenters = new();
|
||||
|
||||
// Phase U.4: per-instance clip-slot index, parallel to Matrices (Slots[i]
|
||||
// is the binding=2 CellClip slot for the instance whose matrix is
|
||||
// Matrices[i]). At layout time the dispatcher writes Slots[i] into
|
||||
|
|
@ -2704,6 +2957,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
public void ClearPerInstanceData()
|
||||
{
|
||||
Matrices.Clear();
|
||||
LocalSortCenters.Clear();
|
||||
Slots.Clear();
|
||||
LightSets.Clear();
|
||||
IndoorFlags.Clear();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue