fix(render): interior shell and detail passes bind their own instance opacity (Campaign VM VM1)
EnvCellRenderer.Rhi's SubmitRhi bound StorageInstances/StorageBatches/ StorageClipSlots/StorageGlobalLights/StorageInstanceLightSets every frame but never GpuBindingModel.StorageInstanceAlpha (binding 7) — the SSBO mesh_modern.vert reads as instanceAlpha[instanceIndex] (vOpacityMultiplier, #188) and, as of Campaign VM VM1 (05970306), mesh_detail.vert now reads the same way (vDetailOpacity). Without a bind of its own, both the interior shell pass and the interior detail replay read whatever section WbDrawDispatcher's own SubmitRhi last bound in the same pass — an unrelated object's opacity array, indexed by these EnvCell instance ids. This predates VM1 (6c79d35chas the same omission on the mesh_modern side); VM1 must not widen a latent defect by adding a second unconditional reader of the same unbound slot. Fix, root cause, no guard: EnvCellRenderer now owns _instanceAlphaData, a grow-only float[] parallel to _gpuInstanceTransforms (same pattern as _clipSlotData/_lightSetData), filled with the constant 1.0f every frame — EnvCell shells have no #188 TransparentPartHook translucency fade (that mechanism fades object PARTS, never cells) — and bound at GpuBindingModel.StorageInstanceAlpha alongside the renderer's other per-frame ring sections, before any draw in the pass. Test: EnvCellRendererTests.SubmitRhi_BindsConstantOneInstanceAlphaBeforeAnyDrawInThePass drives SubmitRhi directly (reflection, mirroring the file's existing private-method test pattern) with N seeded cell instances and one real draw command, then asserts against RecordingGpuDevice that StorageInstanceAlpha is bound with exactly N floats all equal to 1.0f, and that the bind precedes the pass's first MultiDrawIndexedIndirect call. Verified failing (StorageInstanceAlpha was never bound) with the fix temporarily reverted, then passing restored. Verified: dotnet build AcDream.slnx -c Release (0 warnings, 0 errors); dotnet test on AcDream.App.Tests (Release, hermetic lanes) green, 5960/5960 (5959 baseline + 1 new test). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
059703066f
commit
388457a735
3 changed files with 147 additions and 0 deletions
|
|
@ -163,6 +163,18 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Campaign VM VM1 follow-up: per-instance opacity multiplier, laid out
|
||||||
|
// parallel to the transforms exactly like _clipSlotData above. EnvCell
|
||||||
|
// shells have no #188 translucency-fade concept, so every element is
|
||||||
|
// the constant no-op 1.0f — grown but not reallocated per frame, same
|
||||||
|
// as the other per-instance scratch arrays here.
|
||||||
|
if (_instanceAlphaData.Length < uniqueInstanceCount)
|
||||||
|
{
|
||||||
|
_instanceAlphaData = new float[
|
||||||
|
Math.Max(_instanceAlphaData.Length * 2, uniqueInstanceCount)];
|
||||||
|
}
|
||||||
|
Array.Fill(_instanceAlphaData, 1f, 0, uniqueInstanceCount);
|
||||||
|
|
||||||
// A7 Fix D (D-2): per-instance 8-int light set, keyed on the cell each
|
// A7 Fix D (D-2): per-instance 8-int light set, keyed on the cell each
|
||||||
// shell instance belongs to.
|
// shell instance belongs to.
|
||||||
int lightStride = LightManager.MaxLightsPerObject;
|
int lightStride = LightManager.MaxLightsPerObject;
|
||||||
|
|
@ -218,6 +230,9 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
BindRingSection<uint>(
|
BindRingSection<uint>(
|
||||||
encoder, frame, GpuBindingModel.StorageClipSlots,
|
encoder, frame, GpuBindingModel.StorageClipSlots,
|
||||||
_clipSlotData.AsSpan(0, uniqueInstanceCount));
|
_clipSlotData.AsSpan(0, uniqueInstanceCount));
|
||||||
|
BindRingSection<float>(
|
||||||
|
encoder, frame, GpuBindingModel.StorageInstanceAlpha,
|
||||||
|
_instanceAlphaData.AsSpan(0, uniqueInstanceCount));
|
||||||
BindRingSection<float>(
|
BindRingSection<float>(
|
||||||
encoder, frame, GpuBindingModel.StorageGlobalLights,
|
encoder, frame, GpuBindingModel.StorageGlobalLights,
|
||||||
_globalLightData.AsSpan(
|
_globalLightData.AsSpan(
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,18 @@ public sealed partial class EnvCellRenderer :
|
||||||
// slot 0 ⇒ no-clip.
|
// slot 0 ⇒ no-clip.
|
||||||
private uint[] _clipSlotData = Array.Empty<uint>();
|
private uint[] _clipSlotData = Array.Empty<uint>();
|
||||||
|
|
||||||
|
// Campaign VM VM1 follow-up: per-instance opacity multiplier, parallel to
|
||||||
|
// _gpuInstanceTransforms, feeding mesh_modern.vert/mesh_detail.vert's
|
||||||
|
// InstanceAlphaBuf (binding 7, GpuBindingModel.StorageInstanceAlpha).
|
||||||
|
// Before this field the shell and interior-detail passes never bound that
|
||||||
|
// storage buffer at all, so both shaders read whatever section
|
||||||
|
// WbDrawDispatcher's own SubmitRhi last bound in the same pass — an
|
||||||
|
// unrelated object's opacity array, indexed by these cell instance ids.
|
||||||
|
// EnvCell shells never carry a #188 TransparentPartHook translucency fade
|
||||||
|
// (that mechanism fades object PARTS, never cells), so every element is
|
||||||
|
// the constant no-op 1.0f.
|
||||||
|
private float[] _instanceAlphaData = Array.Empty<float>();
|
||||||
|
|
||||||
// A7 Fix D (D-2): this renderer owns its lighting (self-contained state,
|
// A7 Fix D (D-2): this renderer owns its lighting (self-contained state,
|
||||||
// like uViewProjection) instead of reading whatever WbDrawDispatcher last
|
// like uViewProjection) instead of reading whatever WbDrawDispatcher last
|
||||||
// bound. Global point-light snapshot (same data/indices as the dispatcher,
|
// bound. Global point-light snapshot (same data/indices as the dispatcher,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
|
@ -98,6 +99,125 @@ public class EnvCellRendererTests
|
||||||
device.RingBytes.Slice((int)storageBind.OffsetBytes, sizeof(uint))));
|
device.RingBytes.Slice((int)storageBind.OffsetBytes, sizeof(uint))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign VM VM1 follow-up. EnvCellRenderer.Rhi's SubmitRhi bound
|
||||||
|
/// StorageInstances/StorageBatches/StorageClipSlots/StorageGlobalLights/
|
||||||
|
/// StorageInstanceLightSets every frame but never
|
||||||
|
/// GpuBindingModel.StorageInstanceAlpha (binding 7) — the SSBO both
|
||||||
|
/// mesh_modern.vert (interior shells) and mesh_detail.vert (interior
|
||||||
|
/// detail replay) read as instanceAlpha[instanceIndex]. Predates VM1
|
||||||
|
/// (6c79d35c has the same omission); with mesh_detail.vert now reading
|
||||||
|
/// that binding too, an unfixed gap here would have made the interior
|
||||||
|
/// detail overlay read whatever section a DIFFERENT renderer (or a
|
||||||
|
/// stale ring slot) last left in binding 7, indexed by these cells'
|
||||||
|
/// instance ids. This pins that SubmitRhi binds its own constant-1.0f
|
||||||
|
/// section, sized to the live instance count, before it records any
|
||||||
|
/// draw in the pass — one bind serves every subsequent draw in that
|
||||||
|
/// pass, mesh_modern's shell pipeline and (when enabled) mesh_detail's
|
||||||
|
/// replay alike.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void SubmitRhi_BindsConstantOneInstanceAlphaBeforeAnyDrawInThePass()
|
||||||
|
{
|
||||||
|
const int instanceCount = 5;
|
||||||
|
|
||||||
|
using var device = new RecordingGpuDevice();
|
||||||
|
using var meshManager = CreateMeshManager(device);
|
||||||
|
var frameLifetime = new GpuDeviceFrameLifetime(device);
|
||||||
|
var scope = new VulkanWorldPassScope(sampleCount: 1);
|
||||||
|
using var renderer = new EnvCellRenderer(
|
||||||
|
device,
|
||||||
|
frameLifetime,
|
||||||
|
scope,
|
||||||
|
meshManager,
|
||||||
|
new WbFrustum());
|
||||||
|
|
||||||
|
frameLifetime.BeginFrame();
|
||||||
|
IGpuFrame frame = frameLifetime.CurrentFrame!;
|
||||||
|
using IGpuPassEncoder pass = frame.BeginPass(
|
||||||
|
GpuPassDescription.BackbufferClear(
|
||||||
|
"envcell-submit-alpha-binding",
|
||||||
|
Vector4.Zero,
|
||||||
|
sampleCount: 1));
|
||||||
|
using IDisposable publication = scope.Publish(pass);
|
||||||
|
|
||||||
|
// Seed one real draw command, exactly what
|
||||||
|
// RenderModernMDIInternal would have built from a live landblock,
|
||||||
|
// so the test can assert an actual ordering against a real draw
|
||||||
|
// rather than a vacuous "no draw happened" pass.
|
||||||
|
Type rendererType = typeof(EnvCellRenderer);
|
||||||
|
FieldInfo commandsField = rendererType.GetField(
|
||||||
|
"_commands", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||||
|
commandsField.SetValue(renderer, new[]
|
||||||
|
{
|
||||||
|
new DrawElementsIndirectCommand
|
||||||
|
{
|
||||||
|
Count = 3,
|
||||||
|
InstanceCount = (uint)instanceCount,
|
||||||
|
FirstIndex = 0,
|
||||||
|
BaseVertex = 0,
|
||||||
|
BaseInstance = 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
FieldInfo batchesField = rendererType.GetField(
|
||||||
|
"_modernBatches", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||||
|
batchesField.SetValue(renderer, new ModernBatchData[] { default });
|
||||||
|
FieldInfo rangesField = rendererType.GetField(
|
||||||
|
"_mdiDrawRanges", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||||
|
var ranges = (List<EnvCellRenderer.MdiDrawRange>)rangesField.GetValue(renderer)!;
|
||||||
|
ranges.Clear();
|
||||||
|
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 0, firstCommand: 0, commandCount: 1);
|
||||||
|
|
||||||
|
var allInstances = new List<InstanceData>();
|
||||||
|
for (int i = 0; i < instanceCount; i++)
|
||||||
|
{
|
||||||
|
allInstances.Add(new InstanceData
|
||||||
|
{
|
||||||
|
Transform = Matrix4x4.Identity,
|
||||||
|
CellId = 0x8C040100u + (uint)i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
device.Clear();
|
||||||
|
|
||||||
|
MethodInfo submitRhi = rendererType.GetMethod(
|
||||||
|
"SubmitRhi", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||||
|
submitRhi.Invoke(
|
||||||
|
renderer,
|
||||||
|
new object[] { allInstances, WbRenderPass.Opaque, 1, instanceCount });
|
||||||
|
|
||||||
|
IReadOnlyList<GpuRecordedCall> calls = device.Calls;
|
||||||
|
int alphaBindIndex = -1;
|
||||||
|
int firstDrawIndex = -1;
|
||||||
|
for (int i = 0; i < calls.Count; i++)
|
||||||
|
{
|
||||||
|
if (alphaBindIndex < 0
|
||||||
|
&& calls[i] is GpuRecordedStorageBind bind
|
||||||
|
&& bind.Binding == GpuBindingModel.StorageInstanceAlpha)
|
||||||
|
{
|
||||||
|
alphaBindIndex = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstDrawIndex < 0 && calls[i] is GpuRecordedMultiDrawIndirect)
|
||||||
|
firstDrawIndex = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(alphaBindIndex >= 0, "StorageInstanceAlpha was never bound.");
|
||||||
|
Assert.True(firstDrawIndex >= 0, "The seeded draw command was never recorded.");
|
||||||
|
Assert.True(
|
||||||
|
alphaBindIndex < firstDrawIndex,
|
||||||
|
"StorageInstanceAlpha must be bound before the pass's draw call, "
|
||||||
|
+ "not left to whatever a prior renderer's bind left in slot 7.");
|
||||||
|
|
||||||
|
var alphaBind = (GpuRecordedStorageBind)calls[alphaBindIndex];
|
||||||
|
Assert.Equal((uint)(instanceCount * sizeof(float)), alphaBind.SizeBytes);
|
||||||
|
ReadOnlySpan<float> alphaValues = MemoryMarshal.Cast<byte, float>(
|
||||||
|
device.RingBytes.Slice((int)alphaBind.OffsetBytes, (int)alphaBind.SizeBytes));
|
||||||
|
Assert.Equal(instanceCount, alphaValues.Length);
|
||||||
|
foreach (float value in alphaValues)
|
||||||
|
Assert.Equal(1.0f, value);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RetailDetailPipelinesPreserveOpaqueAndTransparentDepthWriteContracts()
|
public void RetailDetailPipelinesPreserveOpaqueAndTransparentDepthWriteContracts()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue