fix(render): complete S4 chunk 2 final alpha parity round
Final allowed fix round for S4-c2 on cc8e5677a. This lands every item in the campaign packet section 12 without adding a flush site, shader, distance, overflow recovery draw, or graphical-client run. R2-1 particle row 5: ParticleRenderer now constructs and owns the actual particle-mesh-opaque pipeline using the existing particle_mesh shaders and layout, Blend=None, depth test/write enabled with WorldCompare, dynamic per-batch cull, clockwise front face, and no alpha-to-coverage. The production dispatch selects it for an opaque-classified mesh particle whose clamped material alpha is 1.0. Nonopaque mesh pipelines remain depth-write-off. The production route keeps cached reserve/immediate delegates and the warmed append/immediate paths allocate 0 B. R2-2 EnvCell exact per-subset routing: ObjectMeshManager carries Content's TextureBatchData.RetailSurfaceMask onto ObjectRenderBatch at the real upload boundary. EnvCellRenderer scans the active prepared cell snapshot and feeds each real transparent batch's exact mask through RetailAlphaMeshRouter. Pure 0x08 Base1ClipMap reaches CLIP, 0x02 alpha-family reaches ALPHA, and table Immediate subsets draw at the cell turn. Separate fixed-route draw sources coalesce to at most one token per (cell,list) and filtered replay draws only that list's subsets; a mixed cell contributes to both lists without duplicate replay. Detail-on routes eligible subsets immediately with the detail pass. The warmed dispatch/source allocation pins measure 0 B; the production scan/filter is covered behaviorally and uses only retained scratch/enumerators (static allocation audit). R2-3 capacity cleanup: RetailAlphaQueue registers a source before the 3,000 entry capacity return. A source whose first append is rejected is therefore reset by flush, EndFrame, or abort, but its rejected payload is never prepared or drawn. R2-4 production proof and prose: both actual Wb submit sites are exercised; the particle tests call the production dispatcher and inspect the constructed owner's production pipeline/selector; EnvCell tests upload real Content batch masks through ObjectMeshManager and drain real filtered MDI calls. The A1 positive proof executes WorldSceneRenderer's real outdoor frame owner through RetailPViewRenderer.DrawInside and RetailAlphaQueue.EndFrame and observes [DrawBuilding x N, RenderNormalMode] with no LandscapeFlush. The packet and register now state the varying retail first-for-list truth and the exact per-subset EnvCell/AP-238, visible AP-239 compositing, and AP-240 feeder scope. Physical active register counts remain AP=159 and AD=92. Final clean-state gates (actual output): - Release solution build: Build succeeded; 0 Warning(s); 0 Error(s). - Hermetic solution filter: every project green, 16,728 passed / 0 failed / 0 skipped total; AcDream.App.Tests 6,875/6,875. - InstalledDat: 255 passed / 10 failed / 1 skipped / 266 total, exactly the allowed identities: TowerAscent_StaircaseStaysConeVisible_EveryStep; MainGameUiAndChatInput_MediaBearingChildrenNowBuildAsRealWidgets (#383); EveryAuthoredInvisibleWidget_StartsHiddenAcrossAllLayouts (#383); Oh_doorway_still_first_frame_diff (#458); and the six AlphaFlushCounts_{CathedralArrival,CathedralLeak,CathedralStairArch, FoundryDeep,HoltburgDoorwayStill,TerraceEdge}_MatchesRetailFrame2. All six AlphaFlushSites_* pass in the same lane. - VulkanShaderDescriptorContractTests + VulkanShaderManifestTests + RenderPackSpirvValidatorTests: 32 passed / 0 failed / 0 skipped. - Corrected queue/router/walk/driver/particle/Wb/EnvCell/PView production filter: 211 passed / 0 failed / 0 skipped. - Explicit warmed production allocation pins: 2 passed; both measure 0 B. - Register: physical AP-238/AP-239/AP-240/AD-120/~~AP-34~~ rows each count exactly 1; active physical rows AP=159 and AD=92. - git diff --check: PASS. Production mutation checks (each applied, run to the named first failure, and exactly reversed before the final gates): 1. Restoring the particle row-5 throw fails OpaqueClassifiedMeshBatch_WithNoMaterialAlpha_DrawsImmediateOnOpaqueDepthState first with InvalidOperationException: mutation: row 5 unreachable. 2. Deleting row 5's immediate callback fails that same production-dispatch test's first collection assertion: expected [(Mesh, 11, True)], actual []. 3. Constructing the actual owner pipeline with depthWrite:false fails ImmediateOpaqueMesh_UsesProductionParticleMeshOpaquePipelineDescription first at Assert.True(description.Depth.Write): expected true, actual false. 4. Mapping the production selector back to _meshAlphaPipeline fails that same test first at Assert.Same: expected particle-mesh-opaque, actual particle-mesh-alpha. 5. Dropping RetailSurfaceMask at the real ObjectMeshManager upload boundary fails the mixed-cell production scan first: expected Clip | Alpha, actual Immediate. 6. Hardcoding the uploaded EnvCell scan to MaskAlphaFamily fails the mixed-cell production scan first: expected Clip | Alpha, actual Alpha; the pure-mask pin also reports expected Clip, actual Alpha. 7. Inverting the production detail predicate fails the detail-on production pin first: expected Immediate, actual Clip. 8. Removing both EnvCell replay filters fails the mixed production drain's real MDI assertions: each call expected DrawCount 1, actual DrawCount 2. 9. Moving RegisterSource below the full-capacity return fails every RejectedFirstUseSource_IsCleanedWithoutPrepareOrDraw row (flush, EndFrame, abort) at the first ResetCount assertion: expected 1, actual 0. 10. Hardcoding detailSurfaceActive=false at the real Wb dispatch fails both production submit-site tests at their first queue-count assertion: expected 0, actual 1. 11. Restoring RetailPViewRenderer's removed outdoor LandscapeFlush call fails OutdoorProductionPView_DrainsBuildingThenRenderNormalModeWithoutLandscapeFlush first at the real drain sequence: expected [DrawBuilding, RenderNormalMode], actual [DrawBuilding, LandscapeFlush, RenderNormalMode]. 12. Deleting WorldSceneRenderer's final EndFrame owner drain fails that same full-path A1 test first: expected [DrawBuilding, RenderNormalMode], actual [DrawBuilding]. Not done/deferred: none. No retail conflict or infeasible contract item was found. No graphical client was launched. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
9ccb61a8ec
commit
0aa166aa09
14 changed files with 1779 additions and 224 deletions
|
|
@ -27,8 +27,8 @@ namespace AcDream.App.Rendering;
|
|||
/// natively and at no cost.</para>
|
||||
///
|
||||
/// <para><b>What differs from the GL arm, and why.</b> The imperative
|
||||
/// <c>glBlendFunc</c> switch becomes five PIPELINES (two billboard blends, three
|
||||
/// mesh blends) because core Vulkan 1.3 does not make blend dynamic; the
|
||||
/// <c>glBlendFunc</c> switch becomes six PIPELINES (two billboard blends,
|
||||
/// opaque plus three mesh blends) because core Vulkan 1.3 does not make blend dynamic; the
|
||||
/// per-flight VAO/VBO pool disappears because every ring allocation inside a
|
||||
/// frame is already distinct memory that lives until the frame retires; the
|
||||
/// binding-9 texture table is not bound at all, because the device owns the
|
||||
|
|
@ -49,6 +49,7 @@ public sealed unsafe partial class ParticleRenderer
|
|||
|
||||
private IGpuPipeline? _billboardAlphaPipeline;
|
||||
private IGpuPipeline? _billboardAdditivePipeline;
|
||||
private IGpuPipeline? _meshOpaquePipeline;
|
||||
private IGpuPipeline? _meshAlphaPipeline;
|
||||
private IGpuPipeline? _meshAdditivePipeline;
|
||||
private IGpuPipeline? _meshInversePipeline;
|
||||
|
|
@ -154,6 +155,8 @@ public sealed unsafe partial class ParticleRenderer
|
|||
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
|
||||
_alphaQueue = alphaQueue;
|
||||
_alphaSource = new AlphaDrawSource(this);
|
||||
_reserveDeferredParticleDraw = ReserveDispatchDeferredParticle;
|
||||
_drawImmediateParticle = DrawImmediateParticleSubmissionRhi;
|
||||
long scratchBudget = alphaScratchBudgetBytes
|
||||
?? AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create(
|
||||
AcDream.App.Rendering.Residency.ResidencyBudgetOptions.Default.AlphaScratchBytes)
|
||||
|
|
@ -219,12 +222,14 @@ public sealed unsafe partial class ParticleRenderer
|
|||
if (_meshAdapter?.MeshManager?.GlobalBuffer is null)
|
||||
return;
|
||||
|
||||
_meshOpaquePipeline = CreateMeshParticlePipeline(
|
||||
device, "particle-mesh-opaque", GpuBlendMode.None, depthWrite: true, sampleCount);
|
||||
_meshAlphaPipeline = CreateMeshParticlePipeline(
|
||||
device, "particle-mesh-alpha", GpuBlendMode.StraightAlpha, sampleCount);
|
||||
device, "particle-mesh-alpha", GpuBlendMode.StraightAlpha, depthWrite: false, sampleCount);
|
||||
_meshAdditivePipeline = CreateMeshParticlePipeline(
|
||||
device, "particle-mesh-additive", GpuBlendMode.Additive, sampleCount);
|
||||
device, "particle-mesh-additive", GpuBlendMode.Additive, depthWrite: false, sampleCount);
|
||||
_meshInversePipeline = CreateMeshParticlePipeline(
|
||||
device, "particle-mesh-inverse", GpuBlendMode.InverseAlpha, sampleCount);
|
||||
device, "particle-mesh-inverse", GpuBlendMode.InverseAlpha, depthWrite: false, sampleCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -273,6 +278,7 @@ public sealed unsafe partial class ParticleRenderer
|
|||
IGpuDevice device,
|
||||
string name,
|
||||
GpuBlendMode blend,
|
||||
bool depthWrite,
|
||||
int sampleCount) =>
|
||||
device.CreatePipeline(new GpuPipelineDescription
|
||||
{
|
||||
|
|
@ -281,7 +287,7 @@ public sealed unsafe partial class ParticleRenderer
|
|||
VertexLayout = MeshVertexLayout,
|
||||
Topology = GpuPrimitiveTopology.TriangleList,
|
||||
Blend = blend,
|
||||
Depth = new GpuDepthState(Test: true, Write: false, WorldDepthContract.WorldCompare),
|
||||
Depth = new GpuDepthState(Test: true, Write: depthWrite, WorldDepthContract.WorldCompare),
|
||||
Cull = GpuCullMode.None,
|
||||
FrontFace = GpuFrontFace.Clockwise,
|
||||
AlphaToCoverage = false,
|
||||
|
|
@ -366,7 +372,8 @@ public sealed unsafe partial class ParticleRenderer
|
|||
instances.Buffer,
|
||||
instances.OffsetBytes,
|
||||
(uint)_meshRunScratch.Count,
|
||||
firstInstance: 0);
|
||||
firstInstance: 0,
|
||||
opaqueDepthState: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -383,7 +390,8 @@ public sealed unsafe partial class ParticleRenderer
|
|||
private void DrawImmediateParticleSubmissionRhi(
|
||||
Matrix4x4 viewProjection,
|
||||
ParticleSubmissionKind kind,
|
||||
int drawIndex)
|
||||
int drawIndex,
|
||||
bool opaqueDepthState)
|
||||
{
|
||||
IGpuPassEncoder encoder = _scope!.RequireEncoder();
|
||||
IGpuFrame frame = RequireRhiFrame();
|
||||
|
|
@ -417,7 +425,8 @@ public sealed unsafe partial class ParticleRenderer
|
|||
instances.Buffer,
|
||||
instances.OffsetBytes,
|
||||
instanceCount: 1,
|
||||
firstInstance: 0);
|
||||
firstInstance: 0,
|
||||
opaqueDepthState: opaqueDepthState);
|
||||
}
|
||||
|
||||
private void DrawInstancesRhi(
|
||||
|
|
@ -502,12 +511,13 @@ public sealed unsafe partial class ParticleRenderer
|
|||
IGpuBuffer instanceBuffer,
|
||||
uint instanceOffsetBytes,
|
||||
uint instanceCount,
|
||||
uint firstInstance)
|
||||
uint firstInstance,
|
||||
bool opaqueDepthState = false)
|
||||
{
|
||||
if (instanceCount == 0)
|
||||
return;
|
||||
|
||||
encoder.BindPipeline(PipelineForMeshBlend(ResolveMeshBlend(batch)));
|
||||
encoder.BindPipeline(PipelineForMeshBlend(ResolveMeshBlend(batch), opaqueDepthState));
|
||||
encoder.SetPushConstants(new GpuPushConstants
|
||||
{
|
||||
ViewProjection = viewProjection,
|
||||
|
|
@ -550,12 +560,41 @@ public sealed unsafe partial class ParticleRenderer
|
|||
firstInstance);
|
||||
}
|
||||
|
||||
private IGpuPipeline PipelineForMeshBlend(TranslucencyKind blend) => blend switch
|
||||
internal readonly record struct MeshParticlePipelineState(
|
||||
GpuBlendMode Blend,
|
||||
GpuDepthState Depth);
|
||||
|
||||
internal static MeshParticlePipelineState ResolveMeshParticlePipelineState(
|
||||
TranslucencyKind blend,
|
||||
bool opaqueDepthState) =>
|
||||
opaqueDepthState
|
||||
? new MeshParticlePipelineState(
|
||||
GpuBlendMode.None,
|
||||
new GpuDepthState(Test: true, Write: true, WorldDepthContract.WorldCompare))
|
||||
: new MeshParticlePipelineState(
|
||||
blend switch
|
||||
{
|
||||
TranslucencyKind.Additive => GpuBlendMode.Additive,
|
||||
TranslucencyKind.InvAlpha => GpuBlendMode.InverseAlpha,
|
||||
_ => GpuBlendMode.StraightAlpha,
|
||||
},
|
||||
new GpuDepthState(Test: true, Write: false, WorldDepthContract.WorldCompare));
|
||||
|
||||
private IGpuPipeline PipelineForMeshBlend(
|
||||
TranslucencyKind blend,
|
||||
bool opaqueDepthState)
|
||||
{
|
||||
TranslucencyKind.Additive => _meshAdditivePipeline!,
|
||||
TranslucencyKind.InvAlpha => _meshInversePipeline!,
|
||||
_ => _meshAlphaPipeline!,
|
||||
};
|
||||
MeshParticlePipelineState state = ResolveMeshParticlePipelineState(
|
||||
blend,
|
||||
opaqueDepthState);
|
||||
return state.Blend switch
|
||||
{
|
||||
GpuBlendMode.None => _meshOpaquePipeline!,
|
||||
GpuBlendMode.Additive => _meshAdditivePipeline!,
|
||||
GpuBlendMode.InverseAlpha => _meshInversePipeline!,
|
||||
_ => _meshAlphaPipeline!,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The RHI form of <see cref="ApplyMeshCullMode"/>. <c>FrontFace</c> is
|
||||
|
|
@ -708,7 +747,8 @@ public sealed unsafe partial class ParticleRenderer
|
|||
meshInstances,
|
||||
_preparedMeshInstances.OffsetBytes,
|
||||
(uint)(i - meshRunStart),
|
||||
meshBaseInstance);
|
||||
meshBaseInstance,
|
||||
opaqueDepthState: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -756,6 +796,8 @@ public sealed unsafe partial class ParticleRenderer
|
|||
_billboardAlphaPipeline = null;
|
||||
Attempt(() => _billboardAdditivePipeline?.Dispose());
|
||||
_billboardAdditivePipeline = null;
|
||||
Attempt(() => _meshOpaquePipeline?.Dispose());
|
||||
_meshOpaquePipeline = null;
|
||||
Attempt(() => _meshAlphaPipeline?.Dispose());
|
||||
_meshAlphaPipeline = null;
|
||||
Attempt(() => _meshAdditivePipeline?.Dispose());
|
||||
|
|
|
|||
|
|
@ -121,6 +121,9 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
private readonly ParticleSystem _particles;
|
||||
private readonly RetailAlphaQueue? _alphaQueue;
|
||||
private readonly AlphaDrawSource _alphaSource;
|
||||
private readonly ReserveDeferredParticleDraw _reserveDeferredParticleDraw;
|
||||
private readonly DrawImmediateParticle _drawImmediateParticle;
|
||||
private DeferredParticleDraw _dispatchDeferredParticle;
|
||||
private readonly Dictionary<uint, ParticleGfxInfo> _particleGfxInfoByGfxObj = new();
|
||||
private readonly Dictionary<int, ParticleGfxInfo> _particleGfxInfoByEmitter = new();
|
||||
private readonly Dictionary<uint, RetailParticleGeometryKind> _geometryKindByGfxObj = new();
|
||||
|
|
@ -185,6 +188,14 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
=> owner.ResetDeferredAlpha();
|
||||
}
|
||||
|
||||
internal delegate int ReserveDeferredParticleDraw();
|
||||
|
||||
internal delegate void DrawImmediateParticle(
|
||||
Matrix4x4 viewProjection,
|
||||
ParticleSubmissionKind kind,
|
||||
int drawIndex,
|
||||
bool opaqueDepthState);
|
||||
|
||||
/// <summary>
|
||||
/// Starts one render frame. Wb point-of-use recovery is limited to one
|
||||
/// request per missing GfxObj even though portal slicing may invoke Draw
|
||||
|
|
@ -332,8 +343,8 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
/// mask 0x00 is a real, reachable input here.
|
||||
///
|
||||
/// <para>M1(b): retail's has_alpha (<c>CMaterial::CheckAlphaValues</c>
|
||||
/// @0x005396a0 — Ghidra-verified 2026-09-04: has_alpha=0 only when
|
||||
/// Ambient.a==Diffuse.a==Specular.a==Emissive.a==1.0f, else has_alpha=1)
|
||||
/// @0x005396a0 — paired-binary verified 2026-09-04: any channel below
|
||||
/// 1.0 sets has_alpha; values at or above 1.0 leave it clear)
|
||||
/// is driven, for a particle, by <c>CMaterial::SetTranslucencySimple</c>
|
||||
/// @0x005396f0 — the only production writer reached from a particle's
|
||||
/// live translucency (<c>CPhysicsPart::SetTranslucency</c> <-
|
||||
|
|
@ -344,8 +355,10 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
/// CURRENT alpha isn't 1.0 (fully opaque)". acdream already carries that
|
||||
/// exact interpolated value — <c>Lerp(p.StartAlpha, p.EndAlpha, tLife)</c>,
|
||||
/// baked into the top byte of <paramref name="meshColorArgb"/> by
|
||||
/// <c>Color32</c> (ParticleSystem.cs:1039-1041,1453) — so no new state is
|
||||
/// needed. Billboards never reach row 4 (their mask 0x02 always
|
||||
/// <c>Color32</c> (ParticleSystem.cs:1039-1041,1453). Production clamps
|
||||
/// particle alpha to [0,1], so top-byte != 0xff is behaviorally identical
|
||||
/// to retail's below-1 predicate within this explicitly bounded domain;
|
||||
/// no new state is needed. Billboards never reach row 4 (their mask 0x02 always
|
||||
/// satisfies row 3), so <paramref name="meshColorArgb"/> only matters for
|
||||
/// mesh particles.</para>
|
||||
///
|
||||
|
|
@ -353,8 +366,9 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
/// screen particles use the independent DrawOrdered path entirely, never
|
||||
/// this method) and never installs a detail surface; MultiPassAlpha
|
||||
/// stays false (Must Not: no environment override) — Row 2 is therefore
|
||||
/// provably unreachable here. Rows 1/5 (Immediate) ARE reachable for an
|
||||
/// Opaque-classified mesh-particle batch with no material alpha.</para>
|
||||
/// provably unreachable here. Row 5 (Immediate) is reachable for an
|
||||
/// Opaque-classified mesh-particle batch with no material alpha; Row 1 is
|
||||
/// not, because this site never installs a detail surface.</para>
|
||||
/// </summary>
|
||||
internal static RetailAlphaMeshDecision RouteParticleSubmission(
|
||||
ParticleSubmissionKind kind, TranslucencyKind meshTranslucency, uint meshColorArgb)
|
||||
|
|
@ -394,50 +408,78 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
_meshDrawListScratch[submission.DrawIndex],
|
||||
viewProjection);
|
||||
|
||||
RetailAlphaMeshDecision decision = submission.Kind == ParticleSubmissionKind.Billboard
|
||||
? RouteParticleSubmission(submission.Kind, default, default)
|
||||
: RouteParticleSubmission(
|
||||
submission.Kind,
|
||||
_meshDrawListScratch[submission.DrawIndex].Batch.Translucency,
|
||||
_meshDrawListScratch[submission.DrawIndex].Instance.ColorArgb);
|
||||
_dispatchDeferredParticle = deferred;
|
||||
TranslucencyKind translucency = submission.Kind == ParticleSubmissionKind.Mesh
|
||||
? _meshDrawListScratch[submission.DrawIndex].Batch.Translucency
|
||||
: default;
|
||||
uint colorArgb = submission.Kind == ParticleSubmissionKind.Mesh
|
||||
? _meshDrawListScratch[submission.DrawIndex].Instance.ColorArgb
|
||||
: default;
|
||||
DeferToRetailAlphaQueue(
|
||||
submission.Kind,
|
||||
translucency,
|
||||
colorArgb,
|
||||
queue,
|
||||
_alphaSource,
|
||||
_reserveDeferredParticleDraw,
|
||||
_drawImmediateParticle,
|
||||
viewProjection,
|
||||
submission.DrawIndex);
|
||||
}
|
||||
}
|
||||
|
||||
switch (decision.Action)
|
||||
/// <summary>
|
||||
/// The production one-submission dispatch used by the scene-particle
|
||||
/// loop. It owns both the queue append and the row-5 immediate callback,
|
||||
/// so tests can discriminate the former throw/deleted-call mutations
|
||||
/// without replacing the router with a pure surrogate.
|
||||
/// </summary>
|
||||
internal static RetailAlphaMeshDecision DeferToRetailAlphaQueue(
|
||||
ParticleSubmissionKind kind,
|
||||
TranslucencyKind meshTranslucency,
|
||||
uint meshColorArgb,
|
||||
RetailAlphaQueue queue,
|
||||
IRetailAlphaDrawSource source,
|
||||
ReserveDeferredParticleDraw reserveDeferred,
|
||||
DrawImmediateParticle drawImmediate,
|
||||
Matrix4x4 viewProjection,
|
||||
int drawIndex)
|
||||
{
|
||||
RetailAlphaMeshDecision decision = RouteParticleSubmission(
|
||||
kind,
|
||||
meshTranslucency,
|
||||
meshColorArgb);
|
||||
switch (decision.Action)
|
||||
{
|
||||
case RetailAlphaMeshAction.Append:
|
||||
{
|
||||
case RetailAlphaMeshAction.Append:
|
||||
{
|
||||
int token = _deferredAlpha.Count;
|
||||
_deferredAlpha.Add(deferred);
|
||||
// Capacity overflow (spec §5): dropped, no recovery.
|
||||
queue.TryAppend(decision.List, _alphaSource, token, decision.OverrideClipmap);
|
||||
break;
|
||||
}
|
||||
|
||||
case RetailAlphaMeshAction.Immediate:
|
||||
// M1(c): rows 1/5 — draw NOW, through the same
|
||||
// per-instance draw code DrawOrderedRhi uses for one
|
||||
// submission (no new renderer). A8: no sort — retail's
|
||||
// immediate RenderMeshSubset draws in mesh/submission
|
||||
// order, so this draws exactly this submission, right
|
||||
// where DrawMesh was called.
|
||||
DrawImmediateParticleSubmissionRhi(viewProjection, submission.Kind, submission.DrawIndex);
|
||||
break;
|
||||
|
||||
case RetailAlphaMeshAction.AppendClipAndImmediate:
|
||||
{
|
||||
// Row 2: provably unreachable at this call site
|
||||
// (MultiPassAlpha stays false), but a router row is a
|
||||
// data-driven outcome, never an invariant to crash on
|
||||
// (feedback_retail_dispatch_is_data_driven) — handled in
|
||||
// full, exactly as spec §4 row 2 describes: append to
|
||||
// CLIP AND ALSO draw immediately.
|
||||
int token = _deferredAlpha.Count;
|
||||
_deferredAlpha.Add(deferred);
|
||||
queue.TryAppend(decision.List, _alphaSource, token, decision.OverrideClipmap);
|
||||
DrawImmediateParticleSubmissionRhi(viewProjection, submission.Kind, submission.DrawIndex);
|
||||
break;
|
||||
}
|
||||
int token = reserveDeferred();
|
||||
queue.TryAppend(decision.List, source, token, decision.OverrideClipmap);
|
||||
break;
|
||||
}
|
||||
case RetailAlphaMeshAction.Immediate:
|
||||
// At this hardcoded no-detail/no-multipass site Immediate is
|
||||
// DrawMesh row 5. Retail SetSurface uses ONE/ZERO, disables
|
||||
// blending, and depth-writes; select that pipeline rather
|
||||
// than the ordinary alpha mesh pipeline.
|
||||
drawImmediate(viewProjection, kind, drawIndex, opaqueDepthState: true);
|
||||
break;
|
||||
case RetailAlphaMeshAction.AppendClipAndImmediate:
|
||||
{
|
||||
int token = reserveDeferred();
|
||||
queue.TryAppend(decision.List, source, token, decision.OverrideClipmap);
|
||||
drawImmediate(viewProjection, kind, drawIndex, opaqueDepthState: false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return decision;
|
||||
}
|
||||
|
||||
private int ReserveDispatchDeferredParticle()
|
||||
{
|
||||
int token = _deferredAlpha.Count;
|
||||
_deferredAlpha.Add(_dispatchDeferredParticle);
|
||||
return token;
|
||||
}
|
||||
|
||||
private void DrawOrdered(ICamera camera)
|
||||
|
|
|
|||
|
|
@ -94,25 +94,21 @@ internal interface IRetailAlphaDrawSource
|
|||
/// retail's per-entry <c>clip</c> byte (<c>SetSurface</c>'s alpha-blend-vs-
|
||||
/// clip-test arm selector).
|
||||
///
|
||||
/// <para>S4-c2 fix round 1 (M4): retail's per-entry <c>new</c>
|
||||
/// ("first-for-this-list") byte is NOT modeled here. Retail's quantifier is
|
||||
/// per-<c>DrawMesh</c>-INVOCATION: <c>DrawMesh</c> @0x0059d4a0 sets both
|
||||
/// list flags true at ENTRY (<c>0059d4cc arg3 = 1; 0059d4d0 var_c = 1</c>)
|
||||
/// and clears each independently after its OWN first append to that list
|
||||
/// WITHIN THAT CALL (<c>0059d5ef if (var_4_1 == 0) var_c = 0; else arg3 =
|
||||
/// 0;</c>) — one retail subset can append to only one list once, so this
|
||||
/// flag is trivially true for every subset retail ever appends; the
|
||||
/// captures confirm it (10,556 <c>AM</c> lines, <c>new=1</c> on 9,685 of
|
||||
/// them — impossible under "first since the last drain", which acdream's
|
||||
/// deleted <c>isFirstForList = target.Count == 0</c> computed instead). One
|
||||
/// acdream append already IS one full <c>DrawMesh</c> invocation (one
|
||||
/// instance, one call), so the truthful port of an always-true flag with no
|
||||
/// acdream reader (Vulkan's per-batch material/matrix binding makes
|
||||
/// retail's per-entry material-capture optimization a no-op here — see
|
||||
/// <see cref="RetailAlphaQueue"/>'s class doc comment) is to not carry it at
|
||||
/// all, rather than keep a field whose value can never vary and whose only
|
||||
/// possible test is therefore vacuous
|
||||
/// (<c>feedback_every_new_pin_must_be_shown_to_fail</c>).</para>
|
||||
/// <para>S4-c2 fix round 2: retail's per-entry <c>new</c>
|
||||
/// ("first-for-this-list") byte is NOT modeled here, but it is not
|
||||
/// always true. <c>DrawMesh</c> @0x0059d4a0 sets the CLIP and ALPHA flags
|
||||
/// true at ENTRY and clears each independently after the first successful
|
||||
/// append to that list within that one <c>DrawMesh</c> call. A call can walk
|
||||
/// several surface subsets and append more than once, including to both
|
||||
/// lists; the capture proves the varying value (10,556 <c>AM</c> lines:
|
||||
/// <c>new=1</c> on 9,685, <c>new=0</c> on 871). Acdream's retained submission
|
||||
/// units do not have a one-to-one relationship with retail <c>DrawMesh</c>
|
||||
/// invocations (ordinary instances and the accepted EnvCell `(cell,list)`
|
||||
/// token can aggregate several subsets), so synthesizing the bit would be
|
||||
/// false precision. No Vulkan draw reads the flag: every prepared batch
|
||||
/// binds its own material and matrix state. Omitting this unread capture-
|
||||
/// optimization bit is therefore the documented modern-backend adaptation,
|
||||
/// not a claim that retail's value cannot vary.</para>
|
||||
/// </summary>
|
||||
internal readonly record struct RetailAlphaEntry(
|
||||
IRetailAlphaDrawSource Source,
|
||||
|
|
@ -174,8 +170,11 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
|||
private int[] _sourceDrawOffsets = new int[4];
|
||||
private readonly RetainedScratchCapacityPolicy _scratchPolicy;
|
||||
private readonly long _scratchBudgetBytes;
|
||||
private readonly Action<RetailAlphaFlushSite>? _drainObserver;
|
||||
|
||||
internal RetailAlphaQueue(long? scratchBudgetBytes = null)
|
||||
internal RetailAlphaQueue(
|
||||
long? scratchBudgetBytes = null,
|
||||
Action<RetailAlphaFlushSite>? drainObserver = null)
|
||||
{
|
||||
long budget = scratchBudgetBytes
|
||||
?? AlphaScratchBudgetProfile.Create(
|
||||
|
|
@ -183,6 +182,7 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
|||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(budget);
|
||||
_scratchBudgetBytes = budget;
|
||||
_scratchPolicy = new RetainedScratchCapacityPolicy(budget);
|
||||
_drainObserver = drainObserver;
|
||||
}
|
||||
|
||||
public bool IsCollecting { get; private set; }
|
||||
|
|
@ -221,8 +221,8 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
|||
/// caller (<see cref="RetailAlphaMeshRouter"/>'s decision) supplies
|
||||
/// <paramref name="overrideClipmap"/>. Retail's per-entry "first for
|
||||
/// this list" flag is not modeled — see <see cref="RetailAlphaEntry"/>'s
|
||||
/// own doc comment (M4) for why it is trivially true for every subset
|
||||
/// retail ever appends and therefore carries no information here.
|
||||
/// own doc comment for why the varying retail bit has no faithful
|
||||
/// one-to-one producer or consumer in this retained Vulkan path.
|
||||
/// </summary>
|
||||
internal bool TryAppend(
|
||||
RetailAlphaList list,
|
||||
|
|
@ -237,11 +237,17 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
|||
throw new ArgumentOutOfRangeException(nameof(token));
|
||||
|
||||
List<RetailAlphaEntry> target = list == RetailAlphaList.Clip ? _clip : _alpha;
|
||||
// All production callers reserve source-owned payload before this
|
||||
// call. Retail drops an entry at capacity, but the source still has
|
||||
// frame-local payload that must be released at the next drain/abort.
|
||||
// Register before the capacity return so a source whose FIRST append
|
||||
// is rejected participates in that cleanup without preparing or
|
||||
// drawing the rejected token.
|
||||
RegisterSource(source);
|
||||
if (target.Count >= ListCapacity)
|
||||
return false;
|
||||
|
||||
target.Add(new RetailAlphaEntry(source, token, overrideClipmap));
|
||||
RegisterSource(source);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -264,14 +270,17 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
|||
/// </summary>
|
||||
public void Flush(RetailAlphaFlushSite site, float threshold)
|
||||
{
|
||||
_ = site; // site distinguishes call sites for tracing/tests only —
|
||||
// the drain algorithm itself does not depend on which one.
|
||||
if (!IsCollecting)
|
||||
throw new InvalidOperationException("Retail alpha flush requires an active frame.");
|
||||
|
||||
if (_clip.Count < threshold * ListCapacity && _alpha.Count < threshold * ListCapacity)
|
||||
return;
|
||||
|
||||
// Optional construction-time observer for the concrete outdoor-frame
|
||||
// ownership gate. It sees only real drains (after retail's threshold
|
||||
// early return), never alters the drain algorithm, and production
|
||||
// composition leaves it null.
|
||||
_drainObserver?.Invoke(site);
|
||||
DrainAndReset();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@ using AcDream.Core.World;
|
|||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface IEnvCellImmediateDrawSink
|
||||
{
|
||||
void DrawImmediate(
|
||||
uint cellId,
|
||||
EnvCellTransparentRoute route,
|
||||
bool detailSurfaceActive);
|
||||
}
|
||||
|
||||
// S3 chunk 4 fix round 2 (L2): RetailPViewFramebufferSize / IRetailPViewFramebufferSource
|
||||
// / SilkRetailPViewFramebufferSource are deleted — their only consumer was
|
||||
// the RHI surface's NDC-to-pixel scissor conversion, and that scissor
|
||||
|
|
@ -41,7 +49,7 @@ internal sealed class RetailPViewCellSource : IRetailPViewCellSource
|
|||
/// is written once; the four places it touches graphics state directly are owned
|
||||
/// by <see cref="IWorldPassSurface"/>.
|
||||
/// </summary>
|
||||
internal sealed partial class RetailPViewPassExecutor
|
||||
internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSink
|
||||
{
|
||||
private readonly IWorldPassSurface _surface;
|
||||
private readonly IRenderFrameGlState _frameGlState;
|
||||
|
|
@ -57,7 +65,15 @@ internal sealed partial class RetailPViewPassExecutor
|
|||
private readonly WorldRenderDiagnostics _diagnostics;
|
||||
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
|
||||
private readonly HashSet<uint> _noSceneParticleEntityIds = [];
|
||||
public RetailPViewPassExecutor(
|
||||
private readonly EnvCellAlphaDrawSource _envCellClipAlphaSource;
|
||||
private readonly EnvCellAlphaDrawSource _envCellBlendAlphaSource;
|
||||
|
||||
internal delegate void RenderImmediateEnvCellRoute(
|
||||
uint cellId,
|
||||
EnvCellTransparentRoute route,
|
||||
bool detailSurfaceActive);
|
||||
|
||||
public RetailPViewPassExecutor(
|
||||
IWorldPassSurface surface,
|
||||
IRenderFrameGlState frameGlState,
|
||||
ClipFrame clipFrame,
|
||||
|
|
@ -87,6 +103,12 @@ public RetailPViewPassExecutor(
|
|||
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
|
||||
_terrainDiagnostics = terrainDiagnostics
|
||||
?? throw new ArgumentNullException(nameof(terrainDiagnostics));
|
||||
_envCellClipAlphaSource = new EnvCellAlphaDrawSource(
|
||||
_envCells.RenderTransparentOrdered,
|
||||
EnvCellTransparentRoute.Clip);
|
||||
_envCellBlendAlphaSource = new EnvCellAlphaDrawSource(
|
||||
_envCells.RenderTransparentOrdered,
|
||||
EnvCellTransparentRoute.Alpha);
|
||||
}
|
||||
|
||||
public void BeginFrame()
|
||||
|
|
@ -168,50 +190,126 @@ public RetailPViewPassExecutor(
|
|||
/// S4-c2 fix round 1 (M6, contract C4): retail <c>DrawEnvCell</c>
|
||||
/// @0x0059f1c2 installs the environment detail surface (may be null)
|
||||
/// before <c>DrawMesh</c> @0x0059f212 and clears it @0x0059f21a — detail
|
||||
/// ON makes the subset take Row 1 (immediate, WITH detail —
|
||||
/// ON makes eligible subsets take Row 1 (immediate, WITH detail —
|
||||
/// <see cref="EnvCellRenderer.TransparentDetailEnabled"/>'s own doc
|
||||
/// comment); detail OFF takes Row 3 (append; mask 0x02 alpha-family for
|
||||
/// an EnvCell transparent shell surface). Routed through the SAME table
|
||||
/// every other alpha submitter uses so the decision is the table's, not
|
||||
/// a hand-coded branch. <see cref="EnvCellRenderer.RenderTransparentOrdered"/>
|
||||
/// already replays a cell's transparent batch on demand, so the queue
|
||||
/// side (<see cref="EnvCellAlphaDrawSource"/>) needs no new deferred-
|
||||
/// replay abstraction — one token per CELL (coarser than retail's own
|
||||
/// per-subset entries; AP-236's residual documents that granularity gap).
|
||||
/// comment); detail OFF still routes each real subset by the exact
|
||||
/// <c>TextureBatchData.RetailSurfaceMask</c> retained on
|
||||
/// <c>ObjectRenderBatch</c>. A pure Base1ClipMap mask 0x08 reaches CLIP;
|
||||
/// alpha-family 0x02 reaches ALPHA; a table-Immediate mask stays at the
|
||||
/// cell turn. The queue coalesces only to one token per (cell,list), the
|
||||
/// accepted AP-238 granularity residual.
|
||||
/// </summary>
|
||||
internal void SubmitOrDrawTransparentCellShell(uint cellId)
|
||||
{
|
||||
RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route(
|
||||
currentlyDrawingSky: false,
|
||||
delayMask: RetailAlphaMeshRouter.DefaultDelayMask,
|
||||
detailSurfaceActive: _envCells.TransparentDetailEnabled,
|
||||
multiPassAlpha: false,
|
||||
subsetMask: RetailAlphaMeshRouter.MaskAlphaFamily,
|
||||
materialHasAlpha: false);
|
||||
|
||||
if (decision.Action == RetailAlphaMeshAction.Immediate)
|
||||
{
|
||||
_singleCellListScratch.Clear();
|
||||
_singleCellListScratch.Add(cellId);
|
||||
_envCells.RenderTransparentOrdered(_singleCellListScratch);
|
||||
return;
|
||||
}
|
||||
|
||||
// Row 2 (AppendClipAndImmediate) needs MultiPassAlpha, fixed false
|
||||
// above — provably unreachable, same reasoning as
|
||||
// WbDrawDispatcher.SubmitToAlphaQueue's ordinary-content case.
|
||||
_envCellAlphaSource ??= new EnvCellAlphaDrawSource(_envCells.RenderTransparentOrdered);
|
||||
int token = _envCellAlphaSource.AddPendingCellId(cellId);
|
||||
_alpha.TryAppend(decision.List, _envCellAlphaSource, token, decision.OverrideClipmap);
|
||||
bool detailSurfaceActive = _envCells.TransparentDetailEnabled;
|
||||
EnvCellTransparentRoute routes = _envCells.GetTransparentRoutes(
|
||||
cellId,
|
||||
detailSurfaceActive);
|
||||
DispatchTransparentCellShell(
|
||||
cellId,
|
||||
routes,
|
||||
detailSurfaceActive,
|
||||
_alpha,
|
||||
_envCellClipAlphaSource,
|
||||
_envCellBlendAlphaSource,
|
||||
this);
|
||||
}
|
||||
|
||||
private EnvCellAlphaDrawSource? _envCellAlphaSource;
|
||||
|
||||
private readonly List<uint> _singleCellListScratch = new(1);
|
||||
|
||||
private void DrawImmediateEnvCellRoute(
|
||||
uint cellId,
|
||||
EnvCellTransparentRoute route,
|
||||
bool detailSurfaceActive)
|
||||
{
|
||||
_singleCellListScratch.Clear();
|
||||
_singleCellListScratch.Add(cellId);
|
||||
_envCells.RenderTransparentOrdered(
|
||||
_singleCellListScratch,
|
||||
route,
|
||||
detailSurfaceActive);
|
||||
}
|
||||
|
||||
void IEnvCellImmediateDrawSink.DrawImmediate(
|
||||
uint cellId,
|
||||
EnvCellTransparentRoute route,
|
||||
bool detailSurfaceActive) =>
|
||||
DrawImmediateEnvCellRoute(cellId, route, detailSurfaceActive);
|
||||
|
||||
/// <summary>
|
||||
/// Production dispatch for one cell after <see cref="EnvCellRenderer"/>
|
||||
/// scanned its real transparent batches. Separate source identities keep
|
||||
/// CLIP and ALPHA replay filters attached to their tokens. Immediate
|
||||
/// subsets draw at the cell turn; each deferred list receives at most one
|
||||
/// token for this cell.
|
||||
/// </summary>
|
||||
internal static void DispatchTransparentCellShell(
|
||||
uint cellId,
|
||||
EnvCellTransparentRoute routes,
|
||||
bool detailSurfaceActive,
|
||||
RetailAlphaQueue queue,
|
||||
EnvCellAlphaDrawSource clipSource,
|
||||
EnvCellAlphaDrawSource alphaSource,
|
||||
IEnvCellImmediateDrawSink renderImmediate)
|
||||
{
|
||||
if ((routes & EnvCellTransparentRoute.Immediate) != 0)
|
||||
{
|
||||
renderImmediate.DrawImmediate(
|
||||
cellId,
|
||||
EnvCellTransparentRoute.Immediate,
|
||||
detailSurfaceActive);
|
||||
}
|
||||
|
||||
if ((routes & EnvCellTransparentRoute.Clip) != 0)
|
||||
{
|
||||
int token = clipSource.AddPendingCellId(cellId);
|
||||
queue.TryAppend(RetailAlphaList.Clip, clipSource, token, overrideClipmap: true);
|
||||
}
|
||||
|
||||
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
|
||||
{
|
||||
int token = alphaSource.AddPendingCellId(cellId);
|
||||
queue.TryAppend(RetailAlphaList.Alpha, alphaSource, token, overrideClipmap: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Delegate adapter retained for focused route/allocation tests;
|
||||
/// production calls the interface overload above with <c>this</c>, so no
|
||||
/// per-cell delegate conversion or retained callback enters the owner.</summary>
|
||||
internal static void DispatchTransparentCellShell(
|
||||
uint cellId,
|
||||
EnvCellTransparentRoute routes,
|
||||
bool detailSurfaceActive,
|
||||
RetailAlphaQueue queue,
|
||||
EnvCellAlphaDrawSource clipSource,
|
||||
EnvCellAlphaDrawSource alphaSource,
|
||||
RenderImmediateEnvCellRoute renderImmediate)
|
||||
{
|
||||
if ((routes & EnvCellTransparentRoute.Immediate) != 0)
|
||||
{
|
||||
renderImmediate(
|
||||
cellId,
|
||||
EnvCellTransparentRoute.Immediate,
|
||||
detailSurfaceActive);
|
||||
}
|
||||
|
||||
if ((routes & EnvCellTransparentRoute.Clip) != 0)
|
||||
{
|
||||
int token = clipSource.AddPendingCellId(cellId);
|
||||
queue.TryAppend(RetailAlphaList.Clip, clipSource, token, overrideClipmap: true);
|
||||
}
|
||||
|
||||
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
|
||||
{
|
||||
int token = alphaSource.AddPendingCellId(cellId);
|
||||
queue.TryAppend(RetailAlphaList.Alpha, alphaSource, token, overrideClipmap: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The queue-facing half of <see cref="SubmitOrDrawTransparentCellShell"/>:
|
||||
/// retains the cell ids submitted this frame in append order and replays
|
||||
/// retains the cell ids submitted to one fixed list in append order and replays
|
||||
/// a drained slice through <see cref="EnvCellRenderer.RenderTransparentOrdered"/>
|
||||
/// (production wiring) — one token per cell, never per subset (M6's
|
||||
/// documented granularity residual). <see cref="IRetailAlphaDrawSource.PrepareAlphaDraws"/>'s
|
||||
|
|
@ -224,7 +322,14 @@ public RetailPViewPassExecutor(
|
|||
/// <c>EnvCellAlphaDrawSourceTests</c> can pin the queue mechanics
|
||||
/// without standing up a GPU-backed renderer.
|
||||
/// </summary>
|
||||
internal sealed class EnvCellAlphaDrawSource(Action<IReadOnlyList<uint>> renderTransparentOrdered)
|
||||
internal delegate void RenderEnvCellsByRoute(
|
||||
IReadOnlyList<uint> cellIds,
|
||||
EnvCellTransparentRoute route,
|
||||
bool detailSurfaceActive);
|
||||
|
||||
internal sealed class EnvCellAlphaDrawSource(
|
||||
RenderEnvCellsByRoute renderTransparentOrdered,
|
||||
EnvCellTransparentRoute route)
|
||||
: IRetailAlphaDrawSource
|
||||
{
|
||||
private readonly List<uint> _pendingCellIds = new();
|
||||
|
|
@ -254,14 +359,17 @@ public RetailPViewPassExecutor(
|
|||
_drawScratch.Clear();
|
||||
for (int i = 0; i < drawCount; i++)
|
||||
_drawScratch.Add(_preparedCellIds[firstPreparedDraw + i]);
|
||||
renderTransparentOrdered(_drawScratch);
|
||||
renderTransparentOrdered(_drawScratch, route, detailSurfaceActive: false);
|
||||
}
|
||||
|
||||
public void ResetAlphaSubmissions()
|
||||
{
|
||||
_pendingCellIds.Clear();
|
||||
_preparedCellIds.Clear();
|
||||
_drawScratch.Clear();
|
||||
}
|
||||
|
||||
internal int PendingCount => _pendingCellIds.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,16 @@ using DatReaderWriter.Enums;
|
|||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
[Flags]
|
||||
internal enum EnvCellTransparentRoute : byte
|
||||
{
|
||||
None = 0,
|
||||
Immediate = 1 << 0,
|
||||
Clip = 1 << 1,
|
||||
Alpha = 1 << 2,
|
||||
All = Immediate | Clip | Alpha,
|
||||
}
|
||||
|
||||
public sealed partial class EnvCellRenderer :
|
||||
IDisposable,
|
||||
IEnvCellLandblockPublisher
|
||||
|
|
@ -803,7 +813,7 @@ public sealed partial class EnvCellRenderer :
|
|||
public void Render(WbRenderPass renderPass)
|
||||
{
|
||||
// WB EnvCellRenderManager.cs:396:
|
||||
RenderCore(renderPass, null, null);
|
||||
RenderCore(renderPass, null, null, EnvCellTransparentRoute.All, detailSurfaceActive: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -817,7 +827,7 @@ public sealed partial class EnvCellRenderer :
|
|||
/// Source: WB EnvCellRenderManager.cs:399-511 (verbatim minus selection highlights).
|
||||
/// </summary>
|
||||
public void Render(WbRenderPass renderPass, HashSet<uint>? filter)
|
||||
=> RenderCore(renderPass, filter, null);
|
||||
=> RenderCore(renderPass, filter, null, EnvCellTransparentRoute.All, detailSurfaceActive: false);
|
||||
|
||||
/// <summary>
|
||||
/// Draws transparent cell shells in the supplied far-to-near PView order.
|
||||
|
|
@ -828,13 +838,40 @@ public sealed partial class EnvCellRenderer :
|
|||
public void RenderTransparentOrdered(IReadOnlyList<uint> orderedCellIds)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(orderedCellIds);
|
||||
RenderCore(WbRenderPass.Transparent, null, orderedCellIds);
|
||||
RenderCore(
|
||||
WbRenderPass.Transparent,
|
||||
null,
|
||||
orderedCellIds,
|
||||
EnvCellTransparentRoute.All,
|
||||
detailSurfaceActive: TransparentDetailEnabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replays only the transparent subsets whose exact retained retail mask
|
||||
/// resolves to <paramref name="route"/>. S4-c2 uses this for its separate
|
||||
/// CLIP/ALPHA queue tokens; the detail-on immediate turn uses the same
|
||||
/// table with <paramref name="detailSurfaceActive"/> true.
|
||||
/// </summary>
|
||||
internal void RenderTransparentOrdered(
|
||||
IReadOnlyList<uint> orderedCellIds,
|
||||
EnvCellTransparentRoute route,
|
||||
bool detailSurfaceActive)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(orderedCellIds);
|
||||
RenderCore(
|
||||
WbRenderPass.Transparent,
|
||||
null,
|
||||
orderedCellIds,
|
||||
route,
|
||||
detailSurfaceActive);
|
||||
}
|
||||
|
||||
private void RenderCore(
|
||||
WbRenderPass renderPass,
|
||||
HashSet<uint>? filter,
|
||||
IReadOnlyList<uint>? orderedCellIds)
|
||||
IReadOnlyList<uint>? orderedCellIds,
|
||||
EnvCellTransparentRoute transparentRoute,
|
||||
bool detailSurfaceActive)
|
||||
{
|
||||
// WB EnvCellRenderManager.cs:400: the RHI arm's three pipelines are built
|
||||
// at construction (see EnvCellRenderer.Rhi.cs), so _initialized alone
|
||||
|
|
@ -969,7 +1006,9 @@ public sealed partial class EnvCellRenderer :
|
|||
drawCalls,
|
||||
allInstances,
|
||||
_drawCallRanges,
|
||||
renderPass);
|
||||
renderPass,
|
||||
transparentRoute,
|
||||
detailSurfaceActive);
|
||||
}
|
||||
|
||||
// WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state).
|
||||
|
|
@ -996,6 +1035,76 @@ public sealed partial class EnvCellRenderer :
|
|||
public bool CellHasTransparent(uint cellId)
|
||||
=> _transparentCellIds.Contains(cellId);
|
||||
|
||||
/// <summary>
|
||||
/// Scans the real prepared transparent render batches for one cell and
|
||||
/// returns the set of outcomes from retail's shared DrawMesh table. A
|
||||
/// cell may contribute one token to each deferred list; masks that resolve
|
||||
/// Immediate remain at the cell turn.
|
||||
/// </summary>
|
||||
internal EnvCellTransparentRoute GetTransparentRoutes(
|
||||
uint cellId,
|
||||
bool detailSurfaceActive)
|
||||
{
|
||||
lock (_renderLock)
|
||||
{
|
||||
if (!_activeSnapshot.BatchedByCell.TryGetValue(cellId, out var groups))
|
||||
return EnvCellTransparentRoute.None;
|
||||
|
||||
EnvCellTransparentRoute routes = EnvCellTransparentRoute.None;
|
||||
foreach ((ulong gfxObjId, List<InstanceData> transforms) in groups)
|
||||
{
|
||||
if (transforms.Count == 0)
|
||||
continue;
|
||||
ObjectRenderData? renderData = _meshManager.TryGetRenderData(gfxObjId);
|
||||
if (renderData is null || renderData.IsSetup)
|
||||
continue;
|
||||
for (int batchIndex = 0; batchIndex < renderData.Batches.Count; batchIndex++)
|
||||
{
|
||||
ObjectRenderBatch batch = renderData.Batches[batchIndex];
|
||||
if (batch.IsTransparent)
|
||||
routes |= RouteTransparentBatch(batch, detailSurfaceActive);
|
||||
}
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exact per-subset EnvCell route. Canonical installed-DAT witness
|
||||
/// F4180104 / surface 08000BFF retains mask 0x08 and therefore reaches
|
||||
/// CLIP when environment detail is absent; alpha-family mask 0x02 reaches
|
||||
/// ALPHA. No collapsed <see cref="ObjectRenderBatch.Translucency"/> value
|
||||
/// participates in this decision.
|
||||
/// </summary>
|
||||
internal static EnvCellTransparentRoute RouteTransparentBatch(
|
||||
ObjectRenderBatch batch,
|
||||
bool detailSurfaceActive)
|
||||
{
|
||||
RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route(
|
||||
currentlyDrawingSky: false,
|
||||
delayMask: RetailAlphaMeshRouter.DefaultDelayMask,
|
||||
detailSurfaceActive: detailSurfaceActive,
|
||||
multiPassAlpha: false,
|
||||
subsetMask: batch.RetailSurfaceMask,
|
||||
materialHasAlpha: false);
|
||||
return decision.Action switch
|
||||
{
|
||||
RetailAlphaMeshAction.Immediate => EnvCellTransparentRoute.Immediate,
|
||||
RetailAlphaMeshAction.Append => decision.List == RetailAlphaList.Clip
|
||||
? EnvCellTransparentRoute.Clip
|
||||
: EnvCellTransparentRoute.Alpha,
|
||||
RetailAlphaMeshAction.AppendClipAndImmediate =>
|
||||
EnvCellTransparentRoute.Clip | EnvCellTransparentRoute.Immediate,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(decision.Action)),
|
||||
};
|
||||
}
|
||||
|
||||
private static bool MatchesTransparentRoute(
|
||||
ObjectRenderBatch batch,
|
||||
EnvCellTransparentRoute route,
|
||||
bool detailSurfaceActive) =>
|
||||
(RouteTransparentBatch(batch, detailSurfaceActive) & route) != 0;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GetCellLightSet (A7 Fix D D-2 helper)
|
||||
// Per-cell up-to-8 point lights, cached per frame. Camera-independent, like
|
||||
|
|
@ -1074,7 +1183,9 @@ public sealed partial class EnvCellRenderer :
|
|||
List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> drawCalls,
|
||||
List<InstanceData> allInstances,
|
||||
IReadOnlyList<DrawCallRange> drawCallRanges,
|
||||
WbRenderPass renderPass)
|
||||
WbRenderPass renderPass,
|
||||
EnvCellTransparentRoute transparentRoute,
|
||||
bool detailSurfaceActive)
|
||||
{
|
||||
// WB BaseObjectRenderManager.cs:710-713:
|
||||
if (drawCalls.Count == 0 || allInstances.Count == 0) return;
|
||||
|
|
@ -1115,6 +1226,13 @@ public sealed partial class EnvCellRenderer :
|
|||
}
|
||||
}
|
||||
|
||||
if (renderPass == WbRenderPass.Transparent
|
||||
&& transparentRoute != EnvCellTransparentRoute.All
|
||||
&& !MatchesTransparentRoute(batch, transparentRoute, detailSurfaceActive))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
totalDraws++;
|
||||
}
|
||||
}
|
||||
|
|
@ -1171,6 +1289,13 @@ public sealed partial class EnvCellRenderer :
|
|||
}
|
||||
}
|
||||
|
||||
if (renderPass == WbRenderPass.Transparent
|
||||
&& transparentRoute != EnvCellTransparentRoute.All
|
||||
&& !MatchesTransparentRoute(batch, transparentRoute, detailSurfaceActive))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int groupIndex = (int)batch.CullMode + (batch.IsAdditive ? 4 : 0);
|
||||
List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)> group =
|
||||
_batchesByCullGroup[groupIndex];
|
||||
|
|
|
|||
|
|
@ -109,6 +109,14 @@ namespace AcDream.App.Rendering.Wb
|
|||
public (int Width, int Height) TextureSize { get; set; }
|
||||
public TextureFormat TextureFormat { get; set; }
|
||||
public uint SurfaceId { get; set; }
|
||||
/// <summary>
|
||||
/// Retail <c>D3DPolyRender::ConstructMesh</c> @0x0059dfa0's exact
|
||||
/// per-subset routing mask. The Content extraction preserves this on
|
||||
/// <c>TextureBatchData</c>; S4-c2 carries it across the upload boundary
|
||||
/// so EnvCell replay can choose CLIP versus ALPHA without reconstructing
|
||||
/// the raw surface bits from <see cref="Translucency"/>.
|
||||
/// </summary>
|
||||
public byte RetailSurfaceMask { get; set; }
|
||||
public TextureKey Key { get; set; }
|
||||
public DatReaderWriter.Enums.CullMode CullMode { get; set; }
|
||||
public AcDream.Core.Meshing.TranslucencyKind Translucency { get; set; }
|
||||
|
|
@ -2250,6 +2258,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
TextureIndex = textureIndex,
|
||||
TextureSize = (format.Width, format.Height),
|
||||
TextureFormat = format.Format,
|
||||
RetailSurfaceMask = batch.RetailSurfaceMask,
|
||||
Translucency = batch.Translucency,
|
||||
IsTransparent = batch.IsTransparent,
|
||||
IsAdditive = batch.IsAdditive,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue