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}