ff_render/nodes/composite/
transform.rs1#[cfg(feature = "wgpu")]
4use super::helpers::{
5 fullscreen_pipeline, linear_sampler, one_tex_sampler_uniform_bgl, pack_f32, submit_render_pass,
6};
7use crate::nodes::RenderNodeCpu;
8
9#[cfg(feature = "wgpu")]
12struct TransformPipeline {
13 render_pipeline: wgpu::RenderPipeline,
14 bind_group_layout: wgpu::BindGroupLayout,
15 sampler: wgpu::Sampler,
16 uniform_buf: wgpu::Buffer,
17}
18
19pub struct TransformNode {
27 pub translate: [f32; 2],
29 pub rotate: f32,
31 pub scale: [f32; 2],
33 #[cfg(feature = "wgpu")]
34 pipeline: std::sync::OnceLock<TransformPipeline>,
35}
36
37impl TransformNode {
38 #[must_use]
39 pub fn new(translate: [f32; 2], rotate: f32, scale: [f32; 2]) -> Self {
40 Self {
41 translate,
42 rotate,
43 scale,
44 #[cfg(feature = "wgpu")]
45 pipeline: std::sync::OnceLock::new(),
46 }
47 }
48}
49
50impl Default for TransformNode {
51 fn default() -> Self {
52 Self::new([0.0, 0.0], 0.0, [1.0, 1.0])
53 }
54}
55
56impl RenderNodeCpu for TransformNode {
57 fn process_cpu(&self, _rgba: &mut [u8], _w: u32, _h: u32) {
58 }
60}
61
62#[cfg(feature = "wgpu")]
63impl TransformNode {
64 fn get_or_create_pipeline(&self, ctx: &crate::context::RenderContext) -> &TransformPipeline {
65 self.pipeline.get_or_init(|| {
66 let device = &ctx.device;
67 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
68 label: Some("Transform shader"),
69 source: wgpu::ShaderSource::Wgsl(
70 include_str!("../../shaders/transform.wgsl").into(),
71 ),
72 });
73 let bgl = one_tex_sampler_uniform_bgl(device, "Transform");
74 let render_pipeline = fullscreen_pipeline(device, &shader, "Transform", &bgl);
75 let sampler = linear_sampler(device, "Transform");
76 let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
78 label: Some("Transform uniforms"),
79 size: 32,
80 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
81 mapped_at_creation: false,
82 });
83 TransformPipeline {
84 render_pipeline,
85 bind_group_layout: bgl,
86 sampler,
87 uniform_buf,
88 }
89 })
90 }
91}
92
93#[cfg(feature = "wgpu")]
94impl crate::nodes::RenderNode for TransformNode {
95 fn process(
96 &self,
97 inputs: &[&wgpu::Texture],
98 outputs: &[&wgpu::Texture],
99 ctx: &crate::context::RenderContext,
100 ) {
101 let Some(input) = inputs.first() else {
102 log::warn!("TransformNode::process called with no inputs");
103 return;
104 };
105 let Some(output) = outputs.first() else {
106 log::warn!("TransformNode::process called with no outputs");
107 return;
108 };
109 let pd = self.get_or_create_pipeline(ctx);
110
111 let uniforms = pack_f32(&[
113 self.translate[0],
114 self.translate[1],
115 self.rotate,
116 0.0,
117 self.scale[0],
118 self.scale[1],
119 0.0,
120 0.0,
121 ]);
122 ctx.queue.write_buffer(&pd.uniform_buf, 0, &uniforms);
123
124 let in_view = input.create_view(&wgpu::TextureViewDescriptor::default());
125 let out_view = output.create_view(&wgpu::TextureViewDescriptor::default());
126
127 let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
128 label: Some("Transform BG"),
129 layout: &pd.bind_group_layout,
130 entries: &[
131 wgpu::BindGroupEntry {
132 binding: 0,
133 resource: wgpu::BindingResource::TextureView(&in_view),
134 },
135 wgpu::BindGroupEntry {
136 binding: 1,
137 resource: wgpu::BindingResource::Sampler(&pd.sampler),
138 },
139 wgpu::BindGroupEntry {
140 binding: 2,
141 resource: pd.uniform_buf.as_entire_binding(),
142 },
143 ],
144 });
145 submit_render_pass(
146 ctx,
147 &pd.render_pipeline,
148 &bind_group,
149 &out_view,
150 "Transform",
151 );
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::nodes::RenderNodeCpu;
159
160 #[test]
161 fn transform_node_cpu_path_should_be_passthrough() {
162 let node = TransformNode::new([0.1, 0.0], 0.0, [2.0, 2.0]);
163 let original = vec![10u8, 20, 30, 255];
164 let mut rgba = original.clone();
165 node.process_cpu(&mut rgba, 1, 1);
166 assert_eq!(rgba, original, "TransformNode CPU must be a no-op");
167 }
168
169 #[test]
170 fn transform_node_default_should_be_identity() {
171 let node = TransformNode::default();
172 assert_eq!(node.translate, [0.0, 0.0]);
173 assert_eq!(node.rotate, 0.0);
174 assert_eq!(node.scale, [1.0, 1.0]);
175 }
176}