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:
Erik 2026-09-04 08:56:49 +02:00
parent 9ccb61a8ec
commit 0aa166aa09
14 changed files with 1779 additions and 224 deletions

View file

@ -1,16 +1,23 @@
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Wb;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.Content;
using AcDream.Core.Meshing;
using AcDream.Core.Vfx;
using Microsoft.Extensions.Logging.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// S4-c2 fix round 1 (M1, blocking): <see cref="ParticleRenderer.RouteParticleSubmission"/>
/// is the pure router-input derivation the blocking fix extracted from
/// <c>DeferToRetailAlphaQueue</c> so it is directly testable without a GPU/
/// mesh-manager harness. Before this round, a mesh-particle batch classified
/// <see cref="TranslucencyKind.Opaque"/> (mask 0x00) reaching <c>DrawMesh</c>
/// row 5 (Immediate) hit an unconditional throw in the render loop — these
/// tests pin the two outcomes the fix made reachable.
/// S4-c2 particle routing and production-dispatch pins. The pure table tests
/// remain narrow coverage; the two principal cases below call the actual
/// <see cref="ParticleRenderer.DeferToRetailAlphaQueue"/> dispatch that the
/// renderer's scene loop uses.
/// </summary>
public sealed class ParticleRendererRouteTests
{
@ -24,16 +31,37 @@ public sealed class ParticleRendererRouteTests
/// derivation to a hardcoded `false` makes this assertion fail because
/// the decision becomes Immediate instead of Append.</summary>
[Fact]
public void OpaqueClassifiedMeshBatch_WithMaterialAlpha_RoutesToAlphaAppend()
public void OpaqueClassifiedMeshBatch_WithMaterialAlpha_DefersAndDrawsOnlyAtFlush()
{
// Top byte (alpha) = 0x80 -> not 0xFF -> materialHasAlpha = true.
const uint colorArgbWithAlpha = 0x80FFFFFFu;
var queue = new RetailAlphaQueue();
var source = new RecordingSource();
int reserveCount = 0;
int immediateCount = 0;
queue.BeginFrame();
RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission(
ParticleSubmissionKind.Mesh, TranslucencyKind.Opaque, colorArgbWithAlpha);
const uint colorArgbWithAlpha = 0x80FFFFFFu;
RetailAlphaMeshDecision decision = ParticleRenderer.DeferToRetailAlphaQueue(
ParticleSubmissionKind.Mesh,
TranslucencyKind.Opaque,
colorArgbWithAlpha,
queue,
source,
reserveDeferred: () => reserveCount++,
drawImmediate: (_, _, _, _) => immediateCount++,
Matrix4x4.Identity,
drawIndex: 7);
Assert.Equal(RetailAlphaMeshAction.Append, decision.Action);
Assert.Equal(RetailAlphaList.Alpha, decision.List);
Assert.Equal(1, reserveCount);
Assert.Equal(1, queue.AlphaCount);
Assert.Equal(0, source.DrawCount);
Assert.Equal(0, immediateCount);
queue.EndFrame();
Assert.Equal(1, source.DrawCount);
Assert.Equal(0, immediateCount);
}
/// <summary>M1(d) pin 2: the SAME Opaque-classified mesh-particle batch
@ -47,14 +75,100 @@ public sealed class ParticleRendererRouteTests
/// as a failure (an unhandled exception) rather than an assertion
/// mismatch.</summary>
[Fact]
public void OpaqueClassifiedMeshBatch_WithNoMaterialAlpha_RoutesImmediate()
public void OpaqueClassifiedMeshBatch_WithNoMaterialAlpha_DrawsImmediateOnOpaqueDepthState()
{
const uint fullyOpaqueColorArgb = 0xFFFFFFFFu;
var queue = new RetailAlphaQueue();
var source = new RecordingSource();
int reserveCount = 0;
var immediate = new List<(ParticleSubmissionKind Kind, int Index, bool Opaque)>();
queue.BeginFrame();
RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission(
ParticleSubmissionKind.Mesh, TranslucencyKind.Opaque, fullyOpaqueColorArgb);
const uint fullyOpaqueColorArgb = 0xFFFFFFFFu;
RetailAlphaMeshDecision decision = ParticleRenderer.DeferToRetailAlphaQueue(
ParticleSubmissionKind.Mesh,
TranslucencyKind.Opaque,
fullyOpaqueColorArgb,
queue,
source,
reserveDeferred: () => reserveCount++,
drawImmediate: (_, kind, index, opaque) => immediate.Add((kind, index, opaque)),
Matrix4x4.Identity,
drawIndex: 11);
Assert.Equal(RetailAlphaMeshAction.Immediate, decision.Action);
Assert.Equal(0, reserveCount);
Assert.Equal(0, queue.PendingCount);
Assert.Equal(
[(ParticleSubmissionKind.Mesh, 11, true)],
immediate);
ParticleRenderer.MeshParticlePipelineState state =
ParticleRenderer.ResolveMeshParticlePipelineState(
TranslucencyKind.Opaque,
opaqueDepthState: immediate[0].Opaque);
Assert.Equal(GpuBlendMode.None, state.Blend);
Assert.True(state.Depth.Test);
Assert.True(state.Depth.Write);
Assert.Equal(WorldDepthContract.WorldCompare, state.Depth.Compare);
queue.EndFrame();
Assert.Equal(0, source.PrepareCount);
Assert.Equal(0, source.DrawCount);
}
/// <summary>
/// M1 production fixed-state/binding pin. Executes the real private
/// pipeline factory against the recording GPU device and the real
/// <c>PipelineForMeshBlend</c> selector (not a restated expected-state
/// helper). Mutation checks: making the opaque factory depth-write false
/// fails the descriptor assertions; mapping the immediate opaque case to
/// <c>_meshAlphaPipeline</c> fails the identity assertion.
/// </summary>
[Fact]
public void ImmediateOpaqueMesh_UsesProductionParticleMeshOpaquePipelineDescription()
{
using var device = new RecordingGpuDevice();
using var manager = new ObjectMeshManager(
new VulkanMeshPipelineDevice(device.Retirement),
device,
new NullPreparedAssetSource(),
NullLogger<ObjectMeshManager>.Instance);
var adapter = (WbMeshAdapter)RuntimeHelpers.GetUninitializedObject(
typeof(WbMeshAdapter));
typeof(WbMeshAdapter).GetField(
"_meshManager",
BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(adapter, manager);
var frames = new GpuDeviceFrameLifetime(device);
var scope = new VulkanWorldPassScope(sampleCount: 1);
using var renderer = new ParticleRenderer(
device,
frames,
scope,
new ParticleSystem(new EmitterDescRegistry(), new Random(42)),
meshAdapter: adapter);
MethodInfo select = typeof(ParticleRenderer).GetMethod(
"PipelineForMeshBlend",
BindingFlags.Instance | BindingFlags.NonPublic)!;
var selected = (IGpuPipeline)select.Invoke(
renderer,
[TranslucencyKind.Opaque, true])!;
Assert.Same(
device.CreatedPipelines.Single(
static pipeline => pipeline.Description.Name == "particle-mesh-opaque"),
selected);
GpuPipelineDescription description = selected.Description;
Assert.Equal("particle_mesh", description.Shaders.Name);
Assert.Equal(GpuBlendMode.None, description.Blend);
Assert.True(description.Depth.Test);
Assert.True(description.Depth.Write);
Assert.Equal(WorldDepthContract.WorldCompare, description.Depth.Compare);
Assert.Equal(GpuCullMode.None, description.Cull);
Assert.Equal(GpuFrontFace.Clockwise, description.FrontFace);
Assert.False(description.AlphaToCoverage);
}
/// <summary>A billboard submission always carries the alpha-family mask
@ -73,4 +187,114 @@ public sealed class ParticleRendererRouteTests
Assert.Equal(RetailAlphaMeshAction.Append, decision.Action);
Assert.Equal(RetailAlphaList.Alpha, decision.List);
}
[Fact]
public void ProductionDispatch_WarmedAppendAndImmediatePathsDoNotAllocate()
{
var queue = new RetailAlphaQueue();
var sink = new NoAllocParticleSink();
ParticleRenderer.ReserveDeferredParticleDraw reserve = sink.Reserve;
ParticleRenderer.DrawImmediateParticle immediate = sink.DrawImmediate;
for (int i = 0; i < 64; i++)
RunPair(queue, sink, reserve, immediate);
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 1_000; i++)
RunPair(queue, sink, reserve, immediate);
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(0, allocated);
}
private static void RunPair(
RetailAlphaQueue queue,
NoAllocParticleSink sink,
ParticleRenderer.ReserveDeferredParticleDraw reserve,
ParticleRenderer.DrawImmediateParticle immediate)
{
queue.BeginFrame();
ParticleRenderer.DeferToRetailAlphaQueue(
ParticleSubmissionKind.Mesh,
TranslucencyKind.Opaque,
0x80FFFFFFu,
queue,
sink,
reserve,
immediate,
Matrix4x4.Identity,
drawIndex: 0);
ParticleRenderer.DeferToRetailAlphaQueue(
ParticleSubmissionKind.Mesh,
TranslucencyKind.Opaque,
0xFFFFFFFFu,
queue,
sink,
reserve,
immediate,
Matrix4x4.Identity,
drawIndex: 1);
queue.EndFrame();
}
private sealed class RecordingSource : IRetailAlphaDrawSource
{
public int PrepareCount { get; private set; }
public int DrawCount { get; private set; }
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens) => PrepareCount++;
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount) =>
DrawCount += drawCount;
public void ResetAlphaSubmissions()
{
}
}
private sealed class NoAllocParticleSink : IRetailAlphaDrawSource
{
private int _nextToken;
public int Reserve() => _nextToken++;
public void DrawImmediate(
Matrix4x4 viewProjection,
ParticleSubmissionKind kind,
int drawIndex,
bool opaqueDepthState)
{
}
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)
{
}
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
{
}
public void ResetAlphaSubmissions() => _nextToken = 0;
}
private sealed class NullPreparedAssetSource : IPreparedAssetSource
{
public PreparedAssetSourceStats Stats => default;
public CacheStats DecodedTextureCacheStats => default;
public PreparedAssetPresence Probe(
AcDream.Content.Pak.PakAssetType type,
uint sourceFileId) =>
PreparedAssetPresence.Missing;
public PreparedAssetReadResult Read(
in PreparedAssetRequest request,
CancellationToken cancellationToken = default) =>
PreparedAssetReadResult.Missing;
public void Dispose()
{
}
}
}