1use wisp::application::{AppConfig, Application};
21use wisp::color::Color;
22use wisp::recording::{CamLayout, CursorRipple, RecordingScene, StreamDimensions};
23use wisp::render::Renderer;
24use wisp::texture::render_texture::RenderTexture;
25
26use crate::recording::FrameSlot;
27
28#[derive(Debug)]
31pub struct ComposedFrame {
32 pub bytes: Vec<u8>,
34 pub width: u32,
36 pub height: u32,
38}
39
40pub struct RecordingCompose {
44 app: Application,
45 renderer: Renderer,
46 scene: RecordingScene,
47 target: RenderTexture,
48 width: u32,
49 height: u32,
50 has_camera_frame: bool,
54 has_screen_frame: bool,
56}
57
58impl RecordingCompose {
59 pub fn new(
68 width: u32,
69 height: u32,
70 screen_dims: StreamDimensions,
71 cam_dims: StreamDimensions,
72 ) -> Result<Self, wisp::Error> {
73 let app = pollster::block_on(Application::new(AppConfig {
74 width,
75 height,
76 ..AppConfig::default()
77 }))?;
78 let renderer = Renderer::new(&app, wisp::wgpu::TextureFormat::Bgra8UnormSrgb)?;
79 let scene = RecordingScene::new(&app, screen_dims, cam_dims, CamLayout::default());
80 let target = RenderTexture::with_format(
81 &app,
82 width,
83 height,
84 wisp::wgpu::TextureFormat::Bgra8UnormSrgb,
85 );
86
87 Ok(Self {
88 app,
89 renderer,
90 scene,
91 target,
92 width,
93 height,
94 has_camera_frame: false,
95 has_screen_frame: false,
96 })
97 }
98
99 pub fn compose_frame(
106 &mut self,
107 camera_slot: &FrameSlot,
108 screen_slot: &FrameSlot,
109 ) -> Option<ComposedFrame> {
110 let cam = camera_slot
117 .lock()
118 .unwrap_or_else(std::sync::PoisonError::into_inner)
119 .clone();
120 let screen = screen_slot
121 .lock()
122 .unwrap_or_else(std::sync::PoisonError::into_inner)
123 .clone();
124
125 if let Some(bytes) = cam {
126 let expected = self.scene.cam_dims().byte_len();
127 if bytes.len() == expected {
128 self.scene.set_camera_frame(&self.app, &bytes);
133 self.has_camera_frame = true;
134 } else {
135 tracing::trace!(
136 got = bytes.len(),
137 expected,
138 "compose: cam frame size mismatch, dropping"
139 );
140 }
141 }
142 if let Some(bytes) = screen {
143 let expected = self.scene.screen_dims().byte_len();
144 if bytes.len() == expected {
145 self.scene.set_screen_frame(&self.app, &bytes);
146 self.has_screen_frame = true;
147 } else {
148 tracing::trace!(
149 got = bytes.len(),
150 expected,
151 "compose: screen frame size mismatch, dropping"
152 );
153 }
154 }
155
156 if !self.has_camera_frame && !self.has_screen_frame {
157 return None;
158 }
159
160 self.scene.set_camera_visible(self.has_camera_frame);
171 self.scene.set_screen_visible(self.has_screen_frame);
172
173 let _stats = self.renderer.render_stage(
174 &self.app,
175 self.target.view(),
176 Color::BLACK,
177 self.scene.stage(),
178 );
179
180 let bytes = self.target.read_pixels(&self.app);
181 Some(ComposedFrame {
182 bytes,
183 width: self.width,
184 height: self.height,
185 })
186 }
187
188 pub fn set_screen_transform(&mut self, transform: wisp::Transform) {
193 self.scene.set_screen_transform(transform);
194 }
195
196 pub fn set_screen_clip(&mut self, shape: Option<wisp::MaskShape>) {
200 self.scene.set_screen_clip(shape);
201 }
202
203 pub fn set_background_gradient(&mut self, from: Color, to: Color, angle_deg: f32) {
206 self.scene.set_background_gradient(from, to, angle_deg);
207 }
208
209 pub fn set_background_color(&mut self, color: Color) {
211 self.scene.set_background_color(color);
212 }
213
214 pub fn set_background_visible(&mut self, visible: bool) {
216 self.scene.set_background_visible(visible);
217 }
218
219 pub fn set_frame_shadow(
222 &mut self,
223 window: wisp::Rect,
224 corner_radius: f32,
225 offset: wisp::Vec2,
226 color: Color,
227 ) {
228 self.scene
229 .set_frame_shadow(window, corner_radius, offset, color);
230 }
231
232 pub fn set_frame_shadow_visible(&mut self, visible: bool) {
234 self.scene.set_frame_shadow_visible(visible);
235 }
236
237 pub fn set_frame_border(
239 &mut self,
240 window: wisp::Rect,
241 corner_radius: f32,
242 stroke: wisp::Stroke,
243 ) {
244 self.scene.set_frame_border(window, corner_radius, stroke);
245 }
246
247 pub fn set_frame_border_visible(&mut self, visible: bool) {
249 self.scene.set_frame_border_visible(visible);
250 }
251
252 pub fn set_background_wallpaper(&mut self, width: u32, height: u32, rgba: &[u8]) {
255 self.scene
256 .set_background_wallpaper(&self.app, width, height, rgba);
257 }
258
259 pub fn set_background_wallpaper_visible(&mut self, visible: bool) {
261 self.scene.set_background_wallpaper_visible(visible);
262 }
263
264 pub fn set_cursor(&mut self, pointer_ndc: wisp::Vec2, half: f32, ripples: &[CursorRipple]) {
267 self.scene.set_cursor(pointer_ndc, half, ripples);
268 }
269
270 pub fn set_cursor_visible(&mut self, visible: bool) {
272 self.scene.set_cursor_visible(visible);
273 }
274
275 #[must_use]
277 pub fn dimensions(&self) -> (u32, u32) {
278 (self.width, self.height)
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use std::sync::{Arc, Mutex};
286
287 fn test_dims() -> (StreamDimensions, StreamDimensions) {
288 (StreamDimensions::new(64, 64), StreamDimensions::new(32, 32))
289 }
290
291 #[test]
292 fn new_allocates_compose_pipeline() {
293 let (screen, cam) = test_dims();
294 let compose = RecordingCompose::new(64, 64, screen, cam).expect("init");
295 assert_eq!(compose.dimensions(), (64, 64));
296 }
297
298 #[test]
299 fn compose_frame_returns_none_when_slots_empty() {
300 let (screen, cam) = test_dims();
301 let mut compose = RecordingCompose::new(64, 64, screen, cam).expect("init");
302 let cam_slot: FrameSlot = Arc::new(Mutex::new(None));
303 let screen_slot: FrameSlot = Arc::new(Mutex::new(None));
304 assert!(compose.compose_frame(&cam_slot, &screen_slot).is_none());
305 }
306
307 #[test]
308 fn compose_frame_returns_bgra_after_screen_upload() {
309 let (screen, cam) = test_dims();
310 let mut compose = RecordingCompose::new(64, 64, screen, cam).expect("init");
311 let cam_slot: FrameSlot = Arc::new(Mutex::new(None));
312 let screen_slot: FrameSlot = Arc::new(Mutex::new(Some(vec![128u8; 64 * 64 * 4])));
313 let composed = compose
314 .compose_frame(&cam_slot, &screen_slot)
315 .expect("frame produced");
316 assert_eq!(composed.width, 64);
317 assert_eq!(composed.height, 64);
318 assert_eq!(composed.bytes.len(), 64 * 64 * 4);
320 }
321
322 #[test]
323 fn compose_frame_drops_size_mismatched_uploads() {
324 let (screen, cam) = test_dims();
325 let mut compose = RecordingCompose::new(64, 64, screen, cam).expect("init");
326 let cam_slot: FrameSlot = Arc::new(Mutex::new(Some(vec![0u8; 99])));
327 let screen_slot: FrameSlot = Arc::new(Mutex::new(None));
328 assert!(compose.compose_frame(&cam_slot, &screen_slot).is_none());
329 }
330
331 #[test]
332 fn compose_frame_hides_cam_when_only_screen_uploads() {
333 let (screen, cam) = test_dims();
339 let mut compose = RecordingCompose::new(64, 64, screen, cam).expect("init");
340 let cam_slot: FrameSlot = Arc::new(Mutex::new(None));
341 let screen_slot: FrameSlot = Arc::new(Mutex::new(Some(vec![128u8; 64 * 64 * 4])));
342
343 let _ = compose
344 .compose_frame(&cam_slot, &screen_slot)
345 .expect("frame produced");
346
347 let cam_id = compose.scene.cam_container_id();
348 assert!(
349 !compose
350 .scene
351 .stage()
352 .get(cam_id)
353 .unwrap()
354 .container()
355 .visible,
356 "cam container must be hidden when no cam frame was uploaded"
357 );
358 let screen_id = compose.scene.screen_sprite_id();
359 assert!(
360 compose
361 .scene
362 .stage()
363 .get(screen_id)
364 .unwrap()
365 .container()
366 .visible,
367 "screen sprite must be visible after a screen-frame upload"
368 );
369 }
370
371 #[test]
372 fn compose_frame_leaves_slots_intact_for_other_consumers() {
373 let (screen, cam) = test_dims();
378 let mut compose = RecordingCompose::new(64, 64, screen, cam).expect("init");
379 let cam_slot: FrameSlot = Arc::new(Mutex::new(None));
380 let screen_bytes = vec![64u8; 64 * 64 * 4];
381 let screen_slot: FrameSlot = Arc::new(Mutex::new(Some(screen_bytes.clone())));
382 let _ = compose.compose_frame(&cam_slot, &screen_slot);
383 assert_eq!(screen_slot.lock().unwrap().as_ref(), Some(&screen_bytes));
385 assert!(cam_slot.lock().unwrap().is_none());
387 }
388}