Skip to main content

bevy_react/layer/render/
transform3d.rs

1//! Render-world plumbing for the composite quad's per-layer uniforms: the 3D
2//! model matrix and the screen-space clip rect (`transform3d` support).
3//!
4//! Every drawn composite quad gets one [`CompositeUniforms`] entry — identity
5//! matrix + open clip for untransformed layers — so `composite.wgsl` stays
6//! single-path. The lifecycle mirrors [`FilterUniforms`](super::FilterUniforms):
7//! a [`DynamicUniformBuffer`] staged in `prepare_layer_composites`, the
8//! per-quad offset riding [`LayerCompositeBatch`](super::LayerCompositeBatch),
9//! and one whole-buffer bind group bound at a dynamic offset by
10//! [`SetCompositeUniforms`].
11
12use bevy::ecs::system::SystemParamItem;
13use bevy::ecs::system::lifetimeless::SRes;
14use bevy::math::{Mat4, Vec2, Vec4};
15use bevy::prelude::*;
16use bevy::render::render_phase::{
17    PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass,
18};
19use bevy::render::render_resource::{BindGroup, DynamicUniformBuffer, ShaderType};
20
21/// Per-quad composite params. Field order matches `CompositeParams` in
22/// `composite.wgsl` byte for byte (128 bytes; guarded by
23/// `composite_uniforms_match_the_documented_wgsl_layout`). Pad names are
24/// digit-free on purpose (the naga-namer constraint, see `FilterUniforms`).
25#[derive(Clone, Copy, ShaderType)]
26pub struct CompositeUniforms {
27    /// Screen-space model matrix (physical px, homogeneous — the vertex stage
28    /// keeps the real `w` for perspective-correct interpolation and flattens
29    /// `z` post-transform). Identity for untransformed layers.
30    pub model: Mat4,
31    /// Screen-space ancestor-clip rect the fragment stage tests transformed
32    /// quads against ([`open_clip`] sentinel = unclipped / already CPU-clamped).
33    pub clip_min: Vec2,
34    pub clip_max: Vec2,
35    /// Edge-feather width in screen px for the analytic edge AA of transformed
36    /// quads (their diagonal silhouettes rasterize without MSAA). `0.0`
37    /// disables the coverage term entirely — the untransformed path, which
38    /// must stay pixel-identical (its CPU-clamped UVs sit inside `[0,1]` at
39    /// clipped edges, where uv-distance feathering would be wrong).
40    pub edge_feather: f32,
41    pub pad_a: f32,
42    pub pad_b: Vec2,
43    /// Rounded-corner mask radii, `[top_left, top_right, bottom_right,
44    /// bottom_left]` physical px — the node's layout-resolved
45    /// `ComputedNode.border_radius` (already clamped per corner to
46    /// `0.5 * min(w, h)`; Bevy's rule, not the CSS proportional-shrink one —
47    /// matching what bevy_ui *paints* is the point: the frost edge must
48    /// coincide with the node's own rounded background). All-zero disables
49    /// the mask term entirely (the `edge_feather` pattern) — today only
50    /// backdrop quads set it.
51    pub radius: Vec4,
52    /// The UNCLIPPED border box the radii round, screen-space physical px.
53    /// Carried separately because the CPU clip clamps the quad's *geometry*
54    /// — the SDF must still measure against the true box.
55    pub box_center: Vec2,
56    pub box_size: Vec2,
57}
58
59/// The clip sentinel: an interval no on-screen fragment escapes, making the
60/// fragment test a no-op for quads clipped on the CPU (or not clipped at all).
61pub fn open_clip() -> (Vec2, Vec2) {
62    (Vec2::splat(-f32::MAX), Vec2::splat(f32::MAX))
63}
64
65/// Frame-staged composite uniforms + their whole-buffer bind group (rebuilt
66/// every frame after `write_buffer` — the buffer may reallocate).
67#[derive(Resource)]
68pub struct CompositeUniformsMeta {
69    pub uniforms: DynamicUniformBuffer<CompositeUniforms>,
70    pub bind_group: Option<BindGroup>,
71}
72
73impl Default for CompositeUniformsMeta {
74    fn default() -> Self {
75        let mut uniforms = DynamicUniformBuffer::default();
76        uniforms.set_label(Some("ui_layer_composite_uniforms"));
77        Self {
78            uniforms,
79            bind_group: None,
80        }
81    }
82}
83
84/// Binds the composite-uniform bind group at the quad's dynamic offset
85/// (staged by `prepare_layer_composites` on [`LayerCompositeBatch`]).
86pub struct SetCompositeUniforms<const I: usize>;
87impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetCompositeUniforms<I> {
88    type Param = SRes<CompositeUniformsMeta>;
89    type ViewQuery = ();
90    type ItemQuery = bevy::ecs::system::lifetimeless::Read<super::LayerCompositeBatch>;
91
92    #[inline]
93    fn render<'w>(
94        _item: &P,
95        _view: (),
96        batch: Option<&'w super::LayerCompositeBatch>,
97        meta: SystemParamItem<'w, '_, Self::Param>,
98        pass: &mut TrackedRenderPass<'w>,
99    ) -> RenderCommandResult {
100        let Some(batch) = batch else {
101            return RenderCommandResult::Skip;
102        };
103        let Some(bind_group) = &meta.into_inner().bind_group else {
104            return RenderCommandResult::Failure("composite uniforms bind group missing");
105        };
106        pass.set_bind_group(I, bind_group, &[batch.uniform_offset]);
107        RenderCommandResult::Success
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use bevy::render::render_resource::encase::UniformBuffer;
115
116    fn f32_at(bytes: &[u8], offset: usize) -> f32 {
117        f32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap())
118    }
119
120    /// The WGSL `CompositeParams` struct in `composite.wgsl` documents a
121    /// 128-byte uniform layout (mat4x4 + clip vec2s + feather + pads +
122    /// corner radii + border box); the Rust mirror must match field for
123    /// field.
124    #[test]
125    fn composite_uniforms_match_the_documented_wgsl_layout() {
126        assert_eq!(CompositeUniforms::min_size().get(), 128);
127
128        let value = CompositeUniforms {
129            model: Mat4::from_translation(bevy::math::Vec3::new(9.0, 0.0, 0.0)),
130            clip_min: Vec2::new(1.0, 2.0),
131            clip_max: Vec2::new(3.0, 4.0),
132            edge_feather: 1.5,
133            pad_a: 0.0,
134            pad_b: Vec2::ZERO,
135            radius: Vec4::new(5.0, 6.0, 7.0, 8.0),
136            box_center: Vec2::new(10.0, 11.0),
137            box_size: Vec2::new(12.0, 13.0),
138        };
139        let mut buffer = UniformBuffer::new(Vec::<u8>::new());
140        buffer.write(&value).expect("uniform write");
141        let bytes = buffer.into_inner();
142        assert_eq!(bytes.len(), 128);
143        // Per-field offsets, per the WGSL struct's comment block.
144        assert_eq!(f32_at(&bytes, 48), 9.0); // model.w_axis.x (col 3 @ 48)
145        assert_eq!(f32_at(&bytes, 64), 1.0); // clip_min.x
146        assert_eq!(f32_at(&bytes, 68), 2.0); // clip_min.y
147        assert_eq!(f32_at(&bytes, 72), 3.0); // clip_max.x
148        assert_eq!(f32_at(&bytes, 76), 4.0); // clip_max.y
149        assert_eq!(f32_at(&bytes, 80), 1.5); // edge_feather
150        assert_eq!(f32_at(&bytes, 96), 5.0); // radius.x (top_left)
151        assert_eq!(f32_at(&bytes, 100), 6.0); // radius.y (top_right)
152        assert_eq!(f32_at(&bytes, 104), 7.0); // radius.z (bottom_right)
153        assert_eq!(f32_at(&bytes, 108), 8.0); // radius.w (bottom_left)
154        assert_eq!(f32_at(&bytes, 112), 10.0); // box_center.x
155        assert_eq!(f32_at(&bytes, 116), 11.0); // box_center.y
156        assert_eq!(f32_at(&bytes, 120), 12.0); // box_size.x
157        assert_eq!(f32_at(&bytes, 124), 13.0); // box_size.y
158    }
159}