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 (6c79d35c has 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:
Erik 2026-08-22 22:13:33 +02:00
parent 059703066f
commit 388457a735
3 changed files with 147 additions and 0 deletions

View file

@ -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
// shell instance belongs to.
int lightStride = LightManager.MaxLightsPerObject;
@ -218,6 +230,9 @@ public sealed unsafe partial class EnvCellRenderer
BindRingSection<uint>(
encoder, frame, GpuBindingModel.StorageClipSlots,
_clipSlotData.AsSpan(0, uniqueInstanceCount));
BindRingSection<float>(
encoder, frame, GpuBindingModel.StorageInstanceAlpha,
_instanceAlphaData.AsSpan(0, uniqueInstanceCount));
BindRingSection<float>(
encoder, frame, GpuBindingModel.StorageGlobalLights,
_globalLightData.AsSpan(

View file

@ -82,6 +82,18 @@ public sealed partial class EnvCellRenderer :
// slot 0 ⇒ no-clip.
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,
// like uViewProjection) instead of reading whatever WbDrawDispatcher last
// bound. Global point-light snapshot (same data/indices as the dispatcher,