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.
333 lines
14 KiB
C#
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()
|
|
{
|
|
}
|
|
}
|
|
}
|