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