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