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>
300 lines
11 KiB
C#
300 lines
11 KiB
C#
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 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
|
|
{
|
|
/// <summary>M1(d) pin 1: an Opaque-classified mesh-particle batch (mask
|
|
/// 0x00) with a non-fully-opaque current alpha (top ColorArgb byte !=
|
|
/// 0xFF, i.e. materialHasAlpha true) routes to ALPHA (Append) — retail's
|
|
/// row 4 material-alpha fallback — not Immediate. Mutation check:
|
|
/// reverting to the pre-fix code (which threw whenever the decision
|
|
/// wasn't Append) would not distinguish this from pin 2 below at all
|
|
/// (both would just throw); reverting ONLY the materialHasAlpha
|
|
/// derivation to a hardcoded `false` makes this assertion fail because
|
|
/// the decision becomes Immediate instead of Append.</summary>
|
|
[Fact]
|
|
public void OpaqueClassifiedMeshBatch_WithMaterialAlpha_DefersAndDrawsOnlyAtFlush()
|
|
{
|
|
var queue = new RetailAlphaQueue();
|
|
var source = new RecordingSource();
|
|
int reserveCount = 0;
|
|
int immediateCount = 0;
|
|
queue.BeginFrame();
|
|
|
|
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
|
|
/// with a fully-opaque current alpha (top ColorArgb byte == 0xFF, i.e.
|
|
/// materialHasAlpha false) draws immediately and never enters a list —
|
|
/// retail's row 5 fallthrough. Mutation check: this is the EXACT case
|
|
/// that threw before the fix (mask 0x00, materialHasAlpha false ->
|
|
/// row 5 Immediate -> the old "rows 1/2/4/5 unreachable" guard fired);
|
|
/// reverting the throw's removal reproduces
|
|
/// <c>InvalidOperationException</c> here, which this test would report
|
|
/// as a failure (an unhandled exception) rather than an assertion
|
|
/// mismatch.</summary>
|
|
[Fact]
|
|
public void OpaqueClassifiedMeshBatch_WithNoMaterialAlpha_DrawsImmediateOnOpaqueDepthState()
|
|
{
|
|
var queue = new RetailAlphaQueue();
|
|
var source = new RecordingSource();
|
|
int reserveCount = 0;
|
|
var immediate = new List<(ParticleSubmissionKind Kind, int Index, bool Opaque)>();
|
|
queue.BeginFrame();
|
|
|
|
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
|
|
/// (0x02) regardless of the (unused) mesh parameters, and therefore
|
|
/// always satisfies row 3 (Append, ALPHA) under the fixed default delay
|
|
/// mask — never Immediate, never CLIP. Mutation check: swapping
|
|
/// <see cref="RetailAlphaMeshRouter.MaskAlphaFamily"/> for
|
|
/// <see cref="RetailAlphaMeshRouter.MaskClipMap"/> in the billboard
|
|
/// branch would route this to the CLIP list instead of ALPHA.</summary>
|
|
[Fact]
|
|
public void Billboard_AlwaysRoutesToAlphaAppend()
|
|
{
|
|
RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission(
|
|
ParticleSubmissionKind.Billboard, default, default);
|
|
|
|
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()
|
|
{
|
|
}
|
|
}
|
|
}
|