Skip to main content

cvkg_render_native/
renderer.rs

1use crate::asset_manager::NativeAssetManager;
2use crate::audio::{RodioAudioEngine, VisualHapticEngine};
3use crate::main_loop::{App, AppEvent};
4use crate::window::{SafeAreaInsets, WindowManager};
5use cvkg_core::{
6    ColorTheme, DrawMaterial, FrameRenderer, Material3D, Mesh, Rect, RenderIntensityMode,
7    RenderStateSnapshot, Renderer, TelemetryData, Transform3D,
8};
9use std::sync::Arc;
10use winit::event_loop::{ControlFlow, EventLoop};
11#[cfg(target_os = "linux")]
12use winit::platform::wayland::EventLoopBuilderExtWayland;
13use winit::window::Window;
14
15thread_local! {
16    /// Thread-local raw pointer to the locked GpuRenderer for the duration of one render pass.
17    ///
18    /// # Safety
19    /// This pointer is ONLY valid when a `MutexGuard<GpuRenderer>` is held on the same thread's
20    /// call stack. It is set at the start of `begin_frame` and cleared at the end of `end_frame`.
21    /// Accessing the pointer when no guard is held is undefined behavior.
22    ///
23    /// PRIVATE: Not `pub` to prevent external crates from creating dangling references.
24    pub(crate) static GPU_FRAME_PTR: std::cell::Cell<*mut cvkg_render_gpu::GpuRenderer> =
25        const { std::cell::Cell::new(std::ptr::null_mut()) };
26}
27
28/// RAII guard that clears `GPU_FRAME_PTR` on drop.
29///
30/// # Safety
31/// Construct ONLY while holding a `MutexGuard<GpuRenderer>` that outlives this guard.
32pub(crate) struct GpuFramePtrGuard;
33
34impl GpuFramePtrGuard {
35    /// Set the thread-local raw pointer. Cleared automatically on drop.
36    ///
37    /// # Safety
38    /// `ptr` must reference memory that outlives this guard (typically a MutexGuard).
39    pub(crate) unsafe fn set(ptr: *mut cvkg_render_gpu::GpuRenderer) -> Self {
40        GPU_FRAME_PTR.with(|cell| cell.set(ptr));
41        Self
42    }
43}
44
45impl Drop for GpuFramePtrGuard {
46    fn drop(&mut self) {
47        GPU_FRAME_PTR.with(|cell| cell.set(std::ptr::null_mut()));
48    }
49}
50
51/// Native renderer backend implementing the Renderer trait.
52/// It wraps a shared GpuRenderer for high-performance GPU drawing.
53/// During a render pass, GPU_FRAME_PTR is set so draw calls bypass the mutex.
54pub struct NativeRenderer {
55    pub(crate) gpu: Arc<std::sync::Mutex<cvkg_render_gpu::GpuRenderer>>,
56    pub(crate) delta_time: f32,
57    pub(crate) elapsed_time: f32,
58    pub(crate) berserker_mode: RenderIntensityMode,
59    pub(crate) rage: f32,
60    pub(crate) window: Arc<Window>,
61}
62
63impl NativeRenderer {
64    /// Returns a reference to the GPU renderer.
65    /// If GPU_FRAME_PTR is set (we're inside a locked render pass) uses that directly.
66    /// Otherwise falls back to acquiring the mutex (safe for calls outside the render pass).
67    ///
68    /// # Safety
69    /// GPU_FRAME_PTR is only non-null when a MutexGuard is live on the same thread's call stack.
70    #[inline(always)]
71    fn gpu_ref(&mut self) -> impl std::ops::DerefMut<Target = cvkg_render_gpu::GpuRenderer> + '_ {
72        GPU_FRAME_PTR.with(|ptr| {
73            let raw = ptr.get();
74            if !raw.is_null() {
75                // SAFETY: Pointer is valid and the mutex guard is live above us on the call stack.
76                GpuRef::Ptr(unsafe { &mut *raw })
77            } else {
78                GpuRef::Guard(self.gpu.lock().unwrap_or_else(|p| p.into_inner()))
79            }
80        })
81    }
82
83    /// Read-only variant for &self Renderer methods.
84    /// Uses the same thread_local fast path; falls back to mutex for out-of-pass calls.
85    ///
86    /// # Safety
87    /// GPU_FRAME_PTR is only non-null when a MutexGuard is live above us on the call stack.
88    #[inline(always)]
89    fn gpu_ref_shared(&self) -> impl std::ops::Deref<Target = cvkg_render_gpu::GpuRenderer> + '_ {
90        GPU_FRAME_PTR.with(|ptr| {
91            let raw = ptr.get();
92            if !raw.is_null() {
93                // SAFETY: Pointer is valid; the mutex guard is held for the render pass duration.
94                // We only read via this path during &self methods, which is safe.
95                GpuRefShared::Ptr(unsafe { &*raw })
96            } else {
97                GpuRefShared::Guard(self.gpu.lock().unwrap_or_else(|p| p.into_inner()))
98            }
99        })
100    }
101
102    /// Create a new NativeRenderer (internal use by App)
103    pub(crate) fn new(
104        window: Arc<Window>,
105        gpu: Arc<std::sync::Mutex<cvkg_render_gpu::GpuRenderer>>,
106        delta_time: f32,
107        elapsed_time: f32,
108        berserker_mode: RenderIntensityMode,
109        rage: f32,
110    ) -> Self {
111        Self {
112            gpu,
113            delta_time,
114            elapsed_time,
115            berserker_mode,
116            rage,
117            window,
118        }
119    }
120
121    /// Start the CVKG native application with the given view.
122    /// `prewarm_assets` is a list of (name, raw_bytes) pairs uploaded to the GPU
123    /// texture atlas on the first frame before any draw calls.
124    pub fn run<V: cvkg_core::View + 'static>(
125        view: V,
126        prewarm_assets: Option<Vec<(String, Vec<u8>)>>,
127    ) {
128        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
129            .format_timestamp_millis()
130            .try_init()
131            .ok();
132
133        let event_loop = EventLoop::<AppEvent>::with_user_event()
134            .with_any_thread(true)
135            .build()
136            .expect("failed to create winit event loop: platform initialization failed");
137        event_loop.set_control_flow(ControlFlow::Wait);
138
139        let mut app = App {
140            view,
141            window_manager: WindowManager::new(),
142            gpu: None,
143            asset_manager: std::sync::Arc::new(NativeAssetManager::new()),
144            proxy: event_loop.create_proxy(),
145            start_time: std::time::Instant::now(),
146            last_frame_time: std::time::Instant::now(),
147            berserker_mode: RenderIntensityMode::Normal,
148            rage: 0.0,
149            state_detector: crate::window::WindowStateDetector::new(),
150            frame_budget: cvkg_core::FrameBudgetTracker::default_120fps(),
151            modifiers: winit::keyboard::ModifiersState::default(),
152            audio_engine: None,
153            haptic_engine: Arc::new(VisualHapticEngine::new()),
154            pending_prewarm: prewarm_assets,
155        };
156
157        event_loop
158            .run_app(&mut app)
159            .expect("winit event loop terminated with error");
160    }
161
162    /// Convenience: run with a single background image loaded from a file path.
163    /// The image is loaded from disk and pre-warmed on the first frame.
164    /// `image_name` is the key used in `draw_image` / `draw_background_image`.
165    pub fn run_with_background<V: cvkg_core::View + 'static>(
166        view: V,
167        image_name: &str,
168        image_path: &str,
169    ) {
170        let image_data = std::fs::read(image_path)
171            .unwrap_or_else(|e| panic!("Failed to load background image '{}': {}", image_path, e));
172        let assets = vec![(image_name.to_string(), image_data)];
173        Self::run(view, Some(assets));
174    }
175}
176
177/// Returned by NativeRenderer::gpu_ref() — either a direct pointer ref or a mutex guard.
178enum GpuRef<'a> {
179    Ptr(&'a mut cvkg_render_gpu::GpuRenderer),
180    Guard(std::sync::MutexGuard<'a, cvkg_render_gpu::GpuRenderer>),
181}
182
183impl<'a> std::ops::Deref for GpuRef<'a> {
184    type Target = cvkg_render_gpu::GpuRenderer;
185    fn deref(&self) -> &Self::Target {
186        match self {
187            GpuRef::Ptr(r) => r,
188            GpuRef::Guard(g) => g,
189        }
190    }
191}
192
193impl<'a> std::ops::DerefMut for GpuRef<'a> {
194    fn deref_mut(&mut self) -> &mut Self::Target {
195        match self {
196            GpuRef::Ptr(r) => r,
197            GpuRef::Guard(g) => &mut *g,
198        }
199    }
200}
201
202/// Read-only variant returned by NativeRenderer::gpu_ref_shared().
203enum GpuRefShared<'a> {
204    Ptr(&'a cvkg_render_gpu::GpuRenderer),
205    Guard(std::sync::MutexGuard<'a, cvkg_render_gpu::GpuRenderer>),
206}
207
208impl<'a> std::ops::Deref for GpuRefShared<'a> {
209    type Target = cvkg_render_gpu::GpuRenderer;
210    fn deref(&self) -> &Self::Target {
211        match self {
212            GpuRefShared::Ptr(r) => r,
213            GpuRefShared::Guard(g) => g,
214        }
215    }
216}
217
218impl cvkg_core::ElapsedTime for NativeRenderer {
219    fn delta_time(&self) -> f32 {
220        self.delta_time
221    }
222
223    fn elapsed_time(&self) -> f32 {
224        self.elapsed_time
225    }
226}
227
228impl cvkg_core::RendererErrorHandler for NativeRenderer {}
229
230impl cvkg_core::Renderer for NativeRenderer {
231    fn begin_world_space_panel(
232        &mut self,
233        node_id: u64,
234        transform: &cvkg_core::Transform3D,
235        glass: Option<cvkg_materials::GlassMaterial>,
236        pixels_per_unit: f32,
237        world_size: (f32, f32),
238    ) {
239        self.gpu_ref().begin_world_space_panel(
240            node_id,
241            transform,
242            glass,
243            pixels_per_unit,
244            world_size,
245        );
246    }
247
248    fn end_world_space_panel(&mut self, node_id: u64) {
249        self.gpu_ref().end_world_space_panel(node_id);
250    }
251
252    fn draw_mesh_3d(&mut self, mesh: &Mesh, material: &Material3D, transform: &Transform3D) {
253        self.gpu_ref().draw_mesh_3d(mesh, material, transform);
254    }
255
256    fn fill_rect(&mut self, rect: Rect, color: [f32; 4]) {
257        self.gpu_ref().fill_rect(rect, color);
258    }
259    fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4]) {
260        self.gpu_ref().fill_rounded_rect(rect, radius, color);
261    }
262    fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]) {
263        self.gpu_ref().fill_ellipse(rect, color);
264    }
265    fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
266        self.gpu_ref().stroke_rect(rect, color, stroke_width);
267    }
268    fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], stroke_width: f32) {
269        self.gpu_ref()
270            .stroke_rounded_rect(rect, radius, color, stroke_width);
271    }
272    fn stroke_ellipse(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
273        self.gpu_ref().stroke_ellipse(rect, color, stroke_width);
274    }
275    fn draw_line(
276        &mut self,
277        x1: f32,
278        y1: f32,
279        x2: f32,
280        y2: f32,
281        color: [f32; 4],
282        stroke_width: f32,
283    ) {
284        self.gpu_ref()
285            .draw_line(x1, y1, x2, y2, color, stroke_width);
286    }
287
288    fn fill_glass_rect(&mut self, rect: Rect, radius: f32, blur_radius: f32) {
289        self.gpu_ref().fill_glass_rect(rect, radius, blur_radius);
290    }
291
292    fn fill_glass_rect_with_intensity(
293        &mut self,
294        rect: Rect,
295        radius: f32,
296        blur_radius: f32,
297        glass_intensity: f32,
298    ) {
299        self.gpu_ref()
300            .fill_glass_rect_with_intensity(rect, radius, blur_radius, glass_intensity);
301    }
302
303    fn fill_glass_rect_with_pressure(
304        &mut self,
305        rect: Rect,
306        radius: f32,
307        blur_radius: f32,
308        pressure: f32,
309    ) {
310        self.gpu_ref()
311            .fill_glass_rect_with_intensity(rect, radius, blur_radius, pressure);
312    }
313
314    fn fill_squircle(&mut self, rect: Rect, n: f32, color: [f32; 4]) {
315        self.gpu_ref().fill_squircle(rect, n, color);
316    }
317
318    fn stroke_squircle(&mut self, rect: Rect, n: f32, color: [f32; 4], stroke_width: f32) {
319        self.gpu_ref().stroke_squircle(rect, n, color, stroke_width);
320    }
321
322    fn draw_focus_ring(
323        &mut self,
324        rect: Rect,
325        radius: f32,
326        offset: f32,
327        width: f32,
328        color: [f32; 4],
329    ) {
330        self.gpu_ref()
331            .draw_focus_ring(rect, radius, offset, width, color);
332    }
333
334    fn draw_linear_gradient(
335        &mut self,
336        rect: Rect,
337        start_color: [f32; 4],
338        end_color: [f32; 4],
339        angle: f32,
340    ) {
341        self.gpu_ref()
342            .draw_linear_gradient(rect, start_color, end_color, angle);
343    }
344    fn draw_radial_gradient(&mut self, rect: Rect, inner_color: [f32; 4], outer_color: [f32; 4]) {
345        self.gpu_ref()
346            .draw_radial_gradient(rect, inner_color, outer_color);
347    }
348    fn draw_texture(&mut self, texture_id: u32, rect: Rect) {
349        self.gpu_ref().draw_texture(texture_id, rect);
350    }
351    fn draw_image(&mut self, image_name: &str, rect: Rect) {
352        self.gpu_ref().draw_image(image_name, rect);
353    }
354    fn load_image(&mut self, name: &str, data: &[u8]) {
355        self.gpu_ref().load_image(name, data);
356    }
357    fn push_clip_rect(&mut self, rect: Rect) {
358        self.gpu_ref().push_clip_rect(rect);
359    }
360    fn pop_clip_rect(&mut self) {
361        self.gpu_ref().pop_clip_rect();
362    }
363    fn push_opacity(&mut self, opacity: f32) {
364        self.gpu_ref().push_opacity(opacity);
365    }
366    fn draw_3d_cube(&mut self, rect: Rect, color: [f32; 4], rotation: [f32; 3]) {
367        self.gpu_ref().draw_3d_cube(rect, color, rotation);
368    }
369    fn render_scene_node_3d(
370        &mut self,
371        position: [f32; 3],
372        rotation: [f32; 4],
373        scale: [f32; 3],
374        color: [f32; 4],
375        meshes: &[Mesh],
376    ) {
377        self.gpu_ref()
378            .render_scene_node_3d(position, rotation, scale, color, meshes);
379    }
380    fn pop_opacity(&mut self) {
381        self.gpu_ref().pop_opacity();
382    }
383    fn bifrost(&mut self, rect: Rect, blur: f32, saturation: f32, opacity: f32) {
384        self.gpu_ref().bifrost(rect, blur, saturation, opacity);
385    }
386    fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
387        self.gpu_ref().push_mjolnir_slice(angle, offset);
388    }
389    fn pop_mjolnir_slice(&mut self) {
390        self.gpu_ref().pop_mjolnir_slice();
391    }
392    fn mjolnir_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
393        self.gpu_ref().mjolnir_shatter(rect, pieces, force, color);
394    }
395    fn mjolnir_fluid_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
396        self.gpu_ref()
397            .mjolnir_fluid_shatter(rect, pieces, force, color);
398    }
399    fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
400        self.gpu_ref().draw_mjolnir_bolt(from, to, color);
401    }
402    fn gungnir(&mut self, rect: Rect, color: [f32; 4], radius: f32, intensity: f32) {
403        self.gpu_ref().gungnir(rect, color, radius, intensity);
404    }
405    fn mani_glow(&mut self, rect: Rect, color: [f32; 4], radius: f32) {
406        self.gpu_ref().mani_glow(rect, color, radius);
407    }
408    fn register_handler(
409        &mut self,
410        event_type: &str,
411        handler: Arc<dyn Fn(cvkg_core::Event) + Send + Sync>,
412    ) {
413        self.gpu_ref().register_handler(event_type, handler);
414    }
415    fn push_vnode(&mut self, rect: Rect, name: &'static str) {
416        self.gpu_ref().push_vnode(rect, name);
417    }
418    fn pop_vnode(&mut self) {
419        self.gpu_ref().pop_vnode();
420    }
421    fn set_z_index(&mut self, z: f32) {
422        self.gpu_ref().set_z_index(z);
423    }
424    fn get_z_index(&self) -> f32 {
425        self.gpu_ref_shared().get_z_index()
426    }
427    fn register_shared_element(&mut self, id: &str, rect: Rect) {
428        self.gpu_ref().register_shared_element(id, rect);
429    }
430    fn set_material(&mut self, material: DrawMaterial) {
431        self.gpu_ref().set_material(material);
432    }
433    fn current_material(&self) -> DrawMaterial {
434        self.gpu_ref_shared().current_material()
435    }
436    fn serialize_svg(&mut self, name: &str) -> Result<String, String> {
437        self.gpu_ref().serialize_svg(name)
438    }
439    fn apply_svg_filter(
440        &mut self,
441        name: &str,
442        filter_id: &str,
443        region: Rect,
444    ) -> Result<String, String> {
445        self.gpu_ref().apply_svg_filter(name, filter_id, region)
446    }
447    fn push_shadow(&mut self, radius: f32, color: [f32; 4], offset: [f32; 2]) {
448        self.gpu_ref().push_shadow(radius, color, offset);
449    }
450    fn pop_shadow(&mut self) {
451        self.gpu_ref().pop_shadow();
452    }
453    fn push_affine(&mut self, transform: [f32; 6]) {
454        self.gpu_ref().push_affine(transform);
455    }
456    fn enter_portal(&mut self, z_index: i32) {
457        tracing::warn!(
458            "Portal rendering (enter_portal) not yet implemented in GPU backend; z_index={}",
459            z_index
460        );
461    }
462    fn exit_portal(&mut self) {
463        tracing::warn!("Portal rendering (exit_portal) not yet implemented in GPU backend");
464    }
465    fn viewport_size(&self) -> Rect {
466        let size = self.window.inner_size();
467        let scale = self.window.scale_factor();
468        let logical = size.to_logical::<f32>(scale);
469        Rect::new(0.0, 0.0, logical.width, logical.height)
470    }
471    fn announce(&mut self, message: &str, priority: cvkg_core::AnnouncementPriority) {
472        tracing::info!("Accessibility announcement [{:?}]: {}", priority, message);
473    }
474    fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
475        self.gpu_ref().load_svg(name, svg_data);
476    }
477    fn draw_svg(&mut self, name: &str, rect: Rect) {
478        self.gpu_ref().draw_svg(name, rect, None, 0);
479    }
480    fn draw_svg_with_offset(&mut self, name: &str, rect: Rect, animation_time_offset: f32) {
481        self.gpu_ref()
482            .draw_svg_with_offset(name, rect, None, 0, animation_time_offset);
483    }
484    fn get_telemetry(&self) -> TelemetryData {
485        self.gpu_ref_shared().telemetry.clone()
486    }
487    fn prewarm_vram(&mut self, assets: Vec<(String, Vec<u8>)>) {
488        self.gpu_ref().prewarm_vram(assets);
489    }
490
491    fn text_scale_factor(&self) -> f32 {
492        self.gpu_ref_shared().text_scale_factor()
493    }
494
495    fn is_over_budget(&self) -> bool {
496        self.gpu_ref_shared().is_over_budget()
497    }
498
499    fn draw_text(
500        &mut self,
501        text: &str,
502        rect: &Rect,
503        size: f32,
504        color: [f32; 4],
505        h_align: cvkg_core::TextHAlign,
506        v_align: cvkg_core::TextVAlign,
507    ) {
508        self.gpu_ref()
509            .draw_text(text, rect, size, color, h_align, v_align);
510    }
511
512    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
513        self.gpu_ref().measure_text(text, size)
514    }
515
516    fn shape_rich_text(
517        &mut self,
518        spans: &[runic_text::TextSpan],
519        max_width: Option<f32>,
520        align: runic_text::TextAlign,
521        overflow: runic_text::TextOverflow,
522    ) -> Option<runic_text::ShapedText> {
523        self.gpu_ref()
524            .shape_rich_text(spans, max_width, align, overflow)
525    }
526
527    fn draw_shaped_text(&mut self, shaped: &runic_text::ShapedText, x: f32, y: f32) {
528        self.gpu_ref().draw_shaped_text(shaped, x, y);
529    }
530
531    fn fill_glass_rect_with_tint(
532        &mut self,
533        rect: Rect,
534        radius: f32,
535        blur_radius: f32,
536        tint_color: [f32; 4],
537        glass_intensity: f32,
538    ) {
539        self.gpu_ref().fill_glass_rect_with_tint(
540            rect,
541            radius,
542            blur_radius,
543            tint_color,
544            glass_intensity,
545        );
546    }
547
548    fn set_theme(&mut self, theme: ColorTheme) {
549        self.gpu_ref().set_theme(theme);
550    }
551
552    fn trigger_shatter_event(&mut self, origin: [f32; 2], force: f32) {
553        self.gpu_ref().trigger_shatter_event(origin, force);
554    }
555
556    fn set_fireball_pos(&mut self, pos: [f32; 2]) {
557        self.gpu_ref().set_fireball_pos(pos);
558    }
559
560    fn set_scene(&mut self, scene: &str) {
561        self.gpu_ref().set_scene(scene);
562    }
563
564    fn set_scene_preset(&mut self, preset: u32) {
565        self.gpu_ref().set_scene_preset(preset);
566    }
567
568    fn set_default_background_color(&mut self, color: [f32; 4]) {
569        self.gpu_ref().set_default_background_color(color);
570    }
571    fn push_transform(&mut self, translation: [f32; 2], scale: [f32; 2], rotation: f32) {
572        self.gpu_ref().push_transform(translation, scale, rotation);
573    }
574    fn pop_transform(&mut self) {
575        self.gpu_ref().pop_transform();
576    }
577
578    fn set_berserker_mode(&mut self, state: RenderIntensityMode) {
579        self.berserker_mode = state;
580
581        if state == RenderIntensityMode::GodMode {
582            tracing::info!("ENTERING GOD MODE: Activating Berserker Determinism (High Priority)");
583            #[cfg(target_os = "linux")]
584            unsafe {
585                let ret = libc::setpriority(libc::PRIO_PROCESS, 0, -10);
586                if ret != 0 {
587                    tracing::warn!(
588                        "GodMode: setpriority failed (errno: {}) — need CAP_SYS_NIO",
589                        std::io::Error::last_os_error()
590                    );
591                }
592            }
593        } else {
594            #[cfg(target_os = "linux")]
595            unsafe {
596                let _ = libc::setpriority(libc::PRIO_PROCESS, 0, 0);
597            }
598        }
599
600        self.gpu_ref().set_berserker_mode(state);
601    }
602
603    fn set_rage(&mut self, rage: f32) {
604        self.rage = rage;
605        self.gpu_ref().set_rage(rage);
606    }
607
608    fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
609        self.gpu_ref().memoize(id, data_hash, render_fn);
610    }
611
612    fn snapshot_render_state(&self) -> RenderStateSnapshot {
613        self.gpu_ref_shared().snapshot_render_state()
614    }
615
616    fn restore_render_state(&mut self, snap: RenderStateSnapshot) {
617        self.gpu_ref().restore_render_state(snap);
618    }
619    fn request_redraw(&mut self) {
620        self.window.request_redraw();
621    }
622
623    fn capture_png(&mut self) -> Vec<u8> {
624        tracing::info!("CAPTURING_FRAME: Initiating GPU readback...");
625        let gpu = self.gpu.lock().unwrap_or_else(|p| p.into_inner());
626        pollster::block_on(gpu.capture_frame()).unwrap_or_else(|e| {
627            tracing::error!("GPU frame capture failed: {}", e);
628            Vec::new()
629        })
630    }
631
632    fn print(&mut self) {
633        tracing::info!("PRINT_BRIDGE: Spooling mission status to native printer...");
634        tracing::debug!("[BRIDGE] PRINTER_READY // SPOOLING_DATA...");
635    }
636}