Skip to main content

concinnity_render/metal/
uniforms.rs

1//! repr(C) uniform structs only the Metal frame encoder and its passes bind.
2//! Each layout must match the corresponding struct in an `.metal` shader under
3//! `metal/shaders/`.
4//!
5//! Blocks whose shader counterpart is a single-source `.slang` declaration are
6//! declared once for every backend in `crate::uniforms`; what is left here is
7//! what only this backend binds. Their layouts are checked by `shader_layout` in
8//! concinnity-device, which reads the expected offsets out of slangc's
9//! reflection per target. The hand-written asserts below are for the families
10//! whose shaders are still per backend -- the cull kernel, the skinning and
11//! morph kernels, the raymarch SDF templates, the legacy per-draw main and
12//! velocity passes.
13
14/// Per-draw-call model matrix pushed at buffer(2) before each draw.
15#[derive(Copy, Clone, bytemuck::NoUninit)]
16#[repr(C)]
17pub struct ModelUniforms {
18    /// Model-to-world matrix (column-major).
19    pub model: [[f32; 4]; 4],
20}
21
22/// Per-draw material roughness pushed to the SSR pre-pass fragment at
23/// buffer(0). Layout matches the `PpMat` struct in the SSR pre-pass MSL.
24#[derive(Copy, Clone, bytemuck::NoUninit)]
25#[repr(C)]
26pub struct SsrPrepassMat {
27    /// Perceptual roughness `[0, 1]` of this draw's material.
28    pub roughness: f32,
29    /// Padding so the field layout matches the shader-side struct.
30    pub _pad: [f32; 3],
31}
32
33/// Per-frame inputs to the GPU-driven cull kernel, pushed inline at
34/// the compute encoder's buffer(2). Layout (208 bytes, a multiple of 16) must
35/// match the `CullUniforms` struct in the cull kernel MSL (`build_cull_pipeline`).
36#[derive(Copy, Clone, bytemuck::NoUninit)]
37#[repr(C)]
38pub struct CullUniforms {
39    /// The six frustum planes (left/right/bottom/top/near/far), each
40    /// `[normal.x, normal.y, normal.z, d]`, extracted CPU-side and already
41    /// normalised so the kernel's plane test matches `gfx::frustum` exactly.
42    pub planes: [[f32; 4]; 6],
43    /// World-space camera position (packed_float3 in MSL, alignment 4).
44    pub cam_pos: [f32; 3],
45    /// Number of valid `DrawObject` records; kernel threads past it return.
46    pub object_count: u32,
47    /// Previous frame's un-jittered view-projection. The kernel projects each
48    /// AABB through this so the NDC depths line up with the Hi-Z values the
49    /// previous frame's main pass produced. `float4x4` lands at offset 112,
50    /// already 16-aligned, so the layout matches MSL with no padding.
51    pub prev_view_proj: [[f32; 4]; 4],
52    /// Hi-Z mip-0 dimensions in texels. `[1.0, 1.0]` when no Hi-Z is bound.
53    pub hiz_size: [f32; 2],
54    /// Mip levels in the bound Hi-Z texture.
55    pub hiz_mip_count: u32,
56    /// `0` skips the Hi-Z occlusion test (first frame / after a resize, before
57    /// a valid pyramid exists); `1` runs it.
58    pub hiz_enabled: u32,
59    /// Unified-cull index where the folded skinned records begin (= static +
60    /// instances). The kernel draws records at or past this through the u16
61    /// skinned index buffer instead of the static u32 one. Equals `object_count`
62    /// when no skinned mesh is folded.
63    pub skinned_base: u32,
64    /// Command-slot base offset for the GPU-driven shadow cull: the
65    /// shadow ICB holds NUM_SHADOW_CASCADES * object_count slots and cascade `c`
66    /// writes its survivors at `cascade_base + tid` (= c * object_count). The
67    /// main cull leaves it 0 (writes at `tid`).
68    pub cascade_base: u32,
69    /// How many shader-bucket ICBs this dispatch's argument buffer carries.
70    /// The main cull passes the world's bucket count; single-stream dispatches
71    /// (shadow, mirror) pass 1. Trailing `_pad_skin` rounds the struct to 208
72    /// bytes so it matches the 16-aligned MSL `CullUniforms`.
73    pub bucket_count: u32,
74    /// Padding so the field layout matches the shader-side struct.
75    pub _pad_skin: u32,
76}
77
78/// Per-frame uniforms for the TAA velocity pre-pass at buffer(0). Layout must
79/// match `VelUniforms` in `pipeline.rs`'s velocity MSL.
80#[derive(Copy, Clone)]
81#[repr(C)]
82pub struct VelocityUniforms {
83    /// Jittered current view-projection: drives the rasterised position so
84    /// the pre-pass covers exactly the same pixels as the main pass.
85    pub jittered_vp: [[f32; 4]; 4],
86    /// Un-jittered current view-projection: keeps the stored motion vector
87    /// free of the sub-pixel projection jitter.
88    pub cur_vp: [[f32; 4]; 4],
89    /// Un-jittered previous-frame view-projection.
90    pub prev_vp: [[f32; 4]; 4],
91}
92
93/// Per-frame view inputs the raymarch pass binds at buffer(0). Layout matches
94/// `RaymarchView` in `shaders/raymarch_helpers.metal`. 160 bytes.
95#[derive(Copy, Clone, bytemuck::NoUninit)]
96#[repr(C)]
97pub struct RaymarchView {
98    /// View-projection matrix, column-major.
99    pub vp: [[f32; 4]; 4],
100    /// Inverse view-projection matrix, column-major.
101    pub inv_vp: [[f32; 4]; 4],
102    /// World-space camera position (xyz). `.w` is ignored.
103    pub cam_pos: [f32; 4],
104    /// HDR target width / height in pixels: the shader divides `position.xy` by
105    /// this to read the depth attachment with integer pixel coordinates.
106    pub viewport: [f32; 2],
107    /// Wall-clock seconds since startup, available to the user SDF.
108    pub time: f32,
109    /// Mip count of the bound IBL prefilter cube; 0 disables the cube-sample IBL
110    /// path and the helper falls back to the hand-tuned hemispheric ambient.
111    /// Mirrors `ViewUniforms.prefilter_mip_count` from the Main pass: same
112    /// semantics, same gate.
113    pub prefilter_mip_count: f32,
114}
115
116/// Per-volume uniforms uploaded at buffer(1). Layout matches `SdfVolumeUniforms`
117/// in `shaders/raymarch_helpers.metal`. 176 bytes (two packed_float3 + pad = 32,
118/// four scalars = 16, 32 float params = 128).
119#[derive(Copy, Clone, bytemuck::NoUninit)]
120#[repr(C)]
121pub struct RaymarchVolumeUniforms {
122    /// World-space centre (`packed_float3` + pad).
123    pub centre: [f32; 3],
124    /// Padding so the field layout matches the shader-side struct.
125    pub _pad0: f32,
126    /// XYZ half-widths of the bounding box (`packed_float3` + pad).
127    pub extent: [f32; 3],
128    /// Padding so the field layout matches the shader-side struct.
129    pub _pad1: f32,
130    /// `1 / max_gradient`; the cone-step scale factor in `coneRaymarch`.
131    pub cone_ratio: f32,
132    /// Per-volume march far-clip in metres.
133    pub max_distance: f32,
134    /// Per-volume step cap (clamped 8..256 at asset load).
135    pub max_steps: i32,
136    /// Currently unused; reserved in the layout so user shaders that probe it
137    /// find a stable slot.
138    pub receive_shadows: i32,
139    /// Generic parameter block; the user shader casts it to whatever struct it
140    /// interprets.
141    pub params: [f32; crate::components::sdf_volume::SDF_PARAMS_LEN],
142}
143
144/// Cascade selector pushed at buffer(4) for the raymarch shadow-caster pipeline.
145/// Picks `shadow.light_vps[cascade_idx]` in both stages. Matches
146/// `RaymarchShadowCascade` in `shaders/raymarch_shadow.metal`. 16 bytes.
147#[derive(Copy, Clone, bytemuck::NoUninit)]
148#[repr(C)]
149pub struct RaymarchShadowCascade {
150    /// Which shadow cascade is being rendered.
151    pub cascade_idx: u32,
152    /// Padding so the field layout matches the shader-side struct.
153    pub _pad: [u32; 3],
154}
155
156/// Morph-target cap per skinned mesh: the fixed weight-array length in the
157/// skinned VS params (ARKit-style faces use ~52 targets).
158pub const MAX_MORPH_TARGETS: usize = 64;
159
160/// Per-draw morph parameters for the legacy skinned vertex shader. Matches the
161/// MSL `VsMorphParams` in main.metal: four uints then the weight array.
162/// 272 bytes.
163#[repr(C)]
164#[derive(Clone, Copy, bytemuck::NoUninit)]
165pub struct VsMorphParams {
166    /// First vertex of this slot's region in the shared vertex buffer.
167    pub vertex_base: u32,
168    /// Vertices in this slot's region.
169    pub vertex_count: u32,
170    /// Morph targets on this slot's mesh.
171    pub target_count: u32,
172    /// Padding so the field layout matches the shader-side struct.
173    pub _pad: u32,
174    /// One weight per morph target, in target order.
175    pub weights: [f32; MAX_MORPH_TARGETS],
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use core::mem::{offset_of, size_of};
182
183    #[test]
184    fn cull_uniforms_layout_matches_msl() {
185        // MSL `CullUniforms` in cull.metal: float4 planes[6], packed_float3
186        // cam_pos + object_count, then a float4x4 at the 16-aligned offset 112,
187        // a float2 + two uints, then skinned_base + cascade_base +
188        // bucket_count + 4B pad rounding to 208.
189        assert_eq!(size_of::<CullUniforms>(), 208);
190        assert_eq!(offset_of!(CullUniforms, planes), 0);
191        assert_eq!(offset_of!(CullUniforms, cam_pos), 96);
192        assert_eq!(offset_of!(CullUniforms, object_count), 108);
193        assert_eq!(offset_of!(CullUniforms, prev_view_proj), 112);
194        assert_eq!(offset_of!(CullUniforms, hiz_size), 176);
195        assert_eq!(offset_of!(CullUniforms, hiz_mip_count), 184);
196        assert_eq!(offset_of!(CullUniforms, hiz_enabled), 188);
197        assert_eq!(offset_of!(CullUniforms, skinned_base), 192);
198        assert_eq!(offset_of!(CullUniforms, cascade_base), 196);
199        assert_eq!(offset_of!(CullUniforms, bucket_count), 200);
200        assert_eq!(size_of::<CullUniforms>() % 16, 0);
201    }
202
203    #[test]
204    fn velocity_uniforms_layout_matches_msl() {
205        // MSL `VelUniforms` in velocity.metal: three float4x4.
206        assert_eq!(size_of::<VelocityUniforms>(), 192);
207        assert_eq!(offset_of!(VelocityUniforms, jittered_vp), 0);
208        assert_eq!(offset_of!(VelocityUniforms, cur_vp), 64);
209        assert_eq!(offset_of!(VelocityUniforms, prev_vp), 128);
210    }
211
212    #[test]
213    fn raymarch_view_layout_matches_msl() {
214        // MSL `RaymarchView` in raymarch_helpers.metal: two float4x4, a
215        // packed_float3 cam_pos (+ pad), float2 viewport, then two scalars.
216        // The Rust `cam_pos: [f32; 4]` covers the same 16 bytes (xyz + pad)
217        // as the MSL `packed_float3 cam_pos; float _pad0;`.
218        assert_eq!(size_of::<RaymarchView>(), 160);
219        assert_eq!(offset_of!(RaymarchView, vp), 0);
220        assert_eq!(offset_of!(RaymarchView, inv_vp), 64);
221        assert_eq!(offset_of!(RaymarchView, cam_pos), 128);
222        assert_eq!(offset_of!(RaymarchView, viewport), 144);
223        assert_eq!(offset_of!(RaymarchView, time), 152);
224        assert_eq!(offset_of!(RaymarchView, prefilter_mip_count), 156);
225        assert_eq!(size_of::<RaymarchView>() % 16, 0);
226    }
227
228    #[test]
229    fn raymarch_volume_uniforms_layout_matches_msl() {
230        // MSL `SdfVolumeUniforms` in raymarch_helpers.metal: two packed_float3
231        // (+ pad), four scalars, then `SdfParams { float vals[32]; }` at offset
232        // 48. The 176-byte size pins SDF_PARAMS_LEN == 32 (48 + 32*4).
233        assert_eq!(size_of::<RaymarchVolumeUniforms>(), 176);
234        assert_eq!(offset_of!(RaymarchVolumeUniforms, centre), 0);
235        assert_eq!(offset_of!(RaymarchVolumeUniforms, _pad0), 12);
236        assert_eq!(offset_of!(RaymarchVolumeUniforms, extent), 16);
237        assert_eq!(offset_of!(RaymarchVolumeUniforms, _pad1), 28);
238        assert_eq!(offset_of!(RaymarchVolumeUniforms, cone_ratio), 32);
239        assert_eq!(offset_of!(RaymarchVolumeUniforms, max_distance), 36);
240        assert_eq!(offset_of!(RaymarchVolumeUniforms, max_steps), 40);
241        assert_eq!(offset_of!(RaymarchVolumeUniforms, receive_shadows), 44);
242        assert_eq!(offset_of!(RaymarchVolumeUniforms, params), 48);
243    }
244
245    #[test]
246    fn raymarch_shadow_cascade_layout_matches_msl() {
247        // MSL `RaymarchShadowCascade` in raymarch_shadow.metal: a uint + pad.
248        assert_eq!(size_of::<RaymarchShadowCascade>(), 16);
249        assert_eq!(offset_of!(RaymarchShadowCascade, cascade_idx), 0);
250        assert_eq!(offset_of!(RaymarchShadowCascade, _pad), 4);
251    }
252
253    #[test]
254    fn vs_morph_params_layout_matches_msl() {
255        // MSL `VsMorphParams` in main.metal: four uints then float[64].
256        assert_eq!(size_of::<VsMorphParams>(), 272);
257        assert_eq!(offset_of!(VsMorphParams, vertex_base), 0);
258        assert_eq!(offset_of!(VsMorphParams, vertex_count), 4);
259        assert_eq!(offset_of!(VsMorphParams, target_count), 8);
260        assert_eq!(offset_of!(VsMorphParams, _pad), 12);
261        assert_eq!(offset_of!(VsMorphParams, weights), 16);
262    }
263}