acdream/tests/AcDream.App.Tests/Rendering/ParticleRendererRouteTests.cs
Erik 252886e84f fix(rendering): repair EnvCell retail CLIP state
Exclude deferred EnvCell subsets from the opaque turn, preserve exact CLIP/ALPHA fixed-state groups through leaf replay, and use retail's row-3 override, blend, depth, and texture-class alpha references. Keep the existing building-detail sentinel distinct from the two CLIP references.

Bound rejected source payload, replace no-op allocation proofs with actual EnvCell and particle RHI paths, rebuild checked-in SPIR-V, and correct AP-238/AP-240 plus the S4-c2 evidence record.

Gates: Release 0W/0E; hermetic 16735/0/0; InstalledDat 255 pass/10 known fail/1 skip; shaders 32/32; focused 239/239; allocation 2/2 at 0 B. The evidence/comment repair re-ran Release 0W/0E, shaders 32/32, affected 37/37, and allocation 2/2; mesh_detail.vert.spv remained byte-identical at SHA-256 5346247ab7d606046943e19b28888c814e08dc6cb27cd9750096ac055457eb57.

Mutation proof, with each mutation restored after its named first failure:

1. Restoring the opaque predicate to !IsAdditive fails WholeLeaf_MixedCellDrawsOpaqueAtTurnThenClipAndAlphaAtDrain first at draw count: expected 1, actual 3.
2. Selecting _alphaPipeline for CLIP fails WholeLeaf_ClipDrainBindsExactStateAndTextureClassReference first at the bind sequence: expected [envcell-clip], actual [envcell-alpha].
3. Disabling CLIP depth write fails that production Theory first at Assert.True(clipPipeline.Depth.Write): expected true, actual false.
4. Swapping palette/DDS references fails the DDS row first: expected 0.784313738, actual 0.392156869; the palette row reports the inverse.
5. Mutating mesh_modern.frag from < to <= fails ClipShaders_UseGreaterEqualForThePerRangeReference first at Assert.Contains("if (color.a < alphaCutoff) discard;"): the required source spelling is absent.
6. Restoring row-3 OverrideClipmap=true fails the real-interface clip Theory first at Assert.False: expected false, actual true.
7. Deleting failed-append rollback fails the flush/end/abort rejection-storm rows first at the bounded pending-count assertion: expected 0, actual 9000.
8. Selecting _transparentDetailPipeline for a ClipMap detail contribution fails the leaf detail pin first at the bind sequence: expected second bind envcell-retail-detail-clip, actual envcell-retail-detail-alpha.
9. Resetting detail ParamB to zero fails the same detail pin first at the second pushed reference: expected 0.784313738, actual 0.
10. Classifying CLIP with exact mask equality excludes legal 0x09 and fails WholeLeaf_PositiveStippleClipMaskUsesClipPipelineAndDdsReference first at pipeline: expected envcell-clip, actual envcell-alpha.
11. Mapping the new blend to SRC_ALPHA/INVSRCALPHA fails AllRetailBlendModesAreRepresentable first at the tuple: expected (One, OneMinusSrcAlpha), actual (SrcAlpha, OneMinusSrcAlpha).
12. Restoring mesh_detail.vert's uParamB > 0.5 category predicate fails ClipShaders_UseGreaterEqualForThePerRangeReference first because vDetailCategory = uParamB == 1.0 is absent.
13. Treating every positive detail uParamB as a cutoff fails that source pin first because isRetailClipReference(uParamB) ? uParamB : 0.05 is absent.
14. Adding arbitrary 0.5 as an accepted reference to either mesh_modern.frag or mesh_detail.frag fails that source pin first at Assert.DoesNotContain("value - 0.5"); both mutations were run and reversed independently.

Retail: D3DPolyRender::SetSurface @ 0x0059c4d0; paired binary @ 0x0059c72a, 0x0059c747/0x0059c74f, 0x0059c821, 0x0059c838, 0x0059c866.
2026-09-04 11:53:45 +02:00

333 lines
14 KiB
C#

using System.Collections;
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 Chorizite.Core.Render.Enums;
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 ProductionImmediateMesh_WarmedDrawImmediateParticleSubmissionRhiDoesNotAllocate()
{
using var device = new RecordingGpuDevice(64 * 1024 * 1024);
using var manager = new ObjectMeshManager(
new VulkanMeshPipelineDevice(device.Retirement),
device,
new NullPreparedAssetSource(),
NullLogger<ObjectMeshManager>.Instance);
ObjectRenderData renderData = Assert.IsType<ObjectRenderData>(manager.UploadMeshData(
new ObjectMeshData
{
ObjectId = 0x010001ECu,
Vertices =
[
new VertexPositionNormalTexture { Position = Vector3.Zero },
new VertexPositionNormalTexture { Position = Vector3.UnitX },
new VertexPositionNormalTexture { Position = Vector3.UnitY },
],
TextureBatches =
{
[(8, 8, TextureFormat.RGBA8)] =
[
new TextureBatchData
{
Key = new TextureKey { SurfaceId = 0x08000015u },
TextureData = new byte[8 * 8 * 4],
Indices = [0, 1, 2],
Translucency = TranslucencyKind.Opaque,
CullMode = DatReaderWriter.Enums.CullMode.Clockwise,
},
],
},
}));
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);
Type keyType = typeof(ParticleRenderer).GetNestedType(
"MeshBatchKey", BindingFlags.NonPublic)!;
Type instanceType = typeof(ParticleRenderer).GetNestedType(
"MeshParticleInstance", BindingFlags.NonPublic)!;
Type drawType = typeof(ParticleRenderer).GetNestedType(
"MeshParticleDraw", BindingFlags.NonPublic)!;
object key = Activator.CreateInstance(
keyType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
args: [0x010001ECu, 0],
culture: null)!;
object instance = Activator.CreateInstance(
instanceType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
args: [Matrix4x4.Identity, 0xFFFFFFFFu, 0f, 0u],
culture: null)!;
object draw = Activator.CreateInstance(
drawType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
args: [key, renderData.Batches[0], instance],
culture: null)!;
var drawList = (IList)typeof(ParticleRenderer).GetField(
"_meshDrawListScratch",
BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(renderer)!;
drawList.Add(draw);
var immediate = (ParticleRenderer.DrawImmediateParticle)typeof(ParticleRenderer)
.GetField("_drawImmediateParticle", BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(renderer)!;
frames.BeginFrame();
renderer.BeginFrame(frameSlot: 0);
IGpuFrame frame = frames.CurrentFrame!;
using IGpuPassEncoder pass = frame.BeginPass(GpuPassDescription.BackbufferClear(
"s4-c2-particle-allocation",
Vector4.Zero,
sampleCount: 1));
using IDisposable publication = scope.Publish(pass);
device.Clear();
device.RecordingEnabled = false;
long allocated = ZeroAllocationProbe.MeasureWarmed(
() => immediate(
Matrix4x4.Identity,
ParticleSubmissionKind.Mesh,
drawIndex: 0,
opaqueDepthState: true),
batchSize: 256,
warmupBatches: 2,
samples: 4);
Assert.Equal(0, allocated);
}
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 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()
{
}
}
}