1#![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
37use cvkg_core::{FrameRenderer, Renderer};
43use image;
44#[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
54pub 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#[derive(Debug)]
67enum AppEvent {
68 AccessibilityAction(accesskit::ActionRequest),
69}
70
71impl NativeRenderer {
72 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 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 last_redraw_start: std::time::Instant,
122 frame_history: std::collections::VecDeque<f32>,
124 frame_count: u64,
126 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 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 } 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 } 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 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 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 let redraw_start = std::time::Instant::now();
293 let last_redraw_start = state.last_redraw_start;
294 state.last_redraw_start = redraw_start;
297
298 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 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 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 drop(gpu);
354 self.view.render(&mut renderer, rect);
355 let draw_end = std::time::Instant::now();
356
357 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 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 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 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 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 telemetry.hardware_stall_detected = telemetry.frame_jitter_ms > 20.0;
417
418 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 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 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, };
568 vdom.dispatch_event(event);
569 }
570 }
571
572 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
573 self.rage = (self.rage - 0.02).max(0.0);
575
576 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 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 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 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 fn capture_png(&mut self) -> Vec<u8> {
939 log::info!("CAPTURING_FRAME: Initiating GPU readback...");
940 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() })
948 }
949
950 fn print(&mut self) {
951 log::info!("PRINT_BRIDGE: Spooling mission status to native printer...");
952 println!("[BRIDGE] PRINTER_READY // SPOOLING_DATA...");
955 }
956}
957
958fn 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
1001struct 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
1036pub 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 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 fn load_image(&self, url: &str) -> cvkg_core::AssetState<std::sync::Arc<Vec<u8>>> {
1078 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 let mut we_inserted = false;
1091 self.cache.rcu(|map| {
1092 if map.contains_key(&key) {
1093 (**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 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 fn preload_image(&self, url: &str) {
1134 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 #[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 let mut file = std::fs::File::create(temp_file_path).unwrap();
1191 file.write_all(test_data).unwrap();
1192 drop(file);
1193
1194 let mut state = manager.load_image(temp_file_path);
1196
1197 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 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 } else {
1238 panic!("Expected Error state, got {:?}", state);
1239 }
1240 }
1241
1242 #[test]
1243 fn test_event_conversion() {
1244 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 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
1264fn load_icon() -> Option<winit::window::Icon> {
1268 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 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}