Skip to main content

cvkg_render_native/
lib.rs

1//! # CVKG Agentic Development Guidelines (v1.2)
2//!
3//! All AI agents contributing to this crate MUST follow ALL seven 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–7) ────────────────────────────────────────
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//!
21//! Sources:
22//   Karpathy: https://github.com/multica-ai/andrej-karpathy-skills
23//   CVKG Extended: Section 2 of the CVKG Design Specification
24
25//! Platform-native widget delegation using winit and AccessKit
26//!
27//! This crate provides platform-specific rendering backends for native desktop targets
28//  using winit for window/event handling and AccessKit for accessibility tree integration.
29
30use std::sync::Arc;
31use winit::{
32    application::ApplicationHandler,
33    event::WindowEvent,
34    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
35    window::{Window, WindowId},
36};
37
38
39/// Native renderer backend implementing the Renderer trait.
40/// It wraps a shared SurtrRenderer for high-performance GPU drawing.
41pub struct NativeRenderer {
42    gpu: Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>,
43    delta_time: f32,
44}
45
46/// Custom events for the native application event loop
47#[derive(Debug)]
48enum AppEvent {
49    AccessibilityAction(accesskit::ActionRequest),
50}
51
52impl NativeRenderer {
53    /// Create a new NativeRenderer (internal use by App)
54    fn new(_window: Arc<Window>, gpu: Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>, delta_time: f32) -> Self {
55        Self { gpu, delta_time }
56    }
57
58    /// Get real-time performance telemetry.
59    fn get_telemetry(&self) -> cvkg_core::TelemetryData {
60        self.gpu.lock().unwrap().get_telemetry()
61    }
62
63    /// Start the CVKG native application with the given view.
64    /// This is the main entry point for desktop applications.
65    pub fn run<V: cvkg_core::View + 'static>(view: V) {
66        let event_loop = EventLoop::<AppEvent>::with_user_event()
67            .build()
68            .expect("Failed to create event loop");
69        event_loop.set_control_flow(ControlFlow::Poll);
70
71        let mut app = App {
72            view,
73            windows: std::collections::HashMap::new(),
74            gpu: None,
75            asset_manager: std::sync::Arc::new(NativeAssetManager::new()),
76            proxy: event_loop.create_proxy(),
77        };
78
79        event_loop.run_app(&mut app).expect("Event loop error");
80    }
81}
82
83struct WindowState {
84    window: Arc<Window>,
85    accesskit_adapter: Option<accesskit_winit::Adapter>,
86    vdom: Option<cvkg_vdom::VDom>,
87    cursor_pos: [f32; 2],
88    /// The instant the last redraw finished, used for measuring inter-frame timing.
89    last_redraw_start: std::time::Instant,
90}
91
92struct App<V: cvkg_core::View> {
93    view: V,
94    windows: std::collections::HashMap<WindowId, WindowState>,
95    gpu: Option<Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>>,
96    asset_manager: std::sync::Arc<NativeAssetManager>,
97    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
98}
99
100impl<V: cvkg_core::View + 'static> ApplicationHandler<AppEvent> for App<V> {
101    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
102        if self.gpu.is_none() {
103            let window_attrs = Window::default_attributes()
104                .with_title("CVKG Forge")
105                .with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0));
106
107            let window = Arc::new(
108                event_loop
109                    .create_window(window_attrs)
110                    .expect("Failed to create window"),
111            );
112            window.set_ime_allowed(true);
113
114            let adapter = accesskit_winit::Adapter::with_direct_handlers(
115                event_loop,
116                &window,
117                ShieldWall { proxy: self.proxy.clone() },
118                ShieldWall { proxy: self.proxy.clone() },
119                ShieldWall { proxy: self.proxy.clone() },
120            );
121
122            let rt = tokio::runtime::Runtime::new().unwrap();
123            let gpu = rt.block_on(cvkg_render_gpu::SurtrRenderer::forge(window.clone()));
124            let gpu = Arc::new(std::sync::Mutex::new(gpu));
125            self.gpu = Some(gpu);
126
127            self.windows.insert(window.id(), WindowState {
128                window,
129                accesskit_adapter: Some(adapter),
130                vdom: Some(cvkg_vdom::VDom::new()),
131                cursor_pos: [0.0, 0.0],
132                last_redraw_start: std::time::Instant::now(),
133            });
134
135            cvkg_core::env::insert::<cvkg_core::AssetKey>(self.asset_manager.clone());
136        }
137    }
138
139    fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
140        let gpu_arc = if let Some(g) = &self.gpu { g.clone() } else { return };
141        let state = if let Some(s) = self.windows.get_mut(&id) { s } else { return };
142
143        match event {
144            WindowEvent::CloseRequested => {
145                self.windows.remove(&id);
146                if self.windows.is_empty() {
147                    event_loop.exit();
148                }
149            }
150            WindowEvent::Resized(physical_size) => {
151                gpu_arc.lock().unwrap().resize(
152                    id,
153                    physical_size.width,
154                    physical_size.height,
155                    state.window.scale_factor() as f32,
156                );
157                state.window.request_redraw();
158            }
159            WindowEvent::RedrawRequested => {
160                let size = state.window.inner_size();
161                let scale = state.window.scale_factor();
162                let logical_size = size.to_logical::<f32>(scale);
163
164                let rect = cvkg_core::Rect {
165                    x: 0.0,
166                    y: 0.0,
167                    width: logical_size.width,
168                    height: logical_size.height,
169                };
170
171                // Start timing for this redraw
172                let redraw_start = std::time::Instant::now();
173                
174                // Build new vdom and diff (layout pass)
175                let layout_start = std::time::Instant::now();
176                let new_vdom = cvkg_vdom::VDom::build(&self.view, rect);
177                let layout_end = std::time::Instant::now();
178
179                // Apply patches
180                let state_flush_start = std::time::Instant::now();
181                if let Some(prev_vdom) = &mut state.vdom {
182                    let patches = prev_vdom.diff(&new_vdom);
183                    if let Some(adapter) = &mut state.accesskit_adapter {
184                        let mut nodes = Vec::new();
185                        for patch in &patches {
186                            match patch {
187                                cvkg_vdom::VDomPatch::Create(node)
188                                | cvkg_vdom::VDomPatch::Replace { node, .. } => {
189                                    nodes.push((accesskit::NodeId(node.id.0 as u64), node.to_accesskit_node()));
190                                }
191                                cvkg_vdom::VDomPatch::Update { id, .. } => {
192                                    if let Some(node) = new_vdom.nodes.get(id) {
193                                        nodes.push((accesskit::NodeId(node.id.0 as u64), node.to_accesskit_node()));
194                                    }
195                                }
196                                _ => {}
197                            }
198                        }
199                        if !nodes.is_empty() {
200                            adapter.update_if_active(|| accesskit::TreeUpdate {
201                                nodes,
202                                tree: None,
203                                focus: accesskit::NodeId(1),
204                            });
205                        }
206                    }
207                    prev_vdom.apply_patches(patches);
208                } else {
209                    state.vdom = Some(new_vdom);
210                }
211                let state_flush_end = std::time::Instant::now();
212
213                // GPU rendering
214                let draw_start = std::time::Instant::now();
215                let delta_time = redraw_start.duration_since(state.last_redraw_start).as_secs_f32();
216                let mut gpu = gpu_arc.lock().unwrap();
217                let encoder = gpu.begin_frame(id);
218                let mut renderer = NativeRenderer::new(state.window.clone(), gpu_arc.clone(), delta_time);
219                self.view.render(&mut renderer, rect);
220                let draw_end = std::time::Instant::now();
221
222                // Submission
223                let gpu_submit_start = std::time::Instant::now();
224                gpu.end_frame(encoder);
225                let gpu_submit_end = std::time::Instant::now();
226
227                // Update telemetry
228                let mut telemetry = gpu.telemetry.clone();
229                // input_time_ms uses the previous frame's completion to this frame's start as a proxy
230                telemetry.input_time_ms = redraw_start.duration_since(state.last_redraw_start).as_secs_f32() * 1000.0;
231                telemetry.layout_time_ms = layout_end.duration_since(layout_start).as_secs_f32() * 1000.0;
232                telemetry.state_flush_time_ms = state_flush_end.duration_since(state_flush_start).as_secs_f32() * 1000.0;
233                telemetry.draw_time_ms = draw_end.duration_since(draw_start).as_secs_f32() * 1000.0;
234                telemetry.gpu_submit_time_ms = gpu_submit_end.duration_since(gpu_submit_start).as_secs_f32() * 1000.0;
235                
236                // Total frame time
237                telemetry.frame_time_ms = gpu_submit_end.duration_since(redraw_start).as_secs_f32() * 1000.0;
238                
239                gpu.telemetry = telemetry;
240                state.last_redraw_start = gpu_submit_end;
241            }
242            WindowEvent::CursorMoved { position, .. } => {
243                let scale = state.window.scale_factor();
244                let logical = position.to_logical::<f32>(scale);
245                state.cursor_pos = [logical.x, logical.y];
246                if let Some(vdom) = &state.vdom {
247                    vdom.dispatch_event(cvkg_core::Event::PointerMove {
248                        x: state.cursor_pos[0],
249                        y: state.cursor_pos[1],
250                    });
251                }
252            }
253            WindowEvent::MouseInput { state: mouse_state, .. } => {
254                if let Some(vdom) = &state.vdom {
255                    let event = match mouse_state {
256                        winit::event::ElementState::Pressed => {
257                            cvkg_core::Event::PointerDown {
258                                x: state.cursor_pos[0],
259                                y: state.cursor_pos[1],
260                            }
261                        }
262                        winit::event::ElementState::Released => cvkg_core::Event::PointerUp {
263                            x: state.cursor_pos[0],
264                            y: state.cursor_pos[1],
265                        },
266                    };
267                    vdom.dispatch_event(event);
268                }
269            }
270            WindowEvent::KeyboardInput { event, .. } => {
271                if let Some(vdom) = &state.vdom {
272                    if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
273                        let key_str = format!("{:?}", code);
274                        let cvkg_event = if event.state == winit::event::ElementState::Pressed {
275                            cvkg_core::Event::KeyDown { key: key_str }
276                        } else {
277                            cvkg_core::Event::KeyUp { key: key_str }
278                        };
279                        vdom.dispatch_event(cvkg_event);
280                    }
281                }
282            }
283            WindowEvent::Ime(ime_event) => {
284                if let Some(vdom) = &state.vdom {
285                    match ime_event {
286                        winit::event::Ime::Commit(string) => {
287                            vdom.dispatch_event(cvkg_core::Event::Ime(string));
288                        }
289                        _ => {}
290                    }
291                }
292            }
293            _ => {}
294        }
295    }
296
297    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: AppEvent) {
298        match event {
299            AppEvent::AccessibilityAction(request) => {
300                let node_id = cvkg_vdom::NodeId(request.target.0 as u64);
301                // For accessibility, we'll route to the first window for now
302                if let Some(state) = self.windows.values_mut().next() {
303                    if let Some(vdom) = &state.vdom {
304                        if let Some(node) = vdom.nodes.get(&node_id) {
305                            match request.action {
306                                accesskit::Action::Click => {
307                                    let event = cvkg_core::Event::PointerClick {
308                                        x: node.layout.x + node.layout.width / 2.0,
309                                        y: node.layout.y + node.layout.height / 2.0,
310                                    };
311                                    vdom.dispatch_event(event);
312                                }
313                                _ => ()
314                            }
315                        }
316                    }
317                }
318            }
319        }
320    }
321
322    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
323        for state in self.windows.values() {
324            state.window.request_redraw();
325        }
326    }
327}
328
329impl cvkg_core::Renderer for NativeRenderer {
330    fn delta_time(&self) -> f32 {
331        self.delta_time
332    }
333
334    fn fill_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
335        self.gpu.lock().unwrap().fill_rect(rect, color);
336    }
337    fn fill_rounded_rect(&mut self, rect: cvkg_core::Rect, radius: f32, color: [f32; 4]) {
338        self.gpu.lock().unwrap().fill_rounded_rect(rect, radius, color);
339    }
340    fn fill_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
341        self.gpu.lock().unwrap().fill_ellipse(rect, color);
342    }
343    fn stroke_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
344        self.gpu.lock().unwrap().stroke_rect(rect, color, stroke_width);
345    }
346    fn stroke_rounded_rect(
347        &mut self,
348        rect: cvkg_core::Rect,
349        radius: f32,
350        color: [f32; 4],
351        stroke_width: f32,
352    ) {
353        self.gpu.lock().unwrap().stroke_rounded_rect(rect, radius, color, stroke_width);
354    }
355    fn stroke_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
356        self.gpu.lock().unwrap().stroke_ellipse(rect, color, stroke_width);
357    }
358    fn draw_line(
359        &mut self,
360        x1: f32,
361        y1: f32,
362        x2: f32,
363        y2: f32,
364        color: [f32; 4],
365        stroke_width: f32,
366    ) {
367        self.gpu.lock().unwrap().draw_line(x1, y1, x2, y2, color, stroke_width);
368    }
369    fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
370        self.gpu.lock().unwrap().draw_text(text, x, y, size, color);
371    }
372    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
373        self.gpu.lock().unwrap().measure_text(text, size)
374    }
375    fn draw_texture(&mut self, texture_id: u32, rect: cvkg_core::Rect) {
376        self.gpu.lock().unwrap().draw_texture(texture_id, rect);
377    }
378    fn draw_image(&mut self, image_name: &str, rect: cvkg_core::Rect) {
379        self.gpu.lock().unwrap().draw_image(image_name, rect);
380    }
381    fn load_image(&mut self, name: &str, data: &[u8]) {
382        self.gpu.lock().unwrap().load_image(name, data);
383    }
384    fn push_clip_rect(&mut self, rect: cvkg_core::Rect) {
385        self.gpu.lock().unwrap().push_clip_rect(rect);
386    }
387    fn pop_clip_rect(&mut self) {
388        self.gpu.lock().unwrap().pop_clip_rect();
389    }
390    fn push_opacity(&mut self, opacity: f32) {
391        self.gpu.lock().unwrap().push_opacity(opacity);
392    }
393    fn pop_opacity(&mut self) {
394        self.gpu.lock().unwrap().pop_opacity();
395    }
396    fn bifrost(&mut self, rect: cvkg_core::Rect, blur: f32, saturation: f32, opacity: f32) {
397        self.gpu.lock().unwrap().bifrost(rect, blur, saturation, opacity);
398    }
399    fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
400        self.gpu.lock().unwrap().push_mjolnir_slice(angle, offset);
401    }
402    fn pop_mjolnir_slice(&mut self) {
403        self.gpu.lock().unwrap().pop_mjolnir_slice();
404    }
405    fn register_shared_element(&mut self, id: &str, rect: cvkg_core::Rect) {
406        self.gpu.lock().unwrap().register_shared_element(id, rect);
407    }
408    fn set_z_index(&mut self, z: f32) {
409        self.gpu.lock().unwrap().set_z_index(z);
410    }
411    fn get_z_index(&self) -> f32 {
412        self.gpu.lock().unwrap().get_z_index()
413    }
414    fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
415        self.gpu.lock().unwrap().load_svg(name, svg_data);
416    }
417    fn draw_svg(&mut self, name: &str, rect: cvkg_core::Rect) {
418        self.gpu.lock().unwrap().draw_svg(name, rect, None, 0);
419    }
420    fn get_telemetry(&self) -> cvkg_core::TelemetryData {
421        self.gpu.lock().unwrap().telemetry.clone()
422    }
423}
424
425// Platform-specific implementations for macOS, Windows, and Linux are handled by winit and AccessKit.
426
427struct ShieldWall {
428    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
429}
430
431impl accesskit::ActionHandler for ShieldWall {
432    fn do_action(&mut self, request: accesskit::ActionRequest) {
433        let _ = self
434            .proxy
435            .send_event(AppEvent::AccessibilityAction(request));
436    }
437}
438
439impl accesskit::ActivationHandler for ShieldWall {
440    fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
441        let mut root = accesskit::Node::new(accesskit::Role::Window);
442        root.set_label("CVKG Application");
443
444        let root_id = accesskit::NodeId(1);
445        Some(accesskit::TreeUpdate {
446            nodes: vec![(root_id, root)],
447            tree: Some(accesskit::Tree::new(root_id)),
448            focus: root_id,
449        })
450    }
451}
452
453impl accesskit::DeactivationHandler for ShieldWall {
454    fn deactivate_accessibility(&mut self) {}
455}
456
457/// A concrete AssetManager for native desktop targets that loads from the local filesystem.
458///
459/// The cache is read on every render frame (lock-free via `ArcSwap::load()`) but written
460/// at most once per URL after disk I/O completes. `rcu()` atomically inserts the result
461/// without blocking concurrent render-loop readers.
462pub struct NativeAssetManager {
463    cache: std::sync::Arc<
464        arc_swap::ArcSwap<
465            std::collections::HashMap<String, cvkg_core::AssetState<std::sync::Arc<Vec<u8>>>>,
466        >,
467    >,
468}
469
470impl Default for NativeAssetManager {
471    fn default() -> Self {
472        Self::new()
473    }
474}
475
476impl NativeAssetManager {
477    /// Create a new, empty NativeAssetManager.
478    pub fn new() -> Self {
479        Self {
480            cache: std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
481                std::collections::HashMap::new(),
482            )),
483        }
484    }
485}
486
487impl cvkg_core::AssetManager for NativeAssetManager {
488    /// Return the cached asset state for `url`.
489    ///
490    /// Fast path: lock-free snapshot read via `ArcSwap::load()`.
491    /// Slow path (cache miss): perform filesystem I/O, then publish the result
492    /// with `rcu()` — no lock is held while reading the disk.
493    fn load_image(&self, url: &str) -> cvkg_core::AssetState<std::sync::Arc<Vec<u8>>> {
494        // Fast path: lock-free read from current cache snapshot
495        if let Some(state) = self.cache.load().get(url) {
496            return state.clone();
497        }
498
499        // Slow path: disk I/O, then atomic rcu insert
500        let result = match std::fs::read(url) {
501            Ok(data) => cvkg_core::AssetState::Ready(std::sync::Arc::new(data)),
502            Err(e) => cvkg_core::AssetState::Error(e.to_string()),
503        };
504        let result_clone = result.clone();
505        let key = url.to_string();
506        self.cache.rcu(move |map| {
507            let mut m = (**map).clone();
508            m.insert(key.clone(), result_clone.clone());
509            m
510        });
511        result
512    }
513
514    fn preload_image(&self, _url: &str) {
515        // Async preloading could be wired to a background thread here
516    }
517}