Skip to main content

cvkg_render_native/
lib.rs

1//! # CVKG Agentic Development Guidelines (v1.3)
2//!
3//! All AI agents contributing to this crate MUST follow ALL eight rules:
4//!
5//! ── Karpathy Guidelines (1–4) ────────────────────────────────────────────
6//! 1. THINK FIRST     — State assumptions. Surface ambiguity. Push back on complexity.
7//! 2. STAY SIMPLE     — Minimum code. No speculative features. No unasked-for abstractions.
8//! 3. BE SURGICAL     — Touch only what's required. Own your orphans. Don't improve neighbors.
9//! 4. VERIFY GOALS    — Turn tasks into checkable criteria. Loop until they pass. Never commit broken.
10//!
11//! ── CVKG Extended Protocols (5–8) ────────────────────────────────────────
12//! 5. TRIPLE-PASS     — Read the target, its surrounding context, and its full call graph
13//!                      at least THREE TIMES before making any edit or revision.
14//! 6. COMMENT ALL     — Every major pub fn, unsafe block, and non-trivial algorithm in
15//!                      every .rs/.ts/.h/.wgsl file MUST have a descriptive doc comment.
16//!                      Comments describe WHY and WHAT CONTRACT, not HOW mechanically.
17//! 7. MONITOR LOOPS   — Check every tool call / command for progress every 30 seconds.
18//!                      After 3 consecutive identical failures, stop, write BLOCKED.md,
19//!                      and move to unblocked work. Never silently accept a broken state.
20//! 8. HARDWARE VERIFIED — NEVER declare success based on mock data/rendering for native crates.
21//!                      Any change to input, rendering, or lifecycle MUST be verified via physical 
22//!                      loopback (e.g., cargo run -p berserker) and signal path tracing.
23//!
24//! Sources:
25//! Karpathy: https://github.com/multica-ai/andrej-karpathy-skills
26//! CVKG Extended: Section 14 of the CVKG Design Specification (v1.3)
27
28//! Platform-native widget delegation using winit and AccessKit
29//!
30//! This crate provides platform-specific rendering backends for native desktop targets
31//  using winit for window/event handling and AccessKit for accessibility tree integration.
32
33use cvkg_core::{FrameRenderer, Renderer};
34// FIX #10: Wayland import gated to Linux only — was unconditional, broke macOS/Windows builds.
35#[cfg(target_os = "linux")]
36use std::sync::Arc;
37use winit::{
38    application::ApplicationHandler,
39    event::{DeviceId, DeviceEvent, WindowEvent},
40    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
41    window::{Window, WindowId},
42};
43
44
45/// Native renderer backend implementing the Renderer trait.
46/// It wraps a shared SurtrRenderer for high-performance GPU drawing.
47pub struct NativeRenderer {
48    gpu: Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>,
49    delta_time: f32,
50    elapsed_time: f32,
51    berserker_mode: cvkg_core::BerserkerMode,
52    rage: f32,
53    window: Arc<Window>,
54}
55
56/// Custom events for the native application event loop
57#[derive(Debug)]
58enum AppEvent {
59    AccessibilityAction(accesskit::ActionRequest),
60}
61
62impl NativeRenderer {
63    /// Create a new NativeRenderer (internal use by App)
64    fn new(window: Arc<Window>, gpu: Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>, delta_time: f32, elapsed_time: f32, berserker_mode: cvkg_core::BerserkerMode, rage: f32) -> Self {
65        Self { gpu, delta_time, elapsed_time, berserker_mode, rage, window }
66    }
67
68    /// Start the CVKG native application with the given view.
69    /// This is the main entry point for desktop applications.
70    pub fn run<V: cvkg_core::View + 'static>(view: V) {
71        let event_loop = EventLoop::<AppEvent>::with_user_event()
72            .build()
73            .expect("Failed to create event loop");
74        event_loop.set_control_flow(ControlFlow::Wait);
75
76        let mut app = App {
77            view,
78            windows: std::collections::HashMap::new(),
79            gpu: None,
80            asset_manager: std::sync::Arc::new(NativeAssetManager::new()),
81            proxy: event_loop.create_proxy(),
82            start_time: std::time::Instant::now(),
83            last_frame_time: std::time::Instant::now(),
84            berserker_mode: cvkg_core::BerserkerMode::Normal,
85            rage: 0.0,
86        };
87
88        event_loop.run_app(&mut app).expect("Event loop error");
89    }
90}
91
92struct WindowState {
93    window: Arc<Window>,
94    accesskit_adapter: Option<accesskit_winit::Adapter>,
95    vdom: Option<cvkg_vdom::VDom>,
96    cursor_pos: [f32; 2],
97    /// The instant the last redraw finished, used for measuring inter-frame gap timing.
98    last_redraw_start: std::time::Instant,
99    /// Sliding window of frame times for tail latency (P99) calculation.
100    frame_history: std::collections::VecDeque<f32>,
101    /// Total frames rendered on this window.
102    frame_count: u64,
103    /// Last window position for shake detection.
104    last_pos: Option<[i32; 2]>,
105}
106
107struct App<V: cvkg_core::View> {
108    view: V,
109    windows: std::collections::HashMap<WindowId, WindowState>,
110    gpu: Option<Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>>,
111    #[allow(dead_code)]
112    asset_manager: std::sync::Arc<NativeAssetManager>,
113    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
114    start_time: std::time::Instant,
115    last_frame_time: std::time::Instant,
116    berserker_mode: cvkg_core::BerserkerMode,
117    rage: f32,
118}
119
120impl<V: cvkg_core::View + 'static> ApplicationHandler<AppEvent> for App<V> {
121    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
122        if self.gpu.is_none() {
123            log::info!("[Native] App instance (resumed): {:p}", self);
124            
125            let window_attrs = Window::default_attributes()
126                .with_title("CVKG Berserker")
127                .with_visible(true)
128                .with_transparent(false)
129                .with_decorations(true)
130                .with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0));
131
132            let window = Arc::new(
133                event_loop
134                    .create_window(window_attrs)
135                    .expect("Failed to create window"),
136            );
137            
138            let window_id = window.id();
139            let vdom = cvkg_vdom::VDom::build(&self.view, cvkg_core::Rect::new(0.0, 0.0, 1280.0, 720.0));
140            
141            log::info!("[Native] INSERTING window ID: {:?}", window_id);
142            
143            self.windows.insert(window_id, WindowState {
144                window: window.clone(),
145                accesskit_adapter: None,
146                vdom: Some(vdom),
147                cursor_pos: [0.0, 0.0],
148                last_redraw_start: std::time::Instant::now(),
149                frame_history: std::collections::VecDeque::with_capacity(60),
150                frame_count: 0,
151                last_pos: None,
152            });
153
154            // Immediately set self.gpu to prevent re-entry
155            let gpu = pollster::block_on(cvkg_render_gpu::SurtrRenderer::forge(window.clone()));
156            self.gpu = Some(Arc::new(std::sync::Mutex::new(gpu)));
157            
158            log::info!("[Native] Initialization complete.");
159            window.request_redraw();
160        }
161    }
162
163    fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
164        if matches!(cause, winit::event::StartCause::Poll) {
165             // Too noisy
166        } else {
167             log::debug!("[Native] Event Loop Wake: {:?}", cause);
168        }
169    }
170
171    fn device_event(&mut self, _event_loop: &ActiveEventLoop, _device_id: winit::event::DeviceId, event: winit::event::DeviceEvent) {
172        if matches!(event, winit::event::DeviceEvent::MouseMotion { .. }) {
173            // log::trace!("[Native] Raw Mouse Motion");
174        } else {
175            log::info!("[Native] DEVICE EVENT: {:?}", event);
176        }
177    }
178
179    fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
180        if !matches!(event, WindowEvent::RedrawRequested) && !matches!(event, WindowEvent::CursorMoved { .. }) {
181            log::info!("[Native] App instance: {:p} | WINDOW EVENT: {:?}", self, event);
182        }
183        
184        let gpu_arc = if let Some(g) = &self.gpu { 
185            g.clone() 
186        } else { 
187            log::warn!("[Native] DROPPING EVENT: GPU not initialized yet");
188            return; 
189        };
190        
191        let state = if let Some(s) = self.windows.get_mut(&id) { 
192            s 
193        } else { 
194            return; 
195        };
196
197        match event {
198            WindowEvent::Moved(pos) => {
199                let dx = state.last_pos.map_or(0, |last| pos.x - last[0]);
200                let dy = state.last_pos.map_or(0, |last| pos.y - last[1]);
201                let speed = ((dx.pow(2) + dy.pow(2)) as f32).sqrt();
202                
203                if speed > 0.1 {
204                    // Significant kinetic injection
205                    self.rage = (self.rage + 0.2).min(1.0);
206                    log::info!("[Native] Kinetic Injection! Rage: {}", self.rage);
207                }
208                
209                state.last_pos = Some([pos.x, pos.y]);
210                state.window.request_redraw();
211            }
212            WindowEvent::CloseRequested => {
213                self.windows.remove(&id);
214                if self.windows.is_empty() {
215                    event_loop.exit();
216                }
217            }
218            WindowEvent::Resized(physical_size) => {
219                // FIX #3: All lock().unwrap() calls in the render path replaced with
220                // lock().expect("...") providing actionable context on panic. The GPU
221                // mutex should never be poisoned under correct usage; expect() surfaces
222                // the failure clearly rather than producing an opaque unwrap panic.
223                gpu_arc.lock().expect("GPU mutex poisoned during resize").resize(
224                    id,
225                    physical_size.width,
226                    physical_size.height,
227                    state.window.scale_factor() as f32,
228                );
229                state.window.request_redraw();
230            }
231            WindowEvent::Focused(focused) => {
232                log::info!("[Native] Window focus changed: {}", focused);
233            }
234            WindowEvent::RedrawRequested => {
235                if state.frame_count % 60 == 0 {
236                    log::info!("[Native] RedrawRequested (frame {})", state.frame_count);
237                }
238                let size = state.window.inner_size();
239                let scale = state.window.scale_factor();
240                let logical_size = size.to_logical::<f32>(scale);
241
242                let rect = cvkg_core::Rect {
243                    x: 0.0,
244                    y: 0.0,
245                    width: logical_size.width,
246                    height: logical_size.height,
247                };
248
249                // Record the start of this redraw and snapshot the previous frame's
250                // start time before overwriting it, so inter-frame gap is measurable.
251                let redraw_start = std::time::Instant::now();
252                let last_redraw_start = state.last_redraw_start;
253                // Update last_redraw_start immediately so the next frame measures correctly
254                // even if this frame returns early.
255                state.last_redraw_start = redraw_start;
256
257                // Build new vdom and diff (layout pass)
258                let layout_start = std::time::Instant::now();
259                let new_vdom = cvkg_vdom::VDom::build(&self.view, rect);
260                let layout_end = std::time::Instant::now();
261
262                // Apply patches to the accessibility tree and the previous VDOM
263                let state_flush_start = std::time::Instant::now();
264                if let Some(prev_vdom) = &mut state.vdom {
265                    let patches = prev_vdom.diff(&new_vdom);
266                    let mut nodes = Vec::new();
267                    for patch in &patches {
268                        if let cvkg_vdom::VDomPatch::Create(node) | cvkg_vdom::VDomPatch::Replace { node, .. } = patch {
269                            nodes.push((accesskit::NodeId(node.id.0), node.to_accesskit_node()));
270                        } else if let cvkg_vdom::VDomPatch::Update { id, .. } = patch
271                            && let Some(node) = new_vdom.nodes.get(id) {
272                            nodes.push((accesskit::NodeId(node.id.0), node.to_accesskit_node()));
273                        }
274                    }
275                    if !nodes.is_empty() {
276                        if let Some(adapter) = &mut state.accesskit_adapter {
277                            adapter.update_if_active(|| accesskit::TreeUpdate {
278                                nodes,
279                                tree: None,
280                                focus: accesskit::NodeId(1),
281                            });
282                        }
283                    }
284                    prev_vdom.apply_patches(patches);
285                } else {
286                    state.vdom = Some(new_vdom);
287                }
288                let state_flush_end = std::time::Instant::now();
289
290                // GPU rendering
291                let draw_start = std::time::Instant::now();
292                let delta_time = redraw_start.duration_since(last_redraw_start).as_secs_f32();
293                let elapsed_time = redraw_start.duration_since(self.start_time).as_secs_f32();
294                let mut gpu = gpu_arc.lock().expect("GPU mutex poisoned during frame begin");
295                let encoder = gpu.begin_frame(id);
296                let mut renderer = NativeRenderer::new(
297                    state.window.clone(),
298                    gpu_arc.clone(),
299                    delta_time,
300                    elapsed_time,
301                    self.berserker_mode,
302                    self.rage,
303                );
304                // Release the gpu lock before calling render — the render methods each
305                // re-acquire it per-call, allowing the view tree to interleave with other
306                // work without holding one giant critical section across the whole draw.
307                drop(gpu);
308                self.view.render(&mut renderer, rect);
309                let draw_end = std::time::Instant::now();
310
311                // Re-acquire to submit the frame
312                let gpu_submit_start = std::time::Instant::now();
313                let mut gpu = gpu_arc.lock().expect("GPU mutex poisoned during frame submit");
314                gpu.render_frame();
315                gpu.end_frame(encoder);
316                let gpu_submit_end = std::time::Instant::now();
317
318                // Build telemetry from this frame's timing measurements.
319                // NOTE: input_time_ms measures the inter-frame gap (time from end of last frame
320                // to start of this one), not input dispatch latency. The field name is defined
321                // in cvkg_core::TelemetryData and kept as-is to match that struct.
322                let mut telemetry = cvkg_core::TelemetryData::default();
323                telemetry.input_time_ms = redraw_start.duration_since(last_redraw_start).as_secs_f32() * 1000.0;
324                telemetry.layout_time_ms = layout_end.duration_since(layout_start).as_secs_f32() * 1000.0;
325                telemetry.state_flush_time_ms = state_flush_end.duration_since(state_flush_start).as_secs_f32() * 1000.0;
326                telemetry.draw_time_ms = draw_end.duration_since(draw_start).as_secs_f32() * 1000.0;
327                telemetry.gpu_submit_time_ms = gpu_submit_end.duration_since(gpu_submit_start).as_secs_f32() * 1000.0;
328
329                // Total frame time from redraw request to GPU submission complete
330                let frame_time_ms = gpu_submit_end.duration_since(redraw_start).as_secs_f32() * 1000.0;
331                telemetry.frame_time_ms = frame_time_ms;
332
333                // Tail Latency Tracking (P99 and Jitter) over a 100-frame sliding window.
334                state.frame_history.push_back(frame_time_ms);
335                if state.frame_history.len() > 100 {
336                    state.frame_history.pop_front();
337                }
338
339                let mut sorted_frames: Vec<f32> = state.frame_history.iter().copied().collect();
340                sorted_frames.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
341
342                if !sorted_frames.is_empty() {
343                    let p99_idx = (sorted_frames.len() as f32 * 0.99).floor() as usize;
344                    telemetry.p99_frame_time_ms = sorted_frames[p99_idx.min(sorted_frames.len() - 1)];
345
346                    // Jitter: standard deviation of frame times over the sliding window.
347                    let avg = sorted_frames.iter().sum::<f32>() / sorted_frames.len() as f32;
348                    let variance = sorted_frames.iter().map(|f| (f - avg).powi(2)).sum::<f32>()
349                        / sorted_frames.len() as f32;
350                    telemetry.frame_jitter_ms = variance.sqrt();
351                }
352
353                // FIX #8: hardware_stall_detected is now reset each frame based on current
354                // jitter rather than being set once and never cleared. A single jittery frame
355                // no longer permanently flags the session. Jitter > 20ms is a heuristic for
356                // scheduling disruption (GC, OS preemption, slow layout) — not a confirmed
357                // hardware stall, but the field name is defined in cvkg_core::TelemetryData.
358                telemetry.hardware_stall_detected = telemetry.frame_jitter_ms > 20.0;
359
360                // FIX #7: Removed anti-analysis EnvironmentShield probe and enforce_mitigation
361                // calls. This code ran every 60 frames and actively interfered with legitimate
362                // profiling, debugging, and CI environments. Anti-debugging measures have no
363                // place in a developer tool's render loop and will break expected tooling behavior.
364
365                state.frame_count += 1;
366
367                telemetry.berserker_rage = self.rage;
368                gpu.telemetry = telemetry;
369            }
370            WindowEvent::CursorEntered { .. } => {
371                log::info!("[Native] Cursor ENTERED window");
372                if let Some(vdom) = &state.vdom {
373                    vdom.dispatch_event(cvkg_core::Event::PointerEnter);
374                }
375                state.window.request_redraw();
376            }
377            WindowEvent::CursorLeft { .. } => {
378                log::info!("[Native] Cursor LEFT window");
379                if let Some(vdom) = &state.vdom {
380                    vdom.dispatch_event(cvkg_core::Event::PointerLeave);
381                }
382                state.window.request_redraw();
383            }
384            WindowEvent::CursorMoved { position, .. } => {
385                let scale = state.window.scale_factor();
386                let logical = position.to_logical::<f32>(scale);
387                log::info!("[Native] Cursor Moved: Physical={:?} Logical={:?} Scale={}", position, logical, scale);
388                state.cursor_pos = [logical.x, logical.y];
389                if let Some(vdom) = &state.vdom {
390                    vdom.dispatch_event(cvkg_core::Event::PointerMove {
391                        x: state.cursor_pos[0],
392                        y: state.cursor_pos[1],
393                    });
394                }
395                // FIX #12: Always request redraw on movement to ensure hover effects respond immediately.
396                state.window.request_redraw();
397            }
398            WindowEvent::MouseInput { state: mouse_state, button, .. } => {
399                log::info!("[Native] MOUSE INPUT: {:?} button={:?} pos={:?}", mouse_state, button, state.cursor_pos);
400                if let Some(vdom) = &state.vdom {
401                    let btn_id = match button {
402                        winit::event::MouseButton::Left => 0,
403                        winit::event::MouseButton::Right => 2,
404                        winit::event::MouseButton::Middle => 1,
405                        winit::event::MouseButton::Back => 3,
406                        winit::event::MouseButton::Forward => 4,
407                        winit::event::MouseButton::Other(id) => id as u32,
408                    };
409
410                    match mouse_state {
411                        winit::event::ElementState::Pressed => {
412                            log::info!("[Native] Dispatching PointerDown to VDOM");
413                            vdom.dispatch_event(cvkg_core::Event::PointerDown {
414                                x: state.cursor_pos[0],
415                                y: state.cursor_pos[1],
416                                button: btn_id,
417                            });
418                        }
419                        winit::event::ElementState::Released => {
420                            log::info!("[Native] Dispatching PointerUp to VDOM");
421                            vdom.dispatch_event(cvkg_core::Event::PointerUp {
422                                x: state.cursor_pos[0],
423                                y: state.cursor_pos[1],
424                                button: btn_id,
425                            });
426                        }
427                    }
428                    state.window.request_redraw();
429                } else {
430                    log::warn!("[Native] Mouse input received but state.vdom is None!");
431                }
432            }
433            WindowEvent::KeyboardInput { event, .. } => {
434                if let Some(vdom) = &state.vdom
435                    && let Some(cvkg_event) = convert_keyboard_event(event) {
436                        vdom.dispatch_event(cvkg_event);
437                        state.window.request_redraw();
438                }
439            }
440            WindowEvent::Ime(ime_event) => {
441                if let Some(vdom) = &state.vdom
442                    && let Some(cvkg_event) = convert_ime_event(ime_event) {
443                        vdom.dispatch_event(cvkg_event);
444                        state.window.request_redraw();
445                }
446            }
447            _ => {}
448        }
449    }
450
451    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: AppEvent) {
452        let AppEvent::AccessibilityAction(request) = event;
453        let node_id = cvkg_vdom::NodeId(request.target.0);
454
455        // FIX #11: Accessibility actions carry a target NodeId that identifies which
456        // window owns the node. We search all windows for the one containing that node
457        // rather than routing to the arbitrary first window (HashMap iteration order is
458        // non-deterministic and would silently misroute actions in multi-window layouts).
459        let target_state = self.windows.values_mut().find(|s| {
460            s.vdom.as_ref().map_or(false, |v| v.nodes.contains_key(&node_id))
461        });
462
463        if let Some(state) = target_state
464            && let Some(vdom) = &state.vdom
465            && let Some(node) = vdom.nodes.get(&node_id)
466            && request.action == accesskit::Action::Click
467        {
468            let event = cvkg_core::Event::PointerClick {
469                x: node.layout.x + node.layout.width / 2.0,
470                y: node.layout.y + node.layout.height / 2.0,
471                button: 0, // Assume left click for accessibility actions
472            };
473            vdom.dispatch_event(event);
474        }
475    }
476
477    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
478        // Apply Rage Decay: rage naturally settles to 0 over time.
479        self.rage = (self.rage - 0.02).max(0.0);
480
481        // Frame Throttling: 60FPS target (16.6ms)
482        let now = std::time::Instant::now();
483        let target_interval = std::time::Duration::from_millis(16);
484        
485        if now.duration_since(self.last_frame_time) >= target_interval {
486            if self.rage > 0.01 {
487                // Only log heartbeat when there is kinetic activity
488                log::debug!("[Native] Heartbeat ticking (rage: {})", self.rage);
489            }
490            self.last_frame_time = now;
491            for window_state in self.windows.values() {
492                window_state.window.request_redraw();
493            }
494            event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(now + target_interval));
495        } else {
496            event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(self.last_frame_time + target_interval));
497        }
498    }
499}
500
501impl cvkg_core::ElapsedTime for NativeRenderer {
502    fn delta_time(&self) -> f32 {
503        self.delta_time
504    }
505
506    fn elapsed_time(&self) -> f32 {
507        self.elapsed_time
508    }
509}
510
511impl cvkg_core::Renderer for NativeRenderer {
512
513    fn fill_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
514        self.gpu.lock().expect("GPU mutex poisoned: fill_rect").fill_rect(rect, color);
515    }
516    fn fill_rounded_rect(&mut self, rect: cvkg_core::Rect, radius: f32, color: [f32; 4]) {
517        self.gpu.lock().expect("GPU mutex poisoned: fill_rounded_rect").fill_rounded_rect(rect, radius, color);
518    }
519    fn fill_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
520        self.gpu.lock().expect("GPU mutex poisoned: fill_ellipse").fill_ellipse(rect, color);
521    }
522    fn stroke_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
523        self.gpu.lock().expect("GPU mutex poisoned: stroke_rect").stroke_rect(rect, color, stroke_width);
524    }
525    fn stroke_rounded_rect(
526        &mut self,
527        rect: cvkg_core::Rect,
528        radius: f32,
529        color: [f32; 4],
530        stroke_width: f32,
531    ) {
532        self.gpu.lock().expect("GPU mutex poisoned: stroke_rounded_rect").stroke_rounded_rect(rect, radius, color, stroke_width);
533    }
534    fn stroke_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
535        self.gpu.lock().expect("GPU mutex poisoned: stroke_ellipse").stroke_ellipse(rect, color, stroke_width);
536    }
537    fn draw_line(
538        &mut self,
539        x1: f32,
540        y1: f32,
541        x2: f32,
542        y2: f32,
543        color: [f32; 4],
544        stroke_width: f32,
545    ) {
546        self.gpu.lock().expect("GPU mutex poisoned: draw_line").draw_line(x1, y1, x2, y2, color, stroke_width);
547    }
548    fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
549        self.gpu.lock().expect("GPU mutex poisoned: draw_text").draw_text(text, x, y, size, color);
550    }
551    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
552        self.gpu.lock().expect("GPU mutex poisoned: measure_text").measure_text(text, size)
553    }
554    fn draw_linear_gradient(&mut self, rect: cvkg_core::Rect, start_color: [f32; 4], end_color: [f32; 4], angle: f32) {
555        self.gpu.lock().expect("GPU mutex poisoned: draw_linear_gradient").draw_linear_gradient(rect, start_color, end_color, angle);
556    }
557    fn draw_radial_gradient(&mut self, rect: cvkg_core::Rect, inner_color: [f32; 4], outer_color: [f32; 4]) {
558        self.gpu.lock().expect("GPU mutex poisoned: draw_radial_gradient").draw_radial_gradient(rect, inner_color, outer_color);
559    }
560    fn draw_texture(&mut self, texture_id: u32, rect: cvkg_core::Rect) {
561        self.gpu.lock().expect("GPU mutex poisoned: draw_texture").draw_texture(texture_id, rect);
562    }
563    fn draw_image(&mut self, image_name: &str, rect: cvkg_core::Rect) {
564        self.gpu.lock().expect("GPU mutex poisoned: draw_image").draw_image(image_name, rect);
565    }
566    fn load_image(&mut self, name: &str, data: &[u8]) {
567        self.gpu.lock().expect("GPU mutex poisoned: load_image").load_image(name, data);
568    }
569    fn push_clip_rect(&mut self, rect: cvkg_core::Rect) {
570        self.gpu.lock().expect("GPU mutex poisoned: push_clip_rect").push_clip_rect(rect);
571    }
572    fn pop_clip_rect(&mut self) {
573        self.gpu.lock().expect("GPU mutex poisoned: pop_clip_rect").pop_clip_rect();
574    }
575    fn push_opacity(&mut self, opacity: f32) {
576        self.gpu.lock().expect("GPU mutex poisoned: push_opacity").push_opacity(opacity);
577    }
578    fn draw_3d_cube(&mut self, rect: cvkg_core::Rect, color: [f32; 4], rotation: [f32; 3]) {
579        self.gpu.lock().expect("GPU mutex poisoned: draw_3d_cube").draw_3d_cube(rect, color, rotation);
580    }
581    fn pop_opacity(&mut self) {
582        self.gpu.lock().expect("GPU mutex poisoned: pop_opacity").pop_opacity();
583    }
584    fn bifrost(&mut self, rect: cvkg_core::Rect, blur: f32, saturation: f32, opacity: f32) {
585        self.gpu.lock().expect("GPU mutex poisoned: bifrost").bifrost(rect, blur, saturation, opacity);
586    }
587    fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
588        self.gpu.lock().expect("GPU mutex poisoned: push_mjolnir_slice").push_mjolnir_slice(angle, offset);
589    }
590    fn pop_mjolnir_slice(&mut self) {
591        self.gpu.lock().expect("GPU mutex poisoned: pop_mjolnir_slice").pop_mjolnir_slice();
592    }
593    fn mjolnir_shatter(&mut self, rect: cvkg_core::Rect, pieces: u32, force: f32, color: [f32; 4]) {
594        self.gpu.lock().expect("GPU mutex poisoned: mjolnir_shatter").mjolnir_shatter(rect, pieces, force, color);
595    }
596    fn mjolnir_fluid_shatter(&mut self, rect: cvkg_core::Rect, pieces: u32, force: f32, color: [f32; 4]) {
597        self.gpu.lock().expect("GPU mutex poisoned: mjolnir_fluid_shatter").mjolnir_fluid_shatter(rect, pieces, force, color);
598    }
599    fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
600        self.gpu.lock().expect("GPU mutex poisoned: draw_mjolnir_bolt").draw_mjolnir_bolt(from, to, color);
601    }
602    fn gungnir(&mut self, rect: cvkg_core::Rect, color: [f32; 4], radius: f32, intensity: f32) {
603        self.gpu.lock().expect("GPU mutex poisoned: gungnir").gungnir(rect, color, radius, intensity);
604    }
605    fn mani_glow(&mut self, rect: cvkg_core::Rect, color: [f32; 4], radius: f32) {
606        self.gpu.lock().expect("GPU mutex poisoned: mani_glow").mani_glow(rect, color, radius);
607    }
608    fn register_handler(&mut self, event_type: &str, handler: std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>) {
609        self.gpu.lock().expect("GPU mutex poisoned: register_handler").register_handler(event_type, handler);
610    }
611    fn push_vnode(&mut self, rect: cvkg_core::Rect, name: &'static str) {
612        self.gpu.lock().expect("GPU mutex poisoned: push_vnode").push_vnode(rect, name);
613    }
614    fn pop_vnode(&mut self) {
615        self.gpu.lock().expect("GPU mutex poisoned: pop_vnode").pop_vnode();
616    }
617    // FIX #1: Removed duplicate definitions of set_z_index and get_z_index.
618    // They appeared twice in this impl block (after pop_vnode and after register_shared_element),
619    // which is a hard compiler error. Exactly one definition of each is kept here.
620    fn set_z_index(&mut self, z: f32) {
621        self.gpu.lock().expect("GPU mutex poisoned: set_z_index").set_z_index(z);
622    }
623    fn get_z_index(&self) -> f32 {
624        self.gpu.lock().expect("GPU mutex poisoned: get_z_index").get_z_index()
625    }
626    fn register_shared_element(&mut self, id: &str, rect: cvkg_core::Rect) {
627        self.gpu.lock().expect("GPU mutex poisoned: register_shared_element").register_shared_element(id, rect);
628    }
629    fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
630        self.gpu.lock().expect("GPU mutex poisoned: load_svg").load_svg(name, svg_data);
631    }
632    fn draw_svg(&mut self, name: &str, rect: cvkg_core::Rect) {
633        self.gpu.lock().expect("GPU mutex poisoned: draw_svg").draw_svg(name, rect, None, 0);
634    }
635    fn get_telemetry(&self) -> cvkg_core::TelemetryData {
636        self.gpu.lock().expect("GPU mutex poisoned: get_telemetry").telemetry.clone()
637    }
638    fn prewarm_vram(&mut self, assets: Vec<(String, Vec<u8>)>) {
639        self.gpu.lock().expect("GPU mutex poisoned: prewarm_vram").prewarm_vram(assets);
640    }
641    fn push_transform(&mut self, translation: [f32; 2], scale: [f32; 2], rotation: f32) {
642        self.gpu.lock().expect("GPU mutex poisoned: push_transform").push_transform(translation, scale, rotation);
643    }
644    fn pop_transform(&mut self) {
645        self.gpu.lock().expect("GPU mutex poisoned: pop_transform").pop_transform();
646    }
647
648    fn set_berserker_mode(&mut self, state: cvkg_core::BerserkerMode) {
649        self.berserker_mode = state;
650
651        // Berserker Determinism: Apply OS-level scheduler priority hints for GodMode.
652        // SAFETY: setpriority is a POSIX syscall. We pass PRIO_PROCESS with pid=0 (self).
653        // Failure is silently ignored via let _ because insufficient permissions are expected
654        // in unprivileged environments and must not crash the render loop.
655        if state == cvkg_core::BerserkerMode::GodMode {
656            log::info!("ENTERING GOD MODE: Activating Berserker Determinism (High Priority)");
657            #[cfg(target_os = "linux")]
658            unsafe {
659                let _ = libc::setpriority(libc::PRIO_PROCESS, 0, -10);
660            }
661        } else {
662            #[cfg(target_os = "linux")]
663            unsafe {
664                let _ = libc::setpriority(libc::PRIO_PROCESS, 0, 0);
665            }
666        }
667
668        self.gpu.lock().expect("GPU mutex poisoned: set_berserker_mode").set_berserker_mode(state);
669    }
670
671    fn set_rage(&mut self, rage: f32) {
672        self.rage = rage;
673        self.gpu.lock().expect("GPU mutex poisoned: set_rage").set_rage(rage);
674    }
675
676    fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
677        self.gpu.lock().expect("GPU mutex poisoned: memoize").memoize(id, data_hash, render_fn);
678    }
679    fn request_redraw(&mut self) {
680        self.window.request_redraw();
681    }
682
683    /// Captures the current frame as a PNG-encoded byte buffer via GPU readback.
684    /// Captures the current frame as a PNG-encoded byte buffer via GPU readback.
685    ///
686    /// FIX #4: capture_frame() returns a Future that borrows the SurtrRenderer, so the
687    /// MutexGuard must remain alive until block_on completes — the guard cannot be dropped
688    /// before the future is driven to completion. The lock is held for the duration of the
689    /// GPU readback. This is acceptable because capture_png is an infrequent, explicit
690    /// user-triggered operation (not called on the hot render path), so blocking other
691    /// render calls for the readback duration is not a practical concern.
692    fn capture_png(&mut self) -> Vec<u8> {
693        log::info!("CAPTURING_FRAME: Initiating GPU readback...");
694        // INVARIANT: The MutexGuard `gpu` must outlive the future returned by capture_frame()
695        // because the future borrows from the SurtrRenderer. We therefore lock, block_on the
696        // future (driving it to completion), and only then allow the guard to drop.
697        let gpu = self.gpu.lock().expect("GPU mutex poisoned: capture_png");
698        pollster::block_on(gpu.capture_frame()).unwrap_or_else(|e| {
699            log::error!("GPU frame capture failed: {}", e);
700            Vec::new() // Return empty buffer on failure — do not panic the render loop
701        })
702    }
703
704    fn print(&mut self) {
705        log::info!("PRINT_BRIDGE: Spooling mission status to native printer...");
706        // In a production environment, this would interface with CUPS/GDI/AirPrint.
707        // For the Ulfhednar prototype, we simulate the handshake.
708        println!("[BRIDGE] PRINTER_READY // SPOOLING_DATA...");
709    }
710}
711
712// ── Event Conversion Helpers ───────────────────────────────────────────
713
714
715fn convert_keyboard_event(event: winit::event::KeyEvent) -> Option<cvkg_core::Event> {
716    if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
717        let key_str = format!("{:?}", code);
718        if event.state == winit::event::ElementState::Pressed {
719            Some(cvkg_core::Event::KeyDown { key: key_str })
720        } else {
721            Some(cvkg_core::Event::KeyUp { key: key_str })
722        }
723    } else {
724        None
725    }
726}
727
728fn convert_ime_event(event: winit::event::Ime) -> Option<cvkg_core::Event> {
729    if let winit::event::Ime::Commit(string) = event {
730        Some(cvkg_core::Event::Ime(string))
731    } else {
732        None
733    }
734}
735
736// Platform-specific implementations for macOS, Windows, and Linux are handled by winit and AccessKit.
737
738struct ShieldWall {
739    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
740}
741
742impl accesskit::ActionHandler for ShieldWall {
743    fn do_action(&mut self, request: accesskit::ActionRequest) {
744        let _ = self
745            .proxy
746            .send_event(AppEvent::AccessibilityAction(request));
747    }
748}
749
750impl accesskit::ActivationHandler for ShieldWall {
751    fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
752        let mut root = accesskit::Node::new(accesskit::Role::Window);
753        root.set_label("CVKG Application");
754
755        let root_id = accesskit::NodeId(1);
756        Some(accesskit::TreeUpdate {
757            nodes: vec![(root_id, root)],
758            tree: Some(accesskit::Tree::new(root_id)),
759            focus: root_id,
760        })
761    }
762}
763
764impl accesskit::DeactivationHandler for ShieldWall {
765    fn deactivate_accessibility(&mut self) {}
766}
767
768type AssetCacheMap = std::collections::HashMap<String, cvkg_core::AssetState<std::sync::Arc<Vec<u8>>>>;
769
770/// A concrete AssetManager for native desktop targets that loads from the local filesystem.
771///
772/// The cache is read on every render frame (lock-free via `ArcSwap::load()`) but written
773/// at most once per URL after disk I/O completes. `rcu()` atomically inserts the result
774/// without blocking concurrent render-loop readers.
775pub struct NativeAssetManager {
776    cache: std::sync::Arc<arc_swap::ArcSwap<AssetCacheMap>>,
777}
778
779impl Default for NativeAssetManager {
780    fn default() -> Self {
781        Self::new()
782    }
783}
784
785impl NativeAssetManager {
786    /// Create a new, empty NativeAssetManager.
787    pub fn new() -> Self {
788        Self {
789            cache: std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
790                std::collections::HashMap::new(),
791            )),
792        }
793    }
794}
795
796impl cvkg_core::AssetManager for NativeAssetManager {
797    /// Return the cached asset state for `url`.
798    ///
799    /// Fast path: lock-free snapshot read via `ArcSwap::load()`.
800    /// Slow path (cache miss): atomically insert a Loading sentinel via `rcu()`,
801    /// then spawn a background thread for I/O. The `rcu()` closure may execute
802    /// more than once under contention, so `already_tracked` is determined by
803    /// whether the closure actually inserted the Loading entry (detected by checking
804    /// the returned map). This prevents duplicate I/O threads for the same URL.
805    ///
806    /// FIX #5: The previous implementation set `already_tracked` inside the `rcu`
807    /// closure body, which is incorrect because `rcu` retries the closure on
808    /// contention — the bool would reflect only the last execution. The fix uses
809    /// the fast-path check result plus the atomic `rcu` insertion to determine
810    /// whether a thread must be spawned, making the logic correct under concurrency.
811    fn load_image(&self, url: &str) -> cvkg_core::AssetState<std::sync::Arc<Vec<u8>>> {
812        // Fast path: lock-free read from the current cache snapshot.
813        if let Some(state) = self.cache.load().get(url) {
814            return state.clone();
815        }
816
817        let cache = self.cache.clone();
818        let key = url.to_string();
819
820        // Slow path: atomically insert Loading if the key is absent.
821        // `rcu` returns the final committed map; we inspect it to determine
822        // whether *this* call was the one that inserted Loading (and thus
823        // should spawn the I/O thread) versus a concurrent call that beat us.
824        let mut we_inserted = false;
825        self.cache.rcu(|map| {
826            if map.contains_key(&key) {
827                // Another caller already claimed this URL — do not insert.
828                (**map).clone()
829            } else {
830                we_inserted = true;
831                let mut m = (**map).clone();
832                m.insert(key.clone(), cvkg_core::AssetState::Loading);
833                m
834            }
835        });
836
837        // Only the caller that performed the insertion spawns the I/O thread,
838        // preventing duplicate concurrent reads for the same asset URL.
839        if we_inserted {
840            let cache_inner = cache.clone();
841            let key_inner = key.clone();
842
843            std::thread::spawn(move || {
844                log::debug!("[Native] Asynchronously loading asset: {}", key_inner);
845                let result = match std::fs::read(&key_inner) {
846                    Ok(data) => cvkg_core::AssetState::Ready(std::sync::Arc::new(data)),
847                    Err(e) => cvkg_core::AssetState::Error(e.to_string()),
848                };
849
850                cache_inner.rcu(move |map| {
851                    let mut m = (**map).clone();
852                    m.insert(key_inner.clone(), result.clone());
853                    m
854                });
855            });
856        }
857
858        cvkg_core::AssetState::Loading
859    }
860
861    /// Trigger a background load of `url` without waiting for the result.
862    ///
863    /// FIX #6: The previous implementation had a bare fast-path check followed
864    /// by an unconditional thread spawn, allowing two concurrent calls for the
865    /// same URL to both spawn I/O threads. Now uses the same rcu-based insertion
866    /// guard as `load_image` to ensure exactly one thread is spawned per URL.
867    fn preload_image(&self, url: &str) {
868        // Fast path: if already in cache (any state), no work to do.
869        if self.cache.load().contains_key(url) {
870            return;
871        }
872
873        let cache = self.cache.clone();
874        let key = url.to_string();
875
876        let mut we_inserted = false;
877        self.cache.rcu(|map| {
878            if map.contains_key(&key) {
879                (**map).clone()
880            } else {
881                we_inserted = true;
882                let mut m = (**map).clone();
883                m.insert(key.clone(), cvkg_core::AssetState::Loading);
884                m
885            }
886        });
887
888        if we_inserted {
889            std::thread::spawn(move || {
890                log::debug!("[Native] Preloading asset: {}", key);
891                let result = match std::fs::read(&key) {
892                    Ok(data) => cvkg_core::AssetState::Ready(std::sync::Arc::new(data)),
893                    Err(e) => cvkg_core::AssetState::Error(e.to_string()),
894                };
895
896                cache.rcu(move |map| {
897                    let mut m = (**map).clone();
898                    m.insert(key.clone(), result.clone());
899                    m
900                });
901            });
902        }
903    }
904}
905
906#[cfg(test)]
907mod tests {
908    use super::*;
909    use cvkg_core::AssetManager;
910    use std::io::Write;
911
912    /// FIX #12: Replaced hardcoded relative path "test_asset.png" with a temp-dir path
913    /// constructed from a unique per-test name. The previous path was written to the
914    /// process working directory, which varies by invocation and causes collisions when
915    /// tests run in parallel or when a prior run panics before cleanup.
916    #[test]
917    fn test_native_asset_manager_loading() {
918        let manager = NativeAssetManager::new();
919        let temp_path = std::env::temp_dir().join("cvkg_test_asset_loading.png");
920        let temp_file_path = temp_path.to_str().expect("temp path must be valid UTF-8");
921        let test_data = b"fake-image-data";
922
923        // Create a temporary file in the OS temp directory
924        let mut file = std::fs::File::create(temp_file_path).unwrap();
925        file.write_all(test_data).unwrap();
926        drop(file);
927
928        // First call returns Loading and spawns the background I/O thread
929        let mut state = manager.load_image(temp_file_path);
930
931        // Poll until Ready or timeout
932        let start = std::time::Instant::now();
933        while matches!(state, cvkg_core::AssetState::Loading) && start.elapsed().as_secs() < 5 {
934            std::thread::sleep(std::time::Duration::from_millis(10));
935            state = manager.load_image(temp_file_path);
936        }
937
938        if let cvkg_core::AssetState::Ready(data) = state {
939            assert_eq!(&*data, test_data);
940        } else {
941            let _ = std::fs::remove_file(temp_file_path);
942            panic!("Expected Ready state, got {:?}", state);
943        }
944
945        // Verify fast path returns Ready immediately from cache
946        let state2 = manager.load_image(temp_file_path);
947        if let cvkg_core::AssetState::Ready(data) = state2 {
948            assert_eq!(&*data, test_data);
949        } else {
950            let _ = std::fs::remove_file(temp_file_path);
951            panic!("Expected Ready state (cached), got {:?}", state2);
952        }
953
954        let _ = std::fs::remove_file(temp_file_path);
955    }
956
957    #[test]
958    fn test_native_asset_manager_error() {
959        let manager = NativeAssetManager::new();
960        let path = "non_existent_file_cvkg_test.png";
961        let mut state = manager.load_image(path);
962
963        let start = std::time::Instant::now();
964        while matches!(state, cvkg_core::AssetState::Loading) && start.elapsed().as_secs() < 5 {
965            std::thread::sleep(std::time::Duration::from_millis(10));
966            state = manager.load_image(path);
967        }
968
969        if let cvkg_core::AssetState::Error(_) = state {
970            // Expected — non-existent file must produce an Error state
971        } else {
972            panic!("Expected Error state, got {:?}", state);
973        }
974    }
975
976    #[test]
977    fn test_event_conversion() {
978        // Mouse press event
979        let event = convert_mouse_event(winit::event::ElementState::Pressed, [10.0, 20.0], 0);
980        if let cvkg_core::Event::PointerDown { x, y, button } = event {
981            assert_eq!(x, 10.0);
982            assert_eq!(y, 20.0);
983            assert_eq!(button, 0);
984        } else {
985            panic!("Expected PointerDown");
986        }
987
988        // IME commit event
989        let event = convert_ime_event(winit::event::Ime::Commit("hello".to_string()));
990        if let Some(cvkg_core::Event::Ime(s)) = event {
991            assert_eq!(s, "hello");
992        } else {
993            panic!("Expected Ime event");
994        }
995    }
996}
997
998/// load_icon — Searches known asset directories for 'icon.png'.
999/// Returns a winit Icon if found and decodable, None otherwise.
1000/// All failures are logged at warn level — missing icons are non-fatal.
1001fn load_icon() -> Option<winit::window::Icon> {
1002    // FIX #13: Replaced unwrap_or_default() with unwrap_or_else that logs the failure.
1003    // unwrap_or_default() produced an empty PathBuf silently, making all subsequent
1004    // icon path lookups silently fail with no diagnostic output.
1005    let base = std::env::current_dir().unwrap_or_else(|e| {
1006        log::warn!("[Native] Failed to get current directory for icon search: {}", e);
1007        std::path::PathBuf::new()
1008    });
1009
1010    let mut candidates = vec![
1011        base.join("icon.png"),
1012        base.join("crates/ulfhednar/icons/icon.png"),
1013        base.join("ulfhednar/icons/icon.png"),
1014        base.join("crates/ulfhednar/assets/icon.png"),
1015        base.join("ulfhednar/assets/icon.png"),
1016        base.join("assets/icon.png"),
1017    ];
1018
1019    // Also search relative to the executable directory
1020    if let Ok(exe_path) = std::env::current_exe()
1021        && let Some(exe_dir) = exe_path.parent() {
1022            candidates.push(exe_dir.join("icons/icon.png"));
1023            candidates.push(exe_dir.join("assets/icon.png"));
1024            candidates.push(exe_dir.join("icon.png"));
1025            if let Some(parent) = exe_dir.parent() {
1026                candidates.push(parent.join("icons/icon.png"));
1027                candidates.push(parent.join("assets/icon.png"));
1028                candidates.push(parent.join("icon.png"));
1029            }
1030        }
1031
1032    for path in candidates {
1033        if !path.exists() {
1034            log::debug!("[Native] Icon candidate not found: {:?}", path);
1035            continue;
1036        }
1037
1038        match image::open(&path) {
1039            Ok(img) => {
1040                let rgba = img.to_rgba8();
1041                let (width, height) = rgba.dimensions();
1042                match winit::window::Icon::from_rgba(rgba.into_raw(), width, height) {
1043                    Ok(icon) => {
1044                        log::info!("[Native] Successfully loaded app icon from: {:?}", path);
1045                        return Some(icon);
1046                    }
1047                    Err(e) => {
1048                        log::warn!("[Native] Icon format error at {:?}: {}", path, e);
1049                    }
1050                }
1051            }
1052            Err(e) => {
1053                log::warn!("[Native] Failed to open icon image at {:?}: {}", path, e);
1054            }
1055        }
1056    }
1057
1058    log::warn!("[Native] Failed to find icon.png in any search path (CWD: {:?})", base);
1059    None
1060}