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}