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.
This commit is contained in:
Erik 2026-09-04 10:36:12 +02:00
parent 0aa166aa09
commit 252886e84f
21 changed files with 812 additions and 277 deletions

View file

@ -1,3 +1,4 @@
using System.Collections;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
@ -9,6 +10,7 @@ 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;
@ -189,54 +191,110 @@ public sealed class ParticleRendererRouteTests
}
[Fact]
public void ProductionDispatch_WarmedAppendAndImmediatePathsDoNotAllocate()
public void ProductionImmediateMesh_WarmedDrawImmediateParticleSubmissionRhiDoesNotAllocate()
{
var queue = new RetailAlphaQueue();
var sink = new NoAllocParticleSink();
ParticleRenderer.ReserveDeferredParticleDraw reserve = sink.Reserve;
ParticleRenderer.DrawImmediateParticle immediate = sink.DrawImmediate;
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);
for (int i = 0; i < 64; i++)
RunPair(queue, sink, reserve, immediate);
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)!;
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 1_000; i++)
RunPair(queue, sink, reserve, immediate);
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
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 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; }
@ -252,31 +310,6 @@ public sealed class ParticleRendererRouteTests
}
}
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;