fix(render): production packed classifier carries the foliage flags; deferred-alpha replay keeps them (Campaign VM VM6 review 2)
Narrow re-review of 43e3abed found every A1-A5/A7/A8 item resolved but
one new blocker in the production packed classifier.
F1 (BLOCKER): RetailPViewPassExecutor.DrawPackedProductionRoute — the
route production world geometry actually draws from — never computed
FoliageFlags at all. WbDrawDispatcher.PackedOracle.cs's
ClassifyPackedBatches built its GroupKey with the field defaulting to
0u, and GetOrCreatePackedGroup never copied it onto the created
InstanceGroup, so production BatchData.flags bits 1/2 were always zero
for every scenery entity: the world geometry never swayed even though
the independently-classified shadow caster did, so shadows visibly
swayed under rigid trees. Both classifier call sites now compute
FoliageFlags via the identical FoliageWindClassification.Classify call
and entity-scoped HasCutoutSubset OR the classic (non-packed) path
uses, and GetOrCreatePackedGroup copies it exactly like
GetOrCreateInstanceGroup always has. The G2/G3 classified-output
digest (AddOpaqueSubmissionGroup/BuildTransparentSubmissionDigest) now
also folds GroupKey.FoliageFlags into its hash — present in the key
since round 1 but never actually read by either digest function, so a
content-level (not just group-count-level) classic-vs-packed
divergence is now caught.
F2 (medium): the delayed-alpha replay path (PrepareDeferredAlphaDraws)
hardcoded Flags = 1, dropping bits 1/2 for any group replayed through
it — a trunk instance promoted into the alpha-blend group mid-fade
(the #188 translucency-promotion case) would stop swaying for the
duration of its fade. Now 1u | key.FoliageFlags.
F3 (nit): ComputeEntityHasCutoutSubset's three call sites (classic,
caster, and the newly-fixed packed classifier) each allocated a
closure over _meshAdapter per Setup entity per frame. A new
context-taking overload passes the mesh adapter as an explicit
argument to a static lambda instead, letting the compiler cache one
delegate for the method's lifetime rather than allocating fresh ones.
A3 test gap: WbDrawDispatcher.BindDirectionalShadowReceiver is now
internal so DirectionalShadowGpuTests can drive it directly with a
bare RecordingGpuDevice pass encoder, proving it emits
UniformAtmosphericFrame with the exact buffer/offset/size a
DirectionalShadowFrameBinding carries — paired with the existing test
proving that binding carries the caster's real bind forward untouched.
F1's missing test: PackedDispatcherOracleTests chains
FoliageWindClassification.Classify (called with the packed
classifier's exact argument shape) for a real 0x8... scenery entity id
through BuildIndirectArrays — the same shared, already-tested
production step both classic and packed group lists feed into BatchData —
proving the resulting flags word carries bit 0x2. Driving
ClassifyPackedBatches/GetOrCreatePackedGroup directly was not a "cheap
test": both are private instance methods reachable only through the
full RetailPViewPassExecutor route, which needs a real IGpuDevice,
world-pass scope, mesh manager, and compiled pipelines to construct —
no test anywhere in the App test project stands one up.
Nits: F4 corrects foliage_wind.glsl's header comment from "bit 31" to
the top-nibble test; F5 documents at the receiver bind site that the
caster's own AtmosphericFrameBufferBinding has its seven ABI v1
members zero/Identity by construction (only the two v2 wind members
are valid) — safe today because mesh_atmospheric.vert reads that
binding solely for wind displacement, flagged as a footgun for a
future v1-reading addition to that shader; F6 notes in the plan
(rather than fixes) that EntityCacheEntry does not proactively
invalidate when FoliageWindExclusions changes on a pack switch —
harmless with the pack off, self-heals on the entry's next natural
eviction.
foliage_wind.glsl's F4 comment-only change updated the SPIR-V
manifest's source hashes for the five includers (mesh_atmospheric.vert
+ four directional_shadow_world_* casters); the compiled .spv bytes
are byte-identical since comments do not affect bytecode.
Verify: Release build 0 warnings/0 errors. App hermetic-lane filter
6,043/0 failed. Core.Tests 4,695/0 failed. RenderPackValidator 30/30.
Full hermetic-filtered solution: 15,271/0 failed across 15 projects.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
43e3abed4d
commit
a82959f1b7
10 changed files with 332 additions and 16 deletions
|
|
@ -467,6 +467,77 @@ public sealed class DirectionalShadowGpuTests
|
|||
Assert.Equal(atmosphericFrame.SizeBytes, binding.AtmosphericFrame.SizeBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign VM VM6 review fix round 2 (A3 test gap): the previous test
|
||||
/// proves the caster's real bind lands unchanged in
|
||||
/// DirectionalShadowFrameBinding.AtmosphericFrame. This proves the OTHER
|
||||
/// half of the seam — that WbDrawDispatcher.BindDirectionalShadowReceiver,
|
||||
/// given that exact binding, actually emits an encoder bind for
|
||||
/// UniformAtmosphericFrame with the SAME buffer/offset/size, not a stale
|
||||
/// or default one. Together the two tests prove the caster and receiver
|
||||
/// agree end to end without standing up the full
|
||||
/// WbDrawDispatcher/mesh-manager/world-pass-scope dependency chain —
|
||||
/// BindDirectionalShadowReceiver only needs a bare pass encoder.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BindDirectionalShadowReceiverEmitsAtmosphericFrameWithTheExactCasterBufferOffsetAndSize()
|
||||
{
|
||||
using var device = new RecordingGpuDevice();
|
||||
using IGpuBuffer shadowBuffer = Buffer(device, "shadow-frame", GpuBufferUsage.Uniform);
|
||||
using IGpuBuffer atmosphericBuffer = Buffer(device, "test-atmospheric-frame", GpuBufferUsage.Uniform);
|
||||
var binding = new DirectionalShadowFrameBinding(
|
||||
FrameSerial: 1,
|
||||
Enabled: true,
|
||||
Buffer: shadowBuffer,
|
||||
OffsetBytes: 0u,
|
||||
SizeBytes: 128u,
|
||||
TextureSlot: GpuTextureSlot.Unassigned,
|
||||
CascadeCount: 4,
|
||||
AtmosphericFrame: new AtmosphericFrameBufferBinding(
|
||||
atmosphericBuffer,
|
||||
OffsetBytes: 64u,
|
||||
SizeBytes: 192u));
|
||||
|
||||
device.Clear();
|
||||
var target = device.CreateRenderTarget(new GpuRenderTargetDescription(
|
||||
"test-world-hdr",
|
||||
640,
|
||||
480,
|
||||
GpuTextureFormat.Rgba16FloatRenderTarget,
|
||||
GpuTextureFormat.Depth24Stencil8,
|
||||
SampleCount: 1));
|
||||
using IGpuFrame frame = device.BeginFrame();
|
||||
using (IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
|
||||
{
|
||||
Name = "test-world-hdr",
|
||||
Color = new GpuColorAttachment(
|
||||
target,
|
||||
GpuLoadOp.Clear,
|
||||
GpuStoreOp.Store,
|
||||
Vector4.Zero),
|
||||
Depth = new GpuDepthAttachment(GpuLoadOp.Clear, GpuStoreOp.Store, 1f, 0),
|
||||
SampleCount = 1,
|
||||
}))
|
||||
{
|
||||
WbDrawDispatcher.BindDirectionalShadowReceiver(encoder, in binding);
|
||||
}
|
||||
frame.End();
|
||||
|
||||
GpuRecordedUniformBind atmosphericBind = Assert.Single(
|
||||
device.OfKind<GpuRecordedUniformBind>(),
|
||||
call => call.Binding == GpuBindingModel.UniformAtmosphericFrame);
|
||||
Assert.Equal("test-atmospheric-frame", atmosphericBind.BufferName);
|
||||
Assert.Equal(64u, atmosphericBind.OffsetBytes);
|
||||
Assert.Equal(192u, atmosphericBind.SizeBytes);
|
||||
|
||||
// The shadow-map binding fired too — BindDirectionalShadowReceiver
|
||||
// is not a no-op that only happens to satisfy the assertion above.
|
||||
GpuRecordedUniformBind shadowBind = Assert.Single(
|
||||
device.OfKind<GpuRecordedUniformBind>(),
|
||||
call => call.Binding == GpuBindingModel.UniformDirectionalShadow);
|
||||
Assert.Equal("shadow-frame", shadowBind.BufferName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StableTopology_ReusesRetainedCommandBuffersWithoutFrameRingCopies()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,10 +1,73 @@
|
|||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.Core.Meshing;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Wb;
|
||||
|
||||
public sealed class PackedDispatcherOracleTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Campaign VM VM6 review fix round 2 (F1 BLOCKER): the production
|
||||
/// packed classifier (WbDrawDispatcher.PackedOracle.cs's
|
||||
/// ClassifyPackedBatches/GetOrCreatePackedGroup) is a private instance
|
||||
/// pipeline reached only through the full RetailPViewPassExecutor →
|
||||
/// DrawPackedProductionRoute route, which needs a real IGpuDevice,
|
||||
/// world-pass scope, mesh manager, and compiled pipelines to construct —
|
||||
/// no test in this suite (or anywhere in the App test project) stands
|
||||
/// one up, so driving ClassifyPackedBatches/GetOrCreatePackedGroup
|
||||
/// directly is not a "cheap test." This proves the two halves of the
|
||||
/// fix that ARE cheaply testable and, chained together, prove the exact
|
||||
/// claim the review asked for: (1) FoliageWindClassification.Classify —
|
||||
/// called with the SAME argument shape ClassifyPackedBatches now uses
|
||||
/// (entity.LocalEntityId, exclusion membership, batch.Translucency,
|
||||
/// entity-scoped HasCutoutSubset) — resolves a real procedural-scenery
|
||||
/// entity id (0x8…) to CutoutFoliageFlag (0x2); (2) that flags value,
|
||||
/// carried on an IndirectGroupInput exactly like GetOrCreatePackedGroup
|
||||
/// now carries it on InstanceGroup.FoliageFlags, reaches the literal
|
||||
/// BatchData.flags word through BuildIndirectArrays — the same shared,
|
||||
/// already-tested production step both the classic and packed group
|
||||
/// lists feed into (WbDrawDispatcherIndirectBuilderTests pins bit 0;
|
||||
/// this pins bits 1/2 landing alongside it for a real scenery id).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SceneryCutoutEntityClassificationReachesTheBatchDataFlagsWordAsBit0x2()
|
||||
{
|
||||
const uint proceduralSceneryTreeId = 0x80010203u; // top nibble 0x8
|
||||
|
||||
uint foliageFlags = FoliageWindClassification.Classify(
|
||||
proceduralSceneryTreeId,
|
||||
isExcluded: false,
|
||||
TranslucencyKind.ClipMap,
|
||||
meshHasCutoutSubset: true);
|
||||
Assert.Equal(FoliageWindClassification.CutoutFoliageFlag, foliageFlags);
|
||||
Assert.Equal(0x2u, foliageFlags);
|
||||
|
||||
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
|
||||
{
|
||||
new(
|
||||
IndexCount: 12,
|
||||
FirstIndex: 0,
|
||||
BaseVertex: 0,
|
||||
InstanceCount: 1,
|
||||
FirstInstance: 0,
|
||||
TextureIndex: 0x5,
|
||||
TextureLayer: 0,
|
||||
Translucency: TranslucencyKind.ClipMap,
|
||||
FoliageFlags: foliageFlags),
|
||||
};
|
||||
var indirect = new DrawElementsIndirectCommand[4];
|
||||
var batches = new WbDrawDispatcher.BatchDataPublic[4];
|
||||
|
||||
WbDrawDispatcher.BuildIndirectArrays(groups, indirect, batches);
|
||||
|
||||
// Bit 0 (the #226 built-mesh marker) | bit 1 (cutout foliage) = 0x3;
|
||||
// isolating bit 1 with a mask proves the foliage classification
|
||||
// specifically reached the word, not merely that SOME nonzero value
|
||||
// did.
|
||||
Assert.Equal(0x2u, batches[0].Flags & 0x2u);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, 0u)]
|
||||
[InlineData(true, 1u)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue