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
|
|
@ -564,6 +564,54 @@ precision at far landblock corners) and A8 (`FoliageWindExclusions` as a
|
|||
`FrozenSet`) also landed; A7 is the divergence note on the `max(maxHeight,
|
||||
0.5)` guard above.
|
||||
|
||||
**Review fix round 2 (2026-08-23):** a narrow re-review found one more
|
||||
blocker. (F1) The PRODUCTION packed classifier (`WbDrawDispatcher
|
||||
.PackedOracle.cs`'s `ClassifyPackedBatches`/`GetOrCreatePackedGroup` — the
|
||||
route `RetailPViewPassExecutor.DrawPackedProductionRoute` actually draws
|
||||
from) never computed `FoliageFlags` at all: it built its `GroupKey` with the
|
||||
field defaulting to `0u` and 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 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 — previously present in the key but never actually read by
|
||||
either digest function, so a content-level (not just 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)
|
||||
`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. A3's test gap also
|
||||
closed: `DirectionalShadowGpuTests` now has a companion test proving
|
||||
`WbDrawDispatcher.BindDirectionalShadowReceiver` (made `internal` for this)
|
||||
actually emits the `UniformAtmosphericFrame` bind with the exact
|
||||
buffer/offset/size a `DirectionalShadowFrameBinding` carries, pairing with
|
||||
the existing test that proves that binding carries the caster's real bind
|
||||
forward untouched. F4 (the `foliage_wind.glsl` header comment) and F5 (a
|
||||
comment at the receiver bind site: the caster's own `AtmosphericFrameBuffer-
|
||||
Binding` 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 this binding solely for wind displacement, a footgun for a
|
||||
future v1-reading addition to that shader) also landed. F6, noted rather
|
||||
than fixed: `EntityClassificationCache`'s `EntityCacheEntry` bakes
|
||||
`GroupKey.FoliageFlags` (hence exclusion membership) in at classification
|
||||
time and is not proactively invalidated when `FoliageWindExclusions`
|
||||
changes — a newly-excluded or newly-included object can show its previous
|
||||
classification until the entity's cache entry is next evicted (e.g. a
|
||||
landblock demote/reload) rather than immediately on a pack switch. Harmless
|
||||
with the pack off (exclusions are pack-scoped) and self-heals on the next
|
||||
natural cache eviction; not worth a proactive invalidation sweep for a
|
||||
rarely-changing, pack-scoped list.
|
||||
|
||||
## VM7 — Closeout and merge
|
||||
|
||||
- Full gates: `tools/run-release-gate.ps1` (hermetic lanes), the AR
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
#define ACDREAM_FOLIAGE_WIND_GLSL
|
||||
|
||||
// Campaign VM VM6 (2026-08-22): weather-driven foliage sway for procedural
|
||||
// scenery (trees/bushes — see ProceduralSceneryIdAllocator, bit 31 of the
|
||||
// entity id). Shared verbatim between mesh_atmospheric.vert (the world
|
||||
// scenery (trees/bushes — see ProceduralSceneryIdAllocator.IsInNamespace,
|
||||
// the top nibble 0x8 of the entity id — NOT bit 31 alone, which also
|
||||
// matches LandblockStaticEntityIdAllocator's 0xC... namespace). Shared
|
||||
// verbatim between mesh_atmospheric.vert (the world
|
||||
// receiver) and the four directional_shadow_world_*.vert casters so a
|
||||
// displaced leaf's shadow moves with it by construction — the caster and
|
||||
// receiver read the identical uAtmosphereClockWind/uAtmosphereWindAmplitude
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "ae6026d4bc04bc07e5c67c1dca6c3dfd16ad377104153808a2e11b60bbe492f7",
|
||||
"sourceSha256": "a30acec1fd60fcbc8a0cfa377726a65077a82836cbcf730cbcc756ce1b9299c5",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "c26158346a1db03e305192a23b595d288ed27a3622b8c2b49cc2fe6b27550946",
|
||||
"sourceSha256": "7651a9622227c0322f75c8e3af2685bceb9bc28b692b53df305ec73161e589ff",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -183,7 +183,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "c752d8f85e0aa4a5bd2b7cb8679276ba46850844362eef0efce885401021433b",
|
||||
"sourceSha256": "9513c5baa4f269a9ddc265e9fa49c71a2834a004aca36939b99f9b5067d25ed1",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -199,7 +199,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "e483aae19e35ad9bdd7226cb1604db33268f8ae4474d774dfa08d5ca681717ee",
|
||||
"sourceSha256": "edf89ab8a8c02bf8fc3099b091ca3508a5b9c18e2ebbfe5bb5a7d6c50a82c745",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -215,7 +215,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "949a50149a6dd7f8f2b44fa035ab80a4227a71a1cbd4ec812bf451d1c2971db3",
|
||||
"sourceSha256": "a00aa83a8bd8ab4791b60bfb967a80b43bafcb0bf758f5e2e056b4272f3c8b1b",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -81,6 +81,33 @@ internal static class FoliageWindClassification
|
|||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign VM VM6 review fix round 2 (F3): zero-alloc production
|
||||
/// overload of <see cref="ComputeEntityHasCutoutSubset{T}"/>. The
|
||||
/// single-generic overload above is exactly what a caller needs to
|
||||
/// predicate on an instance field (e.g. the mesh adapter) — but doing so
|
||||
/// with a lambda that closes over <c>this</c> allocates a fresh closure
|
||||
/// object AND a fresh delegate on every call, and this is called once
|
||||
/// per Setup entity per frame from the world receiver, the shadow
|
||||
/// caster, and the packed production classifier. Passing the would-be-
|
||||
/// captured value as an explicit <typeparamref name="TContext"/>
|
||||
/// argument to a <see langword="static"/> lambda (no captures at all)
|
||||
/// lets the C# compiler cache a single delegate instance for the
|
||||
/// method's lifetime instead of allocating one per call.
|
||||
/// </summary>
|
||||
internal static bool ComputeEntityHasCutoutSubset<T, TContext>(
|
||||
IReadOnlyList<T> setupParts,
|
||||
TContext context,
|
||||
Func<TContext, T, bool> hasCutoutSubset)
|
||||
{
|
||||
for (int i = 0; i < setupParts.Count; i++)
|
||||
{
|
||||
if (hasCutoutSubset(context, setupParts[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The full top-nibble <c>0x8...</c> test via
|
||||
/// <see cref="ProceduralSceneryIdAllocator.IsInNamespace"/> — NOT bit 31
|
||||
|
|
|
|||
|
|
@ -108,7 +108,12 @@ public sealed partial class WbDrawDispatcher
|
|||
return PipelinesFor(encoder);
|
||||
}
|
||||
|
||||
private static void BindDirectionalShadowReceiver(
|
||||
// Campaign VM VM6 review fix round 2 (A3 test gap): internal, not
|
||||
// private, so DirectionalShadowReceiverTests can drive it directly with
|
||||
// a bare RecordingGpuDevice pass encoder instead of standing up the
|
||||
// full WbDrawDispatcher/mesh-manager/world-pass-scope dependency chain
|
||||
// just to prove this one bind call is correct.
|
||||
internal static void BindDirectionalShadowReceiver(
|
||||
IGpuPassEncoder encoder,
|
||||
in DirectionalShadowFrameBinding binding)
|
||||
{
|
||||
|
|
@ -127,6 +132,20 @@ public sealed partial class WbDrawDispatcher
|
|||
// depending on that earlier bind surviving un-reset until this pass
|
||||
// runs. Unbound (the declared-pack / no-caster-this-frame case) is a
|
||||
// no-op here exactly like the caster side.
|
||||
//
|
||||
// Review fix round 2 (F5): the buffer bound here is the CASTER's own
|
||||
// independent allocation (AtmosphericPostProcessGraph
|
||||
// .BuildShadowAtmosphericFrameBinding), NOT the receiver's/post-
|
||||
// process's fuller frame block. Its seven ABI v1 members (sun
|
||||
// screen/color, viewport, weather, sun direction, policy, inverse-
|
||||
// view-projection) are ALL ZERO/Identity by construction — only the
|
||||
// two appended v2 members (clock/wind, wind amplitude) carry real
|
||||
// content. That is safe today because mesh_atmospheric.vert reads
|
||||
// this binding SOLELY for foliage_wind.glsl's wind displacement; if
|
||||
// a future change makes mesh_atmospheric.vert or .frag read any v1
|
||||
// member (sun/weather/policy/etc.) during this same world-mesh
|
||||
// pass, it will read zeros here and must get its own real binding
|
||||
// instead of assuming this one is fully populated.
|
||||
if (binding.AtmosphericFrame.IsBound)
|
||||
{
|
||||
encoder.BindUniformBuffer(
|
||||
|
|
|
|||
|
|
@ -1028,11 +1028,16 @@ public sealed partial class WbDrawDispatcher
|
|||
// FoliageWindClassification.ComputeEntityHasCutoutSubset's
|
||||
// doc comment. Same shared helper the world-receiver
|
||||
// Setup-part loop uses, so the caster's trunk-part
|
||||
// classification agrees with the receiver's.
|
||||
// classification agrees with the receiver's. Review
|
||||
// fix round 2 (F3): the context-taking overload
|
||||
// passes _meshAdapter explicitly to a static lambda
|
||||
// instead of closing over it — zero allocation per
|
||||
// entity per frame.
|
||||
bool entityHasCutoutSubset = FoliageWindClassification
|
||||
.ComputeEntityHasCutoutSubset(
|
||||
renderData.SetupParts,
|
||||
part => _meshAdapter.TryGetRenderData(part.GfxObjId)
|
||||
_meshAdapter,
|
||||
static (adapter, part) => adapter.TryGetRenderData(part.GfxObjId)
|
||||
is { HasCutoutSubset: true });
|
||||
|
||||
for (int setupPartIndex = 0;
|
||||
|
|
|
|||
|
|
@ -437,6 +437,25 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
if (renderData.IsSetup
|
||||
&& renderData.SetupParts.Count > 0)
|
||||
{
|
||||
// Campaign VM VM6 review fix round 2 (F1): same entity-
|
||||
// scoped HasCutoutSubset OR the classic world-receiver loop
|
||||
// computes (WbDrawDispatcher.cs) and the caster loop
|
||||
// (DirectionalShadows.cs) — see
|
||||
// FoliageWindClassification.ComputeEntityHasCutoutSubset's
|
||||
// doc comment. A Setup composite's parts are separate
|
||||
// GfxObjs with independently-cached HasCutoutSubset; without
|
||||
// this bounded per-entity scan, the opaque trunk part of a
|
||||
// production-classified tree would never get the trunk
|
||||
// flag. The context-taking overload passes _meshAdapter
|
||||
// explicitly to a static lambda (F3) — zero allocation per
|
||||
// entity per frame.
|
||||
bool entityHasCutoutSubset = FoliageWindClassification
|
||||
.ComputeEntityHasCutoutSubset(
|
||||
renderData.SetupParts,
|
||||
_meshAdapter,
|
||||
static (adapter, part) => adapter.TryGetRenderData(part.GfxObjId)
|
||||
is { HasCutoutSubset: true });
|
||||
|
||||
for (int setupPartIndex = 0;
|
||||
setupPartIndex < renderData.SetupParts.Count;
|
||||
setupPartIndex++)
|
||||
|
|
@ -477,7 +496,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
indoor,
|
||||
selectionLighting,
|
||||
opacity,
|
||||
cacheEntry))
|
||||
cacheEntry,
|
||||
entityHasCutoutSubset))
|
||||
{
|
||||
reusableAcrossFrames = false;
|
||||
}
|
||||
|
|
@ -593,8 +613,17 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
bool indoor,
|
||||
Vector2 selectionLighting,
|
||||
float opacity,
|
||||
PackedProjectionClassificationEntry? cacheEntry)
|
||||
PackedProjectionClassificationEntry? cacheEntry,
|
||||
// Campaign VM VM6 review fix round 2 (F1): mirrors the classic
|
||||
// dispatcher's ClassifyBatches override exactly. HasCutoutSubset is
|
||||
// a per-PART (per-GfxObj) fact; the caller passes the ENTITY/Setup-
|
||||
// scoped OR across every resolved part for a Setup composite. null
|
||||
// (the default) means "use renderData.HasCutoutSubset directly",
|
||||
// which is already correct for a non-Setup single-mesh entity.
|
||||
bool? entityHasCutoutSubsetOverride = null)
|
||||
{
|
||||
bool entityHasCutoutSubset =
|
||||
entityHasCutoutSubsetOverride ?? renderData.HasCutoutSubset;
|
||||
bool reusableAcrossFrames = true;
|
||||
for (int batchIndex = 0;
|
||||
batchIndex < renderData.Batches.Count;
|
||||
|
|
@ -617,6 +646,22 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
if (!texture.Slot.IsAssigned)
|
||||
continue;
|
||||
|
||||
// Campaign VM VM6 review fix round 2 (F1 BLOCKER): the packed
|
||||
// production classifier never computed FoliageFlags, so the
|
||||
// production BatchData.flags word was always 0 for every
|
||||
// scenery entity — the world geometry never swayed even though
|
||||
// the independently-classified shadow caster did. Classify
|
||||
// BEFORE constructing the key, from the RAW (pre-#188-
|
||||
// promotion) batch.Translucency, exactly as the classic
|
||||
// ClassifyBatches does — see that method's own comment for why
|
||||
// raw translucency is used for classification but the (possibly
|
||||
// promoted) local `translucency` is still what the key/group
|
||||
// partitions draws by.
|
||||
uint foliageFlags = FoliageWindClassification.Classify(
|
||||
entity.LocalEntityId,
|
||||
FoliageWindExclusions.Contains(meshRef.GfxObjId),
|
||||
batch.Translucency,
|
||||
entityHasCutoutSubset);
|
||||
var key = new GroupKey(
|
||||
batch.FirstIndex,
|
||||
(int)batch.BaseVertex,
|
||||
|
|
@ -624,7 +669,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
texture.Slot,
|
||||
texture.Layer,
|
||||
translucency,
|
||||
batch.CullMode);
|
||||
batch.CullMode,
|
||||
foliageFlags);
|
||||
var classified = new PackedClassifiedBatch(
|
||||
key,
|
||||
restPose,
|
||||
|
|
@ -759,6 +805,17 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
TextureLayer = key.TextureLayer,
|
||||
Translucency = key.Translucency,
|
||||
CullMode = key.CullMode,
|
||||
// Campaign VM VM6 review fix round 2 (F1 BLOCKER): this was the
|
||||
// missing copy. GetOrCreateInstanceGroup (the classic route) has
|
||||
// set FoliageFlags from key.FoliageFlags since the round-1
|
||||
// fix; this packed-route sibling never did, so every packed
|
||||
// production InstanceGroup carried FoliageFlags == 0 regardless
|
||||
// of what ClassifyPackedBatches computed into the key —
|
||||
// BatchData.flags bits 1/2 were always 0 for production world
|
||||
// geometry even though the independently-classified shadow
|
||||
// caster set them correctly, so casters swayed while the
|
||||
// meshes they shadowed did not.
|
||||
FoliageFlags = key.FoliageFlags,
|
||||
Registration = _nextPackedGroupRegistration++,
|
||||
LastUsedFrame = _packedGroupFrame,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1855,9 +1855,14 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
// each part's own. A part missing this frame contributes
|
||||
// nothing; the entity is already marked incomplete by the
|
||||
// loop below and reclassifies once every part has loaded.
|
||||
// Review fix round 2 (F3): the context-taking overload
|
||||
// passes _meshAdapter as an explicit argument to a static
|
||||
// lambda instead of closing over it, so this call allocates
|
||||
// nothing per entity per frame.
|
||||
bool entityHasCutoutSubset = FoliageWindClassification.ComputeEntityHasCutoutSubset(
|
||||
renderData.SetupParts,
|
||||
part => _meshAdapter.TryGetRenderData(part.GfxObjId) is { HasCutoutSubset: true });
|
||||
_meshAdapter,
|
||||
static (adapter, part) => adapter.TryGetRenderData(part.GfxObjId) is { HasCutoutSubset: true });
|
||||
|
||||
// #188: setupPartIndex is the SAME index space
|
||||
// TransparentPartHook.PartIndex addresses — retail's CPartArray
|
||||
|
|
@ -2576,6 +2581,12 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
hash.Add(key.TextureLayer);
|
||||
hash.Add((int)key.Translucency);
|
||||
hash.Add((int)key.CullMode);
|
||||
// Review fix round 2 (F1): fold FoliageFlags into the G2/G3
|
||||
// classified-output digest so a scenery/non-scenery group-
|
||||
// partition divergence between the classic and packed
|
||||
// dispatchers is caught by content, not only by an incidental
|
||||
// group-count difference.
|
||||
hash.Add(key.FoliageFlags);
|
||||
hash.Add(entry.ViewerDistance);
|
||||
AddSubmissionInstance(
|
||||
ref hash,
|
||||
|
|
@ -2626,6 +2637,10 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
hash.Add(key.TextureLayer);
|
||||
hash.Add((int)key.Translucency);
|
||||
hash.Add((int)key.CullMode);
|
||||
// Review fix round 2 (F1): see the matching comment in
|
||||
// BuildTransparentSubmissionDigest — folds FoliageFlags into the
|
||||
// G2/G3 classified-output digest.
|
||||
hash.Add(key.FoliageFlags);
|
||||
hash.Add(group.Matrices.Count);
|
||||
|
||||
ulong xorLow = 0;
|
||||
|
|
@ -2786,8 +2801,17 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
// DrawMesh invokes RenderMeshSubset with detail enabled for
|
||||
// every built-mesh material subset while curr_detail_surface
|
||||
// is installed. The per-instance category still rejects
|
||||
// ordinary objects in mesh_detail.
|
||||
Flags = 1,
|
||||
// ordinary objects in mesh_detail. Review fix round 2 (F2):
|
||||
// bit 0 (the #226 built-mesh marker) is unconditional here —
|
||||
// every delayed-alpha entry IS a built-mesh alpha submission
|
||||
// — but the previous hardcoded `1` silently dropped bits 1/2
|
||||
// (Campaign VM VM6 foliage classification). A trunk instance
|
||||
// promoted into the alpha-blend group mid-fade (translucency
|
||||
// < 1 forces AlphaBlend even for an otherwise-Opaque trunk —
|
||||
// see ClassifyBatches's #188 promotion) must keep swaying
|
||||
// through the deferred-alpha replay instead of going rigid
|
||||
// for the duration of its fade.
|
||||
Flags = 1u | key.FoliageFlags,
|
||||
};
|
||||
_indirectCommands[i] = new DrawElementsIndirectCommand
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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