acdream/src/AcDream.App/Rendering/Gpu/GpuPushConstants.cs
Erik 602bc9dddb feat(render): V6e — both particle shaders cross the dialect
Campaign V slice V6e, second of three. Billboard particles and mesh particles
are the last two pairs blocked on the texture-table shape; sky follows.

particle takes the same treatment mesh_modern took: the `flat uvec2` handle
varying becomes a `flat uint` slot and the fragment stage samples through
ACDREAM_SAMPLE_ARRAY. What is different here is the untextured particle. The
shader used to ask "is the handle I was given zero", which GL can answer because
its emulated table stores handles; Vulkan cannot, because set 2 is an opaque
descriptor array and reading an element nobody wrote is undefined rather than
zero. So the question moves to the index: the CPU writes ACDREAM_TEXTURE_NONE
for a particle with no texture instead of interning the null handle as a table
slot, and both dialects test the same value. GL renders identically — the same
particles take the same branch to the same procedural blob — and the handle
table simply stops carrying an entry that never named a texture. A test pins the
sentinel across all three declarations of it, because a silent disagreement here
would sample slot 0xFFFFFFFF instead of drawing the blob.

particle_mesh needed no restructuring, only names. Vulkan GLSL has no default
uniform block, so `uniform uint uTextureIndex;` is not unsupported but
unspellable, and the two values are per-pass — one texture and one layer for a
whole sub-batch — which is exactly what the shared push-constant block is for.
uTextureIndex becomes uTextureIndexA; uTextureLayer becomes uParamA, which was
the spare scalar and is a natural fit because the shader converted the layer to
float anyway. The widening moved from the shader to the CPU; layers are small
integers, so the sampled value is bit-identical.

Gates: Release build clean; App tests 4,058 passed / 3 skipped (baseline 4,057
plus the sentinel drift guard). Offline pixel gate against 95f8c25f: two
captures, 29 px and 21 px of 563,200 compared (3.73e-05 and 5.15e-05), with a
same-commit control between them of 13 px and this commit measuring 14 px
against its own parent. The scene draws no particles, so this gate is a tripwire
that the world path is undisturbed, not evidence about particles.

Particles remain user-gate debt — the same debt V2c and V4e already carry, to be
paid by casting a spell in a connected session.

Manifest: 6/9 pairs compile. Remaining: mesh (legacy, no consumer), sky (next
commit), terrain_modern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 09:26:13 +02:00

84 lines
3.4 KiB
C#

using System.Numerics;
using System.Runtime.InteropServices;
namespace AcDream.App.Rendering.Gpu;
/// <summary>
/// The single push-constant block shared by every acdream pipeline — 96 of the
/// 128 bytes Vulkan guarantees, leaving 32 bytes of headroom for later slices.
///
/// One shared block (rather than a per-shader block) is what lets every world
/// pipeline share ONE pipeline layout, so switching pipelines mid-pass does not
/// invalidate bound descriptor sets or push constants. Shaders declare only the
/// fields they read; unused fields cost nothing.
///
/// The GL backend maps each field to the correspondingly named uniform and skips
/// any the program does not declare (location -1). The Vulkan backend writes the
/// struct verbatim with one <c>vkCmdPushConstants</c>.
///
/// Layout is asserted by <c>GpuContractTests</c>; changing it is a contract change
/// that must land together with the matching edit to the shared GLSL preamble.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal struct GpuPushConstants
{
/// <summary>
/// GLSL <c>uViewProjection</c>. acdream's cameras build this with
/// <c>Matrix4x4.CreatePerspectiveFieldOfView</c>, whose NDC z range is [0,1] —
/// already Vulkan's convention (see <c>PortalProjection.cs</c>). No projection
/// rework is needed for the migration, and Vulkan gains the half of the depth
/// range GL was discarding.
/// </summary>
public Matrix4x4 ViewProjection;
/// <summary>
/// GLSL <c>uDrawIDOffset</c>. Issue #52: the draw index resets to 0 at the
/// start of each multi-draw-indirect call, so a pass that begins partway into
/// the batch array must offset its lookup. Vulkan's <c>gl_DrawID</c> resets
/// identically per <c>vkCmdDrawIndexedIndirect</c>, so the pattern carries over
/// unchanged.
/// </summary>
public int DrawIdOffset;
/// <summary>GLSL <c>uLightingMode</c>: 0 = object (plain Lambert + sun), 1 = EnvCell (half-Lambert wrap, no sun).</summary>
public int LightingMode;
/// <summary>GLSL <c>uRenderPass</c>: 0 = opaque, 1 = translucent.</summary>
public int RenderPass;
/// <summary>GLSL <c>uLightDebug</c>: #176 stripe-hunt isolation modes; 0 = off.</summary>
public int LightDebug;
/// <summary>
/// GLSL <c>uTextureIndexA</c>. Primary texture-table slot for pipelines whose
/// texture is per-pass rather than per-batch — currently the terrain atlas.
/// </summary>
public uint TextureIndexA;
/// <summary>GLSL <c>uTextureIndexB</c>. Secondary per-pass slot — currently the terrain alpha-mask array.</summary>
public uint TextureIndexB;
/// <summary>
/// GLSL <c>uParamA</c>. Spare scalar, claimed at slice V6e by
/// <c>particle_mesh</c> as the array layer its per-pass texture is sampled
/// from — a value the shader converted to float anyway.
/// </summary>
public float ParamA;
/// <summary>GLSL <c>uParamB</c>. Spare scalar; unclaimed at V0.</summary>
public float ParamB;
/// <summary>Neutral defaults: identity transform, opaque object lighting, no debug mode.</summary>
public static GpuPushConstants Default => new()
{
ViewProjection = Matrix4x4.Identity,
DrawIdOffset = 0,
LightingMode = 0,
RenderPass = 0,
LightDebug = 0,
TextureIndexA = 0,
TextureIndexB = 0,
ParamA = 0f,
ParamB = 0f,
};
}