fix(render): correct foliage-wind classification, receiver/caster desync, and frame binding (Campaign VM VM6 review)
Opus dual-lens review of the three VM6 commits (0930c35d,39e8408c,6cc5e183) found two blockers and two should-fix issues; all landed here along with the review's nits and documentation corrections. Blockers: - A1: the procedural-scenery classifier tested bit 31 alone instead of the full top nibble (0xF000_0000 == 0x8000_0000), so it also matched LandblockStaticEntityIdAllocator's 0xC... namespace (fences/gates/ building shells with a cutout subset), the 0xDA11_D0xx paperdoll id, and the 0xFFFF_FF01 portal-tunnel id as procedural scenery — all three would have swayed. ProceduralSceneryIdAllocator.IsInNamespace now does the exact top-nibble test; FoliageWindClassification delegates to it. - A2: GroupKey (the receiver's instance-batching key) did not carry FoliageFlags while the caster's dedup key already did, so a scenery instance and a non-scenery instance sharing a mesh subset coalesced into one receiver InstanceGroup whose flags were last-writer-wins — disagreeing with the correctly-keyed caster. GroupKey now carries FoliageFlags, computed before key construction and set exactly once at group creation; the imperative re-stamp is gone, and CachedBatch's now-redundant FoliageFlags field is removed. Should-fix: - A3: the world receiver pass bound UniformAtmosphericFrame only by accident (leftover from the caster pass, which runs first each frame, since Vulkan binding state isn't reset between passes). DirectionalShadowFrameBinding now carries the caster's exact AtmosphericFrameBufferBinding and BindDirectionalShadowReceiver binds it explicitly. - A4: a Setup-composed tree's opaque trunk part never got the trunk flag because HasCutoutSubset is cached per GfxObj part, not per entity. FoliageWindClassification.ComputeEntityHasCutoutSubset now ORs HasCutoutSubset across an entity's resolved sibling parts once per entity, threaded into ClassifyBatches/AddDirectionalShadowBatches via a new optional override parameter. Nits: A5 hashes the per-vertex flutter seed relative to the instance origin instead of absolute world XY (fp32 sin() precision loss at far landblock corners), mirrored in both foliage_wind.glsl and FoliageWindModel; A7 documents the max(maxHeight, 0.5) divide-guard as a deliberate pseudocode divergence; A8 switches FoliageWindExclusions' construction to ToFrozenSet() and softens the "never stale" doc comment to "no slower than one frame behind." Tests added: top-nibble classification (0xFFFFFFFFu now correctly false), GroupKey inequality across entity-driven scenery/landblock- static classification, a caster-batch test proving the same pairing never coalesces, ComputeEntityHasCutoutSubset unit + end-to-end two-part-Setup tests, the caster→receiver AtmosphericFrame binding carry-through, flutter-hash translation invariance relative to instance origin, and a Storm-wind mid-height displacement floor guarding against a "no motion" regression. Docs: plan VM6 body corrected to the five-row WeatherKind table, "bits 1 and 2", "all four" caster shaders, and top-nibble wording throughout; the owner gate checklist's Rain/Storm step; the stale v1-only shader- interface compatibility entry; semantic-bindings-v1.md's v2 members folded into the main 192-byte block; the IA-25 register row's top- nibble wording; AtmosphericFrameInputs.cs's ABI size reference. foliage_wind.glsl's A5 change recompiled exactly the five shaders that include it (mesh_atmospheric.vert, the four directional_shadow_world_* casters) plus the manifest; no other .spv changed. Verify: Release build 0 warnings/0 errors. App hermetic-lane filter 6,041/0 failed (no environment-specific failures this run). RenderPackValidator 30/30. Full hermetic-filtered solution: 15,269/0 failed across 15 projects. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
27a446f05c
commit
43e3abed4d
28 changed files with 724 additions and 123 deletions
|
|
@ -14,7 +14,19 @@ internal readonly record struct DirectionalShadowFrameBinding(
|
|||
uint OffsetBytes,
|
||||
uint SizeBytes,
|
||||
GpuTextureSlot TextureSlot,
|
||||
int CascadeCount)
|
||||
int CascadeCount,
|
||||
// Campaign VM VM6 review fix round (A3): the SAME AtmosphericFrame
|
||||
// binding the caster pass bound this frame (see
|
||||
// DirectionalSunShadowRenderer.RenderPrepared's atmosphericFrame
|
||||
// parameter), carried on this existing per-frame seam so the world
|
||||
// receiver pass (mesh_atmospheric.vert, which reads
|
||||
// uAtmosphereClockWind/uAtmosphereWindAmplitude) binds set 3/binding 5
|
||||
// itself instead of relying on whatever the caster pass happened to
|
||||
// leave bound earlier in the frame. Unbound (default) is valid — it
|
||||
// just means no caster ran this frame (or the source never supplied
|
||||
// one, e.g. a declared pack); BindDirectionalShadowReceiver skips
|
||||
// binding 5 in that case exactly like the caster side does.
|
||||
AtmosphericFrameBufferBinding AtmosphericFrame = default)
|
||||
{
|
||||
internal static DirectionalShadowFrameBinding Disabled => default;
|
||||
|
||||
|
|
|
|||
|
|
@ -590,11 +590,15 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
|||
DirectionalShadowUniforms.SizeInBytes);
|
||||
// Campaign VM VM6: the caster pass reads foliage-wind clock/
|
||||
// amplitude inputs through the same shared AtmosphericFrame
|
||||
// block the world receiver binds (set 3/binding 5), so a
|
||||
// displaced leaf's shadow moves with it by construction. Bound
|
||||
// only when the caller supplied one — declared (non-built-in)
|
||||
// packs leave this unbound and their caster shaders, which never
|
||||
// declare binding 5, are unaffected.
|
||||
// block (set 3/binding 5) the world receiver pass ALSO
|
||||
// explicitly binds — this exact buffer/offset is carried
|
||||
// forward on DirectionalShadowFrameBinding.AtmosphericFrame
|
||||
// (review fix round A3) so WbDrawDispatcher.BindDirectionalShadowReceiver
|
||||
// binds it itself rather than depending on this caster bind
|
||||
// surviving un-reset until the receiver pass runs later in the
|
||||
// frame. Bound only when the caller supplied one — declared
|
||||
// (non-built-in) packs leave this unbound and their caster
|
||||
// shaders, which never declare binding 5, are unaffected.
|
||||
if (atmosphericFrame.IsBound)
|
||||
{
|
||||
encoder.BindUniformBuffer(
|
||||
|
|
@ -647,7 +651,11 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
|||
uniformAllocation.OffsetBytes,
|
||||
DirectionalShadowUniforms.SizeInBytes,
|
||||
_textureSlot,
|
||||
cascadeCount);
|
||||
cascadeCount,
|
||||
// Campaign VM VM6 review fix round (A3): the exact same
|
||||
// buffer/offset the caster pass just bound above, carried
|
||||
// forward so the receiver pass can bind it itself.
|
||||
AtmosphericFrame: atmosphericFrame);
|
||||
|
||||
(bool hasGpu, double gpuMilliseconds) = ResolveGpu(cascadeCount);
|
||||
int drawsPerCascade = terrainDraws.Commands.IsEmpty ? 0 : 1;
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ internal sealed class AtmosphericFrameInputState : IAtmosphericWorldFrameSink
|
|||
/// six std140 vec4 values, one mat4, then two more std140 vec4 values —
|
||||
/// 192 bytes. The two appended members carry the foliage-wind clock/weather
|
||||
/// and amplitude inputs <c>foliage_wind.glsl</c> reads; every earlier member
|
||||
/// keeps its ABI v1 offset. See <see cref="RenderPackShaderAbi.AtmosphericFrameSizeBytesV1"/>
|
||||
/// keeps its ABI v1 offset. See <see cref="RenderPackShaderAbi.AtmosphericFrameSizeBytes"/>
|
||||
/// and <c>atmospheric_common.glsl</c> for the byte-level contract both
|
||||
/// backends and the SPIR-V validator agree on.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Frozen;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
|
@ -312,7 +313,7 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
userSettingOverrides);
|
||||
_foliageWindExclusions =
|
||||
(descriptor.AtmospherePolicy?.FoliageExclusions
|
||||
?? (IReadOnlyList<uint>)[]).ToHashSet();
|
||||
?? (IReadOnlyList<uint>)[]).ToFrozenSet();
|
||||
_windClockSecondsOverride = windClockSecondsOverride;
|
||||
_packSettings = PackSettingsUniforms.Create(
|
||||
descriptor,
|
||||
|
|
|
|||
|
|
@ -70,9 +70,14 @@ internal static class FoliageWindModel
|
|||
if ((batchFlags & FoliageWindClassification.CutoutFoliageFlag) != 0u)
|
||||
{
|
||||
float branch = k * amplitude.Y * s * MathF.Sin((1.1f * t) + ph + (2.0f * h));
|
||||
var worldXY = new Vector2(worldPos.X, worldPos.Y);
|
||||
// Review fix round A5: relative to the instance origin, not
|
||||
// absolute world XY — matches foliage_wind.glsl exactly (see
|
||||
// its comment for the fp32-precision-at-far-landblocks reason).
|
||||
var relativeXY = new Vector2(
|
||||
worldPos.X - instanceOrigin.X,
|
||||
worldPos.Y - instanceOrigin.Y);
|
||||
float vh = Frac(
|
||||
MathF.Sin(Vector2.Dot(worldXY, new Vector2(12.9898f, 78.233f))) * 43758.5453f);
|
||||
MathF.Sin(Vector2.Dot(relativeXY, new Vector2(12.9898f, 78.233f))) * 43758.5453f);
|
||||
float flutter = h * amplitude.Z * s * MathF.Sin((6.0f * t) + (7.0f * vh));
|
||||
d += (dir * branch)
|
||||
+ (perp * 0.35f * branch)
|
||||
|
|
|
|||
|
|
@ -72,10 +72,15 @@ vec3 acdreamFoliageDisplace(
|
|||
{
|
||||
// Cutout (leaves): branch swing at ~1 Hz with a phase that runs up
|
||||
// the tree, plus a fast per-vertex flutter decorrelated by a hash of
|
||||
// the vertex's own world XY so leaves on the same tree do not move
|
||||
// in lockstep.
|
||||
// the vertex's position RELATIVE TO ITS OWN INSTANCE ORIGIN (not
|
||||
// absolute world XY — review fix round A5: a landblock far from the
|
||||
// world origin can put worldPos.xy in the tens of thousands, and
|
||||
// sin() of a large fp32 argument loses precision fast, which showed
|
||||
// up as visibly patterned/quantized flutter near far map corners)
|
||||
// so leaves on the same tree do not move in lockstep.
|
||||
float branch = k * amp.y * s * sin(1.1 * t + ph + 2.0 * h);
|
||||
float vh = fract(sin(dot(worldPos.xy, vec2(12.9898, 78.233))) * 43758.5453);
|
||||
vec2 relativeXY = worldPos.xy - instanceOrigin.xy;
|
||||
float vh = fract(sin(dot(relativeXY, vec2(12.9898, 78.233))) * 43758.5453);
|
||||
float flutter = h * amp.z * s * sin(6.0 * t + 7.0 * vh);
|
||||
d += dir * branch + perp * 0.35 * branch
|
||||
+ vec2(cos(6.2832 * vh), sin(6.2832 * vh)) * flutter;
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -151,7 +151,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "fe67e9bff0e528b08fa699f0be17d1c49660ad63ab34f7ac7dd31f79a2253006",
|
||||
"sourceSha256": "ae6026d4bc04bc07e5c67c1dca6c3dfd16ad377104153808a2e11b60bbe492f7",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "2a155a7964ff15bd936394b8ffd3a331d5598e140ca36268759520bab86b1bdc",
|
||||
"sourceSha256": "c26158346a1db03e305192a23b595d288ed27a3622b8c2b49cc2fe6b27550946",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -183,7 +183,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "74b1aa64396dd74185d644de7e9f40626c9422f335da60deb7c606f2a45d92c2",
|
||||
"sourceSha256": "c752d8f85e0aa4a5bd2b7cb8679276ba46850844362eef0efce885401021433b",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -199,7 +199,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "f15425837f7a8f84337f4493989bcd7cb1af87f916f7678fc016947f68d26be5",
|
||||
"sourceSha256": "e483aae19e35ad9bdd7226cb1604db33268f8ae4474d774dfa08d5ca681717ee",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -215,7 +215,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "7e3e49483024145bb71929f715a68087380e5126e15b5bef2c1c5c7d5e2e3516",
|
||||
"sourceSha256": "949a50149a6dd7f8f2b44fa035ab80a4227a71a1cbd4ec812bf451d1c2971db3",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,15 +27,7 @@ internal readonly record struct CachedBatch(
|
|||
Matrix4x4 RestPose,
|
||||
Vector3 LocalSortCenter = default,
|
||||
WbDrawDispatcher.InstanceGroup? Group = null,
|
||||
long GroupRegistration = 0,
|
||||
// Campaign VM VM6: the foliage-wind classification bits ClassifyBatches
|
||||
// computed for this subset (0 for anything that isn't procedural-scenery
|
||||
// foliage). Preserved here — not just on the live InstanceGroup — so a
|
||||
// cache-hit replay that recreates an evicted InstanceGroup (see
|
||||
// WbDrawDispatcher.ApplyCacheHitDirect's stale-registration path)
|
||||
// re-stamps the correct flags instead of leaving a freshly-recreated
|
||||
// group at its zero default.
|
||||
uint FoliageFlags = 0u);
|
||||
long GroupRegistration = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Immutable retail-picking descriptor for one static entity part. The cache
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
|
|
@ -29,8 +30,6 @@ internal static class FoliageWindClassification
|
|||
/// </summary>
|
||||
internal const uint TrunkFlag = 0x4u;
|
||||
|
||||
private const uint SceneryEntityIdBit = 0x8000_0000u;
|
||||
|
||||
/// <summary>
|
||||
/// Pure classification: no allocation, no scan. <paramref name="meshHasCutoutSubset"/>
|
||||
/// is expected to be computed once per GfxObj/Setup mesh and cached with
|
||||
|
|
@ -53,10 +52,44 @@ internal static class FoliageWindClassification
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bit 31 of the entity id — see <c>ProceduralSceneryIdAllocator</c>'s
|
||||
/// <c>0x8XXYYIII</c> namespace. No other consumer decodes more than this
|
||||
/// one bit.
|
||||
/// Campaign VM VM6 review fix round (A4): a Setup composite's parts are
|
||||
/// separate GfxObjs, each with its own independently-cached
|
||||
/// <c>ObjectRenderData.HasCutoutSubset</c> — an opaque trunk part carries
|
||||
/// no cutout batches of its own (the leaves are a DIFFERENT part), so
|
||||
/// classifying it from only its own <c>HasCutoutSubset</c> never gives
|
||||
/// it <see cref="TrunkFlag"/>. Both the world-receiver and
|
||||
/// directional-shadow-caster Setup-part walks call this ONCE per entity
|
||||
/// per frame (not per batch) to OR every currently-resolved part's
|
||||
/// <c>HasCutoutSubset</c> into one entity-scoped value before
|
||||
/// classifying each part against it. Generic over the caller's own part
|
||||
/// representation so it needs no dependency on <c>ObjectRenderData</c>
|
||||
/// or the mesh adapter — the caller supplies <paramref name="hasCutoutSubset"/>
|
||||
/// to look each part's value up however it already does. Short-circuits
|
||||
/// on the first <see langword="true"/>; a part the caller cannot
|
||||
/// currently resolve simply contributes nothing (the caller marks the
|
||||
/// entity incomplete separately and reclassifies once every part loads).
|
||||
/// </summary>
|
||||
internal static bool ComputeEntityHasCutoutSubset<T>(
|
||||
IReadOnlyList<T> setupParts,
|
||||
Func<T, bool> hasCutoutSubset)
|
||||
{
|
||||
for (int i = 0; i < setupParts.Count; i++)
|
||||
{
|
||||
if (hasCutoutSubset(setupParts[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The full top-nibble <c>0x8...</c> test via
|
||||
/// <see cref="ProceduralSceneryIdAllocator.IsInNamespace"/> — NOT bit 31
|
||||
/// alone. Bit 31 alone also matches <c>LandblockStaticEntityIdAllocator</c>'s
|
||||
/// <c>0xC...</c> ids (fences, gates, building shells) and the synthetic
|
||||
/// render ids <c>0xDA11_D0xx</c> / <c>0xFFFF_FF01</c> — using bit 31
|
||||
/// alone here would sway non-scenery objects that happen to have a
|
||||
/// cutout subset.
|
||||
/// </summary>
|
||||
internal static bool IsProceduralScenery(uint entityId) =>
|
||||
(entityId & SceneryEntityIdBit) != 0u;
|
||||
ProceduralSceneryIdAllocator.IsInNamespace(entityId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,16 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// (<c>CompareOpaqueSubmissionOrder</c> / <c>CompareTransparentSubmissionOrder</c>),
|
||||
/// and the delayed-alpha path sorts by viewer distance then submission ordinal.
|
||||
/// The key reaches only equality, hashing, and the scene-digest fingerprints.</para>
|
||||
///
|
||||
/// <para>Campaign VM VM6 review fix round: <see cref="FoliageFlags"/> joined
|
||||
/// the key so a mesh subset reachable from BOTH a procedural-scenery entity
|
||||
/// and a non-scenery entity (same index range/texture/translucency/cull
|
||||
/// mode) buckets into two DIFFERENT groups instead of coalescing into one
|
||||
/// group whose <c>InstanceGroup.FoliageFlags</c> was last-writer-wins between
|
||||
/// the two classifications — which flickered the shared group's flags
|
||||
/// between frames and let the caster (keyed correctly from the start) and
|
||||
/// the receiver (previously keyed without this field) disagree about the
|
||||
/// same subset.</para>
|
||||
/// </summary>
|
||||
internal readonly record struct GroupKey(
|
||||
uint FirstIndex,
|
||||
|
|
@ -30,4 +40,5 @@ internal readonly record struct GroupKey(
|
|||
GpuTextureSlot TextureSlot,
|
||||
uint TextureLayer,
|
||||
TranslucencyKind Translucency,
|
||||
CullMode CullMode = CullMode.CounterClockwise);
|
||||
CullMode CullMode = CullMode.CounterClockwise,
|
||||
uint FoliageFlags = 0u);
|
||||
|
|
|
|||
|
|
@ -119,6 +119,22 @@ public sealed partial class WbDrawDispatcher
|
|||
binding.Buffer,
|
||||
binding.OffsetBytes,
|
||||
binding.SizeBytes);
|
||||
// Campaign VM VM6 review fix round (A3): mesh_atmospheric.vert reads
|
||||
// uAtmosphereClockWind/uAtmosphereWindAmplitude from set 3/binding 5
|
||||
// — bind it here explicitly with the EXACT buffer/offset the caster
|
||||
// pass bound earlier this same frame (carried on
|
||||
// DirectionalShadowFrameBinding.AtmosphericFrame), rather than
|
||||
// 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.
|
||||
if (binding.AtmosphericFrame.IsBound)
|
||||
{
|
||||
encoder.BindUniformBuffer(
|
||||
GpuBindingModel.UniformAtmosphericFrame,
|
||||
binding.AtmosphericFrame.Buffer!,
|
||||
binding.AtmosphericFrame.OffsetBytes,
|
||||
binding.AtmosphericFrame.SizeBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeDirectionalShadowReceiverPipelines()
|
||||
|
|
|
|||
|
|
@ -1024,6 +1024,17 @@ public sealed partial class WbDrawDispatcher
|
|||
|
||||
if (renderData.IsSetup && renderData.SetupParts.Count > 0)
|
||||
{
|
||||
// Campaign VM VM6 review fix round (A4) — see
|
||||
// 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.
|
||||
bool entityHasCutoutSubset = FoliageWindClassification
|
||||
.ComputeEntityHasCutoutSubset(
|
||||
renderData.SetupParts,
|
||||
part => _meshAdapter.TryGetRenderData(part.GfxObjId)
|
||||
is { HasCutoutSubset: true });
|
||||
|
||||
for (int setupPartIndex = 0;
|
||||
setupPartIndex < renderData.SetupParts.Count;
|
||||
setupPartIndex++)
|
||||
|
|
@ -1069,7 +1080,8 @@ public sealed partial class WbDrawDispatcher
|
|||
in transformSource,
|
||||
ref batches,
|
||||
ref rejectedTransparent,
|
||||
ref unresolvedCutoutTextures);
|
||||
ref unresolvedCutoutTextures,
|
||||
entityHasCutoutSubset);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -1151,8 +1163,16 @@ public sealed partial class WbDrawDispatcher
|
|||
in DirectionalShadowTransformSource transformSource,
|
||||
ref int sourceBatches,
|
||||
ref int rejectedTransparent,
|
||||
ref int unresolvedCutoutTextures)
|
||||
ref int unresolvedCutoutTextures,
|
||||
// Campaign VM VM6 review fix round (A4): the ENTITY/Setup-scoped OR
|
||||
// of every resolved part's HasCutoutSubset — see the matching
|
||||
// parameter on WbDrawDispatcher.ClassifyBatches (the world receiver)
|
||||
// for why a Setup composite's own per-part HasCutoutSubset is not
|
||||
// enough. Null (the default) means "use renderData.HasCutoutSubset
|
||||
// directly", correct for the non-Setup flat-MeshRef caller below.
|
||||
bool? entityHasCutoutSubsetOverride = null)
|
||||
{
|
||||
bool entityHasCutoutSubset = entityHasCutoutSubsetOverride ?? renderData.HasCutoutSubset;
|
||||
for (int batchIndex = 0;
|
||||
batchIndex < renderData.Batches.Count;
|
||||
batchIndex++)
|
||||
|
|
@ -1203,7 +1223,7 @@ public sealed partial class WbDrawDispatcher
|
|||
candidate.LocalEntityId,
|
||||
FoliageWindExclusions.Contains(meshRef.GfxObjId),
|
||||
batch.Translucency,
|
||||
renderData.HasCutoutSubset);
|
||||
entityHasCutoutSubset);
|
||||
|
||||
_directionalShadowDraws.Add(
|
||||
batch.FirstIndex,
|
||||
|
|
|
|||
|
|
@ -490,9 +490,9 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
/// material would otherwise classify them as foliage. Empty (no
|
||||
/// exclusions) when no atmospheric pack is active. Assigned every frame
|
||||
/// by <c>AtmosphericPostProcessGraph.RenderDirectionalShadows</c> from
|
||||
/// the currently-selected pack's declaration — self-contained by
|
||||
/// construction, so a pack switch or deactivation can never leave a
|
||||
/// stale exclusion set applied.
|
||||
/// the currently-selected pack's declaration, so a pack switch or
|
||||
/// deactivation is reflected on the very next frame that runs the
|
||||
/// assignment — not "never," just "no slower than one frame behind."
|
||||
/// </summary>
|
||||
public IReadOnlySet<uint> FoliageWindExclusions { get; set; } =
|
||||
System.Collections.Frozen.FrozenSet<uint>.Empty;
|
||||
|
|
@ -1841,6 +1841,24 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
bool drewAny = false;
|
||||
if (renderData.IsSetup && renderData.SetupParts.Count > 0)
|
||||
{
|
||||
// Campaign VM VM6 review fix round (A4): a Setup composite's
|
||||
// parts are separate GfxObjs with their own independently
|
||||
// cached ObjectRenderData/HasCutoutSubset — a tree's opaque
|
||||
// trunk part carries no cutout batches of its own (the
|
||||
// leaves are a DIFFERENT part), so classifying each part
|
||||
// from only its own HasCutoutSubset would never give the
|
||||
// trunk part the trunk flag. One bounded scan of THIS
|
||||
// Setup's own (small, fixed-per-object) part list, once per
|
||||
// entity per frame — not per batch, not per instance — ORs
|
||||
// every currently-resolved part's HasCutoutSubset into one
|
||||
// entity-scoped value classification uses below instead of
|
||||
// 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.
|
||||
bool entityHasCutoutSubset = FoliageWindClassification.ComputeEntityHasCutoutSubset(
|
||||
renderData.SetupParts,
|
||||
part => _meshAdapter.TryGetRenderData(part.GfxObjId) is { HasCutoutSubset: true });
|
||||
|
||||
// #188: setupPartIndex is the SAME index space
|
||||
// TransparentPartHook.PartIndex addresses — retail's CPartArray
|
||||
// numbers parts by their ordinal position in the Setup's own
|
||||
|
|
@ -1905,7 +1923,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
opacityMultiplier = 1f - translucencyValue; // CMaterial::SetTranslucencySimple 0x005396f0
|
||||
}
|
||||
|
||||
if (!ClassifyBatches(partData, model, entity, meshRef, paletteIdentity, restPose, opacityMultiplier, collector))
|
||||
if (!ClassifyBatches(partData, model, entity, meshRef, paletteIdentity, restPose, opacityMultiplier, collector, entityHasCutoutSubset))
|
||||
currentEntityIncomplete = true;
|
||||
_selectionSink?.AddVisiblePart(
|
||||
entity.ServerGuid,
|
||||
|
|
@ -2378,7 +2396,8 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
g.TextureSlot,
|
||||
g.TextureLayer,
|
||||
g.Translucency,
|
||||
g.CullMode);
|
||||
g.CullMode,
|
||||
g.FoliageFlags);
|
||||
|
||||
private void ObserveCurrentDispatcherSubmission(
|
||||
int visibleInstanceCount,
|
||||
|
|
@ -2996,15 +3015,19 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
internal static void ApplyCacheHit(
|
||||
EntityCacheEntry entry,
|
||||
Matrix4x4 entityWorld,
|
||||
Action<GroupKey, Matrix4x4, Vector3, uint> appendInstance)
|
||||
Action<GroupKey, Matrix4x4, Vector3> appendInstance)
|
||||
{
|
||||
foreach (var cached in entry.Batches)
|
||||
{
|
||||
// Campaign VM VM6 review fix round: cached.Key.FoliageFlags is
|
||||
// now the ONLY source of truth for a replayed group's foliage
|
||||
// classification (GetOrCreateInstanceGroup derives it from the
|
||||
// key at creation) — no separate FoliageFlags field to drift
|
||||
// out of sync with it.
|
||||
appendInstance(
|
||||
cached.Key,
|
||||
cached.RestPose * entityWorld,
|
||||
cached.LocalSortCenter,
|
||||
cached.FoliageFlags);
|
||||
cached.LocalSortCenter);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3033,7 +3056,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
GroupRegistration = group.Registration,
|
||||
};
|
||||
}
|
||||
AppendInstanceToGroup(group!, model, cached.LocalSortCenter, cached.FoliageFlags);
|
||||
AppendInstanceToGroup(group!, model, cached.LocalSortCenter);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3147,11 +3170,10 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
private void AppendInstanceToGroup(
|
||||
GroupKey key,
|
||||
Matrix4x4 model,
|
||||
Vector3 localSortCenter,
|
||||
uint foliageFlags)
|
||||
Vector3 localSortCenter)
|
||||
{
|
||||
InstanceGroup grp = GetOrCreateInstanceGroup(key);
|
||||
AppendInstanceToGroup(grp, model, localSortCenter, foliageFlags);
|
||||
AppendInstanceToGroup(grp, model, localSortCenter);
|
||||
}
|
||||
|
||||
private InstanceGroup GetOrCreateInstanceGroup(GroupKey key)
|
||||
|
|
@ -3177,6 +3199,13 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
TextureLayer = key.TextureLayer,
|
||||
Translucency = key.Translucency,
|
||||
CullMode = key.CullMode,
|
||||
// Campaign VM VM6 review fix round: FoliageFlags is now part of
|
||||
// GroupKey (see GroupKey's doc comment), so it is set exactly
|
||||
// once here, at group creation, from the SAME key that decides
|
||||
// group identity — never re-stamped imperatively afterward,
|
||||
// which is what let a shared group's classification flicker
|
||||
// between whichever caller ran last.
|
||||
FoliageFlags = key.FoliageFlags,
|
||||
Registration = _nextGroupRegistration++,
|
||||
LastUsedFrame = _groupFrame,
|
||||
};
|
||||
|
|
@ -3187,16 +3216,9 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
private void AppendInstanceToGroup(
|
||||
InstanceGroup grp,
|
||||
Matrix4x4 model,
|
||||
Vector3 localSortCenter,
|
||||
uint foliageFlags)
|
||||
Vector3 localSortCenter)
|
||||
{
|
||||
grp.LastUsedFrame = _groupFrame;
|
||||
// Campaign VM VM6: re-stamp on every append (idempotent — the value
|
||||
// is a deterministic function of the mesh subset the group's key
|
||||
// already identifies) so a stale-registration replay that recreates
|
||||
// an evicted InstanceGroup (see ApplyCacheHitDirect above) never
|
||||
// leaves it at its zero default.
|
||||
grp.FoliageFlags = foliageFlags;
|
||||
grp.Matrices.Add(model);
|
||||
grp.LocalSortCenters.Add(localSortCenter);
|
||||
grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++);
|
||||
|
|
@ -3337,8 +3359,18 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
PaletteCompositeIdentity paletteIdentity,
|
||||
Matrix4x4 restPose,
|
||||
float opacityMultiplier = 1.0f,
|
||||
List<CachedBatch>? collector = null)
|
||||
List<CachedBatch>? collector = null,
|
||||
// Campaign VM VM6 review fix round (A4): HasCutoutSubset is a
|
||||
// per-PART (per-GfxObj) fact — a Setup composite's parts are
|
||||
// separate GfxObjs, so a tree's opaque trunk part's OWN
|
||||
// renderData.HasCutoutSubset is false even though the SAME Setup's
|
||||
// leaves part has one. The caller passes the ENTITY/Setup-scoped OR
|
||||
// across every resolved part here; null (the default) means "use
|
||||
// renderData.HasCutoutSubset directly", which is already correct for
|
||||
// a non-Setup single-mesh entity (see the flat MeshRef call site).
|
||||
bool? entityHasCutoutSubsetOverride = null)
|
||||
{
|
||||
bool entityHasCutoutSubset = entityHasCutoutSubsetOverride ?? renderData.HasCutoutSubset;
|
||||
bool allTexturesReady = true;
|
||||
for (int batchIdx = 0; batchIdx < renderData.Batches.Count; batchIdx++)
|
||||
{
|
||||
|
|
@ -3369,9 +3401,25 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
GpuTextureSlot texSlot = texture.Slot;
|
||||
uint texLayer = texture.Layer;
|
||||
|
||||
// Campaign VM VM6 review fix round: classify BEFORE constructing
|
||||
// the key and fold the result INTO the key (rather than
|
||||
// stamping it onto whatever group the key already resolves to).
|
||||
// Classification is from the RAW (pre-#188-promotion)
|
||||
// batch.Translucency — a mid-fade trunk is still a trunk, it
|
||||
// just landed in the alpha-blend group instead of opaque. This
|
||||
// is what keeps a scenery instance and a non-scenery instance
|
||||
// of the identical mesh subset in two SEPARATE groups instead of
|
||||
// coalescing into one group whose classification depends on
|
||||
// whichever entity classified it last.
|
||||
uint foliageFlags = FoliageWindClassification.Classify(
|
||||
entity.LocalEntityId,
|
||||
FoliageWindExclusions.Contains(meshRef.GfxObjId),
|
||||
batch.Translucency,
|
||||
entityHasCutoutSubset);
|
||||
var key = new GroupKey(
|
||||
batch.FirstIndex, (int)batch.BaseVertex,
|
||||
batch.IndexCount, texSlot, texLayer, translucency, batch.CullMode);
|
||||
batch.IndexCount, texSlot, texLayer, translucency, batch.CullMode,
|
||||
foliageFlags);
|
||||
|
||||
InstanceGroup grp = GetOrCreateInstanceGroup(key);
|
||||
grp.Matrices.Add(model);
|
||||
|
|
@ -3381,23 +3429,13 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
|
||||
grp.Opacities.Add(opacityMultiplier); // #188 — parallel to Matrices
|
||||
grp.SelectionLighting.Add(_currentEntitySelectionLighting);
|
||||
// Campaign VM VM6: classify from the RAW (pre-#188-promotion)
|
||||
// batch.Translucency — a mid-fade trunk is still a trunk, it
|
||||
// just landed in the alpha-blend group instead of opaque.
|
||||
uint foliageFlags = FoliageWindClassification.Classify(
|
||||
entity.LocalEntityId,
|
||||
FoliageWindExclusions.Contains(meshRef.GfxObjId),
|
||||
batch.Translucency,
|
||||
renderData.HasCutoutSubset);
|
||||
grp.FoliageFlags = foliageFlags;
|
||||
collector?.Add(new CachedBatch(
|
||||
key,
|
||||
texSlot,
|
||||
restPose,
|
||||
renderData.SortCenter,
|
||||
grp,
|
||||
grp.Registration,
|
||||
foliageFlags));
|
||||
grp.Registration));
|
||||
}
|
||||
return allTexturesReady;
|
||||
}
|
||||
|
|
@ -3851,9 +3889,14 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
// into BatchData.flags alongside #226's bit 0 at BuildIndirectArrays.
|
||||
// Group-level, not per-instance, because BatchData is read once per
|
||||
// draw call (Batches[gl_DrawIDARB]) — every instance sharing one
|
||||
// mesh-subset draw shares its classification. Set by ClassifyBatches
|
||||
// on a fresh classification and re-stamped by AppendInstanceToGroup
|
||||
// on every cache-hit replay (see CachedBatch.FoliageFlags).
|
||||
// mesh-subset draw shares its classification. Review fix round: this
|
||||
// is now set EXACTLY ONCE, in GetOrCreateInstanceGroup, from the
|
||||
// owning GroupKey.FoliageFlags — never re-stamped imperatively after
|
||||
// creation, so a shared group's classification can no longer
|
||||
// flicker between whichever caller classified it last. A mesh
|
||||
// subset reachable from both a scenery and a non-scenery entity now
|
||||
// buckets into two distinct groups (distinct GroupKey.FoliageFlags)
|
||||
// instead of coalescing into one.
|
||||
public uint FoliageFlags;
|
||||
|
||||
public float SortDistance; // squared distance from camera to first instance, for opaque sort
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue