cardinal_gui/
uxn.rs

1/// Returns a reference to the build_orca_inject_queue function for use in event injection
2pub fn build_orca_inject_queue_ref(
3) -> Option<fn(&str) -> std::collections::VecDeque<InjectEvent>> {
4    Some(build_orca_inject_queue)
5}
6/// Build an InjectEvent queue for orca file injection with rectangle and efficient movement
7pub fn build_orca_inject_queue(
8    file_path: &str,
9) -> std::collections::VecDeque<InjectEvent> {
10    use std::collections::VecDeque;
11    use std::fs::File;
12    use std::io::{BufRead, BufReader};
13    use varvara::Key;
14
15    let mut queue = VecDeque::new();
16    const CTRL_H: Key = Key::Ctrl;
17    const RIGHT: Key = Key::Right;
18    const LEFT: Key = Key::Left;
19    const UP: Key = Key::Up;
20    const DOWN: Key = Key::Down;
21    // Read file into lines
22    let mut lines: Vec<Vec<char>> = Vec::new();
23    let mut max_len = 0;
24    if let Ok(file) = File::open(file_path) {
25        let reader = BufReader::new(file);
26        for line in reader.lines().map_while(Result::ok) {
27            let chars: Vec<char> = line.chars().collect();
28            max_len = max_len.max(chars.len());
29            lines.push(chars);
30        }
31    }
32    let rows = lines.len();
33    let cols = max_len;
34    // Build rectangle with '/' border
35    let mut grid = vec![vec!['/'; cols + 2]; rows + 2];
36    for i in 1..=rows {
37        for j in 1..=cols {
38            grid[i][j] = if j - 1 < lines[i - 1].len() {
39                lines[i - 1][j - 1]
40            } else {
41                ' '
42            };
43        }
44    }
45    // Start at (1,1)
46    let mut cur_row = 1;
47    let mut cur_col = 1;
48    queue.push_back(InjectEvent::KeyPress(CTRL_H));
49    queue.push_back(InjectEvent::KeyRelease(CTRL_H));
50    // Visit all non '.' cells efficiently (row-major order)
51    for (r, row) in grid.iter().enumerate().take(rows + 2) {
52        for (c, &ch) in row.iter().enumerate().take(cols + 2) {
53            if ch != '.' {
54                // Move to (r,c)
55                let dr = r as isize - cur_row as isize;
56                let dc = c as isize - cur_col as isize;
57                for _ in 0..dr.abs() {
58                    queue.push_back(if dr > 0 {
59                        InjectEvent::KeyPress(DOWN)
60                    } else {
61                        InjectEvent::KeyPress(UP)
62                    });
63                    queue.push_back(if dr > 0 {
64                        InjectEvent::KeyRelease(DOWN)
65                    } else {
66                        InjectEvent::KeyRelease(UP)
67                    });
68                }
69                for _ in 0..dc.abs() {
70                    queue.push_back(if dc > 0 {
71                        InjectEvent::KeyPress(RIGHT)
72                    } else {
73                        InjectEvent::KeyPress(LEFT)
74                    });
75                    queue.push_back(if dc > 0 {
76                        InjectEvent::KeyRelease(RIGHT)
77                    } else {
78                        InjectEvent::KeyRelease(LEFT)
79                    });
80                }
81                cur_row = r;
82                cur_col = c;
83                // Print char
84                queue.push_back(InjectEvent::Char(ch as u8));
85                // If this is a border '/' (not top/bottom), write hex x,y to the right
86                if ch == '/' && r != 0 && r != rows + 1 {
87                    // x = c, y = r
88                    let hex = format!("{c:02X}{r:02X}");
89                    for b in hex.bytes() {
90                        queue.push_back(InjectEvent::Char(b));
91                    }
92                }
93            }
94        }
95    }
96    queue
97}
98
99#[cfg(not(target_arch = "wasm32"))]
100use getrandom;
101#[derive(Debug, Clone)]
102pub enum InjectEvent {
103    Char(u8),
104    KeyPress(Key),
105    KeyRelease(Key),
106    Sleep(u64),      // milliseconds
107    Chord(Vec<Key>), // Simultaneous keypresses
108}
109// uxn.rs - Uxn integration for e_window
110
111#[allow(clippy::module_inception)]
112pub mod uxn {
113
114    use std::path::Path;
115    use std::sync::{Arc, Mutex};
116    use uxn::{Backend, Uxn};
117    use varvara::Varvara;
118    /// UxnModule: Encapsulates a Uxn VM and its state for e_window
119    pub struct UxnModule {
120        pub uxn: Arc<Mutex<Uxn<'static>>>,
121        pub varvara: Option<Varvara>,
122    }
123
124    impl UxnModule {
125        /// Create a new UxnModule, optionally loading a ROM file
126        pub fn new(rom_path: Option<&Path>) -> Result<Self, String> {
127            // Use a static RAM buffer for the Uxn VM
128            static mut RAM: [u8; 65536] = [0; 65536];
129            #[allow(static_mut_refs)]
130            let ram: &'static mut [u8; 65536] = unsafe { &mut RAM };
131            let mut uxn = Uxn::new(ram, Backend::Interpreter);
132            let mut varvara = Varvara::default();
133
134            // varvara.screen.buffer.resize(1024 * 768 * 4, 0);
135            // varvara.screen.changed = true;
136            if let Some(path) = rom_path {
137                let rom = std::fs::read(path)
138                    .map_err(|e| format!("Failed to read ROM: {e}"))?;
139                let _ = uxn.reset(&rom);
140                varvara.reset(&rom);
141
142                varvara.load_sym_with_rom_path(path);
143            }
144
145            Ok(UxnModule {
146                uxn: Arc::new(Mutex::new(uxn)),
147                varvara: Some(varvara),
148            })
149        }
150
151        // /// Reset the Uxn VM (clears memory and state)
152        // pub fn reset(&self, rom: &[u8]) {
153        //     let mut uxn = self.uxn.lock().unwrap();
154        //     let _ = uxn.reset(rom);
155        // }
156
157        /// Load a new ROM into the Uxn VM (resets VM)
158        pub fn load_rom(&mut self, rom_path: &Path) -> Result<(), String> {
159            let rom = std::fs::read(rom_path)
160                .map_err(|e| format!("Failed to read ROM: {e}"))?;
161            let mut uxn = self.uxn.lock().unwrap();
162            let _ = uxn.reset(&rom);
163            if let Some(varvara) = self.varvara.as_mut() {
164                varvara.load_sym_with_rom_path(rom_path);
165            }
166            Ok(())
167        }
168    }
169}
170
171use log::{error, info};
172use std::path::Path;
173use std::sync::{Arc, Mutex};
174// Re-export the cardinal-uxn and cardinal-varvara crates for Uxn VM and utilities
175use ::uxn::{Backend, Uxn};
176use varvara::Key;
177use varvara::MouseState;
178use varvara::TrackerState;
179use varvara::Varvara;
180/// UxnModule: Encapsulates a Uxn VM and its state for e_window
181pub struct UxnModule {
182    pub uxn: Arc<Mutex<Uxn<'static>>>,
183    pub varvara: Option<Varvara>,
184}
185
186impl UxnModule {
187    /// Create a new UxnModule, optionally loading a ROM file
188    pub fn new(rom_path: Option<&Path>) -> Result<Self, String> {
189        // Use a static RAM buffer for the Uxn VM
190        static mut RAM: [u8; 65536] = [0; 65536];
191        #[allow(static_mut_refs)]
192        let ram: &'static mut [u8; 65536] = unsafe { &mut RAM };
193        let mut uxn = Uxn::new(ram, Backend::Interpreter);
194        let mut varvara = Varvara::default();
195        if let Some(path) = rom_path {
196            let rom = std::fs::read(path)
197                .map_err(|e| format!("Failed to read ROM: {e}"))?;
198            let _ = uxn.reset(&rom);
199            varvara.load_sym_with_rom_path(path);
200        }
201        Ok(UxnModule {
202            uxn: Arc::new(Mutex::new(uxn)),
203            varvara: Some(varvara),
204        })
205    }
206
207    // Step/run methods are not available in cardinal-uxn. Use run/reset as needed.
208
209    // No run_cycles method; use run/reset as needed.
210
211    /// Reset the Uxn VM (clears memory and state)
212    pub fn reset(&self, rom: &[u8]) {
213        let mut uxn = self.uxn.lock().unwrap();
214        let _ = uxn.reset(rom);
215    }
216
217    /// Load a new ROM into the Uxn VM (resets VM)
218    pub fn load_rom(&mut self, rom_path: &Path) -> Result<(), String> {
219        let rom = std::fs::read(rom_path)
220            .map_err(|e| format!("Failed to read ROM: {e}"))?;
221        if let Some(varvara) = self.varvara.as_mut() {
222            varvara.load_sym_with_rom_path(rom_path);
223        }
224        self.reset(&rom);
225        Ok(())
226    }
227
228    // No get_state method; UxnState is not available in cardinal-uxn.
229}
230
231// Optionally, add egui integration for UxnModule (UI panel, etc.)
232pub mod egui_ui {
233    use super::*;
234    use egui::{CollapsingHeader, Ui};
235
236    pub fn show_uxn_panel(ui: &mut Ui, _uxn_mod: &UxnModule) {
237        CollapsingHeader::new("Uxn VM State").show(ui, |ui| {
238            ui.label("Uxn state display not implemented (no UxnState in _uxn)");
239        });
240    }
241}
242use eframe::egui;
243use std::sync::mpsc;
244// use log::{error, info};
245
246#[derive(Debug)]
247pub enum Event {
248    LoadRom(Vec<u8>),
249    SetMuted(bool),
250    Console(u8),
251}
252
253pub struct UxnApp<'a> {
254    pub vm: Uxn<'a>,
255    pub dev: Varvara,
256    scale: f32,
257    size: (u16, u16),
258    next_frame: f64,
259    scroll: (f32, f32),
260    cursor_pos: Option<(f32, f32)>,
261    texture: egui::TextureHandle,
262    event_rx: mpsc::Receiver<Event>,
263    resized: Option<Box<dyn FnMut(u16, u16)>>,
264    window_mode: String,
265    aspect_ratio: f32,
266    // For auto ROM cycling
267    auto_rom_select: bool,
268    auto_timer: f64,
269    auto_index: usize,
270    auto_roms: Vec<varvara::RomData>,
271    /// Parallel to auto_roms: labels or filenames for each ROM
272    auto_rom_labels: Vec<String>,
273    /// Callback for when the ROM changes (filename or label)
274    #[allow(clippy::type_complexity)]
275    on_rom_change: Option<Box<dyn Fn(&str) + Send + Sync>>,
276    /// Callback for first update/frame (for deferred actions)
277    #[allow(clippy::type_complexity)]
278    on_first_update: Option<Box<dyn FnOnce(&mut UxnApp<'a>) + Send + 'a>>,
279    first_update_done: bool,
280    /// The current ROM label or filename (if available)
281    current_rom_label: Option<String>,
282    /// Queue for deferred input events (for orca injection)
283    input_queue: std::collections::VecDeque<InjectEvent>,
284    /// The last ROM file path loaded (if any)
285    last_rom_path: Option<std::path::PathBuf>,
286    // --- Hot-reload support ---
287    pub reload_rx: std::sync::mpsc::Receiver<()>,
288    pub rom_path_arc:
289        std::sync::Arc<std::sync::Mutex<Option<std::path::PathBuf>>>,
290    #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
291    pub usb_controller: Option<UsbControllerHandle>,
292    #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
293    pub last_usb_event: Option<(u8, Vec<u8>)>, // (pedal_state, raw data)
294}
295
296#[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
297pub struct UsbControllerHandle {
298    pub rx: std::sync::mpsc::Receiver<
299        varvara::controller_usb::UsbControllerMessage,
300    >,
301}
302
303impl<'a> UxnApp<'a> {
304    /// Set a callback to be called on the first update/frame (for deferred actions)
305    pub fn set_on_first_update(
306        &mut self,
307        f: Box<dyn FnOnce(&mut UxnApp<'a>) + Send + 'a>,
308    ) {
309        self.on_first_update = Some(f);
310        self.first_update_done = false;
311    }
312    /// Set the current ROM label or filename (for title/callback)
313    pub fn set_rom_label<S: Into<String>>(&mut self, label: S) {
314        self.current_rom_label = Some(label.into());
315    }
316    #[allow(clippy::too_many_arguments)]
317    pub fn new_with_mode(
318        mut vm: Uxn<'a>,
319        mut dev: Varvara,
320        mut size: (u16, u16),
321        scale: f32,
322        event_rx: mpsc::Receiver<Event>,
323        ctx: &egui::Context,
324        window_mode: String,
325        auto_roms: Vec<varvara::RomData>,
326        auto_rom_labels: Vec<String>,
327        auto_rom_select: bool,
328        reload_rx: std::sync::mpsc::Receiver<()>,
329        rom_path_arc: std::sync::Arc<
330            std::sync::Mutex<Option<std::path::PathBuf>>,
331        >,
332    ) -> Self {
333        if let Some(path) = rom_path_arc.lock().unwrap().as_ref() {
334            if path.exists() {
335                dev.load_sym_with_rom_path(path);
336            }
337        }
338        // Run the VM and redraw once to initialize the framebuffer
339        vm.run(&mut dev, 0x100);
340
341        dev.redraw(&mut vm);
342
343        let w = 2048_usize; //size.0);
344        let h = 2048_usize;
345        size.0 = w as u16;
346        size.1 = h as u16;
347        let image =
348            egui::ColorImage::new([w, h], vec![egui::Color32::BLACK; w * h]);
349        let texture =
350            ctx.load_texture("frame", image, egui::TextureOptions::NEAREST);
351        let aspect_ratio = w as f32 / h as f32;
352        let mut auto_index = 0;
353        let mut auto_timer = 0.0;
354        let mut current_rom_label = None;
355        if auto_rom_select && !auto_roms.is_empty() {
356            println!(
357                "[AUTO ROM CYCLING] Enabled. Cycling through {} ROMs every 10s.",
358                auto_roms.len()
359            );
360            // Load the first ROM immediately
361            let rom = &auto_roms[0];
362            let _ = vm.reset(&rom.rom);
363            dev.reset(&rom.rom);
364            vm.run(&mut dev, 0x100);
365            dev.redraw(&mut vm);
366
367            auto_index = 0;
368            auto_timer = 0.0;
369            // Use the provided label if available, else fallback
370            if !auto_rom_labels.is_empty() {
371                current_rom_label = Some(auto_rom_labels[0].clone());
372            } else {
373                current_rom_label = Some("ROM 1".to_string());
374            }
375        }
376        UxnApp {
377            vm,
378            dev,
379            scale,
380            size,
381            next_frame: 0.0,
382            event_rx,
383            resized: None,
384            scroll: (0.0, 0.0),
385            cursor_pos: None,
386            texture,
387            window_mode,
388            aspect_ratio,
389            auto_rom_select,
390            auto_timer,
391            auto_index,
392            auto_roms,
393            on_rom_change: None,
394            current_rom_label,
395            auto_rom_labels,
396            on_first_update: None,
397            first_update_done: false,
398            input_queue: std::collections::VecDeque::new(),
399            reload_rx,
400            rom_path_arc,
401            last_rom_path: None,
402            #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
403            usb_controller: None,
404            #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
405            last_usb_event: None,
406        }
407    }
408    /// Set a callback to be called when the ROM changes (filename or label)
409    pub fn set_on_rom_change<F>(&mut self, f: F)
410    where
411        F: Fn(&str) + Send + Sync + 'static,
412    {
413        self.on_rom_change = Some(Box::new(f));
414    }
415
416    pub fn load_symbols(
417        &mut self,
418        path: &std::path::Path,
419    ) -> Result<(), String> {
420        println!("[UxnApp] Loading symbols from: {}", path.display());
421        let _ = self.dev.load_symbols_into_self(path.to_str().unwrap());
422        Ok(())
423    }
424
425    /// Reload the ROM from the given path (for hot-reload)
426    pub fn reload_rom(&mut self, path: &std::path::Path) -> Result<(), String> {
427        println!("[UxnApp] Reloading ROM from: {}", path.display());
428        let rom = std::fs::read(path)
429            .map_err(|e| format!("Failed to read ROM: {e}"))?;
430        let _ = self.vm.reset(&rom);
431        self.dev.reset(&rom);
432        self.vm.run(&mut self.dev, 0x100);
433        self.dev.redraw(&mut self.vm);
434
435        self.load_symbols(path)?;
436
437        // Explicitly beep after reload
438        #[cfg(windows)]
439        {
440            unsafe {
441                winapi::um::winuser::MessageBeep(0xFFFFFFFF);
442            }
443        }
444        Ok(())
445    }
446
447    pub fn set_resize_callback(&mut self, f: Box<dyn FnMut(u16, u16)>) {
448        self.resized = Some(f);
449    }
450
451    /// Load a ROM from a file path, tracking the file path for symbol support
452    pub fn load_rom_with_path<P: AsRef<std::path::Path>>(
453        &mut self,
454        path: P,
455    ) -> anyhow::Result<()> {
456        let path = path.as_ref();
457        let data = std::fs::read(path)?;
458        self.last_rom_path = Some(path.to_path_buf());
459        let _ = self.vm.reset(&data);
460        self.dev.reset(&data);
461        self.vm.run(&mut self.dev, 0x100);
462        // Try to load .sym file if ROM was loaded from a file
463        let sym_path = path.with_extension("sym");
464        if sym_path.exists() {
465            let _ = self.dev.load_symbols_into_self(sym_path.to_str().unwrap());
466        }
467        // Optionally update the ROM label
468        if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
469            self.set_rom_label(name);
470        }
471        let out = self.dev.output(&self.vm);
472        out.check()?;
473        Ok(())
474    }
475
476    /// Backward-compatible load_rom for in-memory loads (no file path)
477    fn load_rom(&mut self, data: &[u8]) -> anyhow::Result<()> {
478        self.last_rom_path = None;
479        let _ = self.vm.reset(data);
480        self.dev.reset(data);
481        self.vm.run(&mut self.dev, 0x100);
482        let out = self.dev.output(&self.vm);
483        out.check()?;
484        Ok(())
485    }
486
487    // Helper to get the last ROM path if available (for symbol loading)
488    // fn get_last_rom_path(&self) -> Option<std::path::PathBuf> {
489    //     self.last_rom_path.clone()
490    // }
491
492    /// Queue a sequence of input events to be sent per frame
493    pub fn queue_input<I: IntoIterator<Item = InjectEvent>>(
494        &mut self,
495        input: I,
496    ) {
497        self.input_queue.extend(input);
498    }
499}
500
501impl eframe::App for UxnApp<'_> {
502    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
503        // --- ROM hot-reload check ---
504        if self.reload_rx.try_recv().is_ok() {
505            let path_opt = { self.rom_path_arc.lock().unwrap().clone() };
506            if let Some(ref path) = path_opt {
507                println!(
508                    "[ROM WATCHER] Detected ROM file change, reloading: {}",
509                    path.display()
510                );
511                if let Err(e) = self.reload_rom(path) {
512                    eprintln!("[ROM WATCHER] Failed to reload ROM in app: {e}");
513                } else {
514                    println!("[ROM WATCHER] ROM reloaded in app successfully.");
515                }
516            }
517        }
518        // --- First update callback for deferred actions (e.g., orca file injection) ---
519        if !self.first_update_done {
520            if let Some(cb) = self.on_first_update.take() {
521                cb(self);
522            }
523            self.first_update_done = true;
524        }
525        // --- Ctrl+C and Ctrl+R event handling ---
526        let orca_dir = r"C:\w\music\Orca-c\examples\basics";
527        let files: Vec<std::path::PathBuf> = std::fs::read_dir(orca_dir)
528            .map(|read_dir| {
529                read_dir
530                    .filter_map(|entry| {
531                        entry.ok().and_then(|e| {
532                            let path = e.path();
533                            if path.extension().and_then(|ext| ext.to_str())
534                                == Some("orca")
535                            {
536                                Some(path)
537                            } else {
538                                None
539                            }
540                        })
541                    })
542                    .collect::<Vec<_>>()
543            })
544            .unwrap_or_else(|_| Vec::new());
545        static mut LAST_CTRL_R: Option<std::time::Instant> = None;
546        let mut exit_requested = false;
547        for event in ctx.input(|i| i.events.clone()) {
548            if let egui::Event::Key { key, pressed, .. } = event {
549                if key == egui::Key::C
550                    && ctx.input(|i| i.modifiers.ctrl)
551                    && pressed
552                {
553                    exit_requested = true;
554                }
555                if key == egui::Key::R
556                    && ctx.input(|i| i.modifiers.ctrl)
557                    && pressed
558                {
559                    let now = std::time::Instant::now();
560                    let last = unsafe { LAST_CTRL_R };
561                    let allow = match last {
562                        Some(t) => now.duration_since(t).as_millis() > 500,
563                        None => true,
564                    };
565                    if allow {
566                        unsafe {
567                            LAST_CTRL_R = Some(now);
568                        }
569                        if !files.is_empty() {
570                            #[cfg(target_arch = "wasm32")]
571                            {
572                                // On wasm, just cycle sequentially through the files
573                                static mut LAST_IDX: usize = 0;
574                                let idx = unsafe {
575                                    let i = LAST_IDX;
576                                    LAST_IDX = (LAST_IDX + 1) % files.len();
577                                    i
578                                };
579                                let random_file = &files[idx];
580                                let queue = build_orca_inject_queue(
581                                    random_file.to_str().unwrap(),
582                                );
583                                self.queue_input(queue);
584                            }
585                            #[cfg(not(target_arch = "wasm32"))]
586                            {
587                                // On native, pick a random file
588                                if let Ok(idx) = get_random_u128()
589                                    .map(|v| (v as usize) % files.len())
590                                {
591                                    let idx: usize = idx;
592                                    let random_file = &files[idx];
593                                    let queue = build_orca_inject_queue(
594                                        random_file.to_str().unwrap(),
595                                    );
596                                    self.queue_input(queue);
597                                }
598                            }
599                        }
600                    }
601                }
602            }
603        }
604
605        /// Returns a random u128 using getrandom
606        #[cfg(not(target_arch = "wasm32"))]
607        pub fn get_random_u128() -> Result<u128, getrandom::Error> {
608            let mut buf = [0u8; 16];
609            getrandom::fill(&mut buf)?;
610            Ok(u128::from_ne_bytes(buf))
611        }
612
613        #[cfg(target_arch = "wasm32")]
614        #[allow(dead_code)]
615        pub fn get_random_u128() -> Result<u128, Result<u128, ()>> {
616            // WASM: fallback to a deterministic value or use JS random if needed
617            Ok(42)
618        }
619        if exit_requested {
620            std::process::exit(0);
621        }
622        // --- Per-frame input injection ---
623        if let Some(event) = self.input_queue.pop_front() {
624            match event {
625                InjectEvent::Char(c) => self.dev.char(&mut self.vm, c),
626                InjectEvent::KeyPress(k) => {
627                    self.dev.pressed(&mut self.vm, k, false)
628                }
629                InjectEvent::KeyRelease(k) => {
630                    self.dev.released(&mut self.vm, k)
631                }
632                InjectEvent::Sleep(ms) => {
633                    std::thread::sleep(std::time::Duration::from_millis(ms));
634                }
635                InjectEvent::Chord(keys) => {
636                    for k in &keys {
637                        self.dev.pressed(&mut self.vm, *k, false);
638                    }
639                }
640            }
641        }
642        // --- AUTO ROM CYCLING: Switch between ROMs every 10 seconds if enabled ---
643        if self.auto_rom_select && !self.auto_roms.is_empty() {
644            let dt = ctx.input(|i| i.stable_dt) as f64;
645            log::debug!(
646                "[AUTO ROM CYCLING] auto_timer: {:.3}, dt: {:.3}, auto_index: {}, roms: {}",
647                self.auto_timer,
648                dt,
649                self.auto_index,
650                self.auto_roms.len()
651            );
652            self.auto_timer += dt;
653            if self.auto_timer == 0.0 && self.auto_index == 0 {
654                log::debug!("[AUTO ROM CYCLING] First ROM already loaded.");
655                // Already loaded first ROM in new_with_mode
656            } else if self.auto_timer > 10.0 {
657                log::debug!(
658                    "[AUTO ROM CYCLING] Switching ROM: {} -> {}",
659                    self.auto_index,
660                    (self.auto_index + 1) % self.auto_roms.len()
661                );
662                self.auto_timer = 0.0;
663                self.auto_index = (self.auto_index + 1) % self.auto_roms.len();
664                let rom = self.auto_roms[self.auto_index].clone();
665                // Use the correct label for the new ROM
666                let label = if self.auto_index < self.auto_rom_labels.len() {
667                    self.auto_rom_labels[self.auto_index].clone()
668                } else {
669                    format!("ROM {}", self.auto_index + 1)
670                };
671                self.set_rom_label(label);
672                let _ = self.load_rom(&rom.rom);
673                if rom.sym.is_some() {
674                    // Load symbols if available
675                    if let Some(sym) = &rom.sym {
676                        let _ = self.dev.load_symbols_from_vec(sym);
677                    }
678                }
679            }
680        }
681        while let Ok(e) = self.event_rx.try_recv() {
682            match e {
683                Event::LoadRom(data) => {
684                    if let Err(e) = self.load_rom(&data) {
685                        error!("could not load rom: {e:?}");
686                    }
687                }
688                Event::SetMuted(m) => {
689                    self.dev.audio_set_muted(m);
690                }
691                Event::Console(b) => {
692                    self.dev.console(&mut self.vm, b);
693                }
694            }
695        }
696        // if let Some(ref mut varvara_controller) =
697        //     self.dev
698        //         .controller
699        //         .as_any()
700        //         .downcast_mut::<varvara::controller::Controller>()
701        // {
702        //     // Example pedal event mapping (replace with your actual pedal state logic)
703        //     // Assume pedal_state is a u8 bitmask from your input source
704        //     let pedal_state: u8 = 0; // TODO: get real pedal state
705        //     static mut PREV_PEDAL: u8 = 0;
706        //     let _prev;
707        //     unsafe {
708        //         _prev = PREV_PEDAL;
709        //         PREV_PEDAL = pedal_state;
710        //     }
711        //     // Use canonical helper from varvara::controller
712        //     varvara::controller::inject_pedal_keys(
713        //         varvara_controller,
714        //         &mut self.vm,
715        //         _prev,
716        //         pedal_state,
717        //     );
718        // }
719    #[cfg(target_arch = "wasm32")]
720    let events = Vec::new();
721    #[cfg(not(target_arch = "wasm32"))]
722    let mut events = Vec::new();
723        #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
724        {
725            // Only borrow self.dev.controller mutably once in this block
726            let mut last_pedal = None;
727            if let Some(controller_usb) =
728                self.dev
729                    .controller
730                    .as_any()
731                    .downcast_mut::<varvara::controller_usb::ControllerUsb>()
732            {
733                events = varvara::controller_usb::ControllerPollEvents::poll_usb_events(controller_usb, &mut self.vm);
734                last_pedal = controller_usb.last_pedal;
735            }
736            // Now, after the mutable borrow is done, update last_usb_event, etc.
737            static mut PREV_PEDAL: u8 = 0;
738            if let Some(pedal) = last_pedal {
739                // Update last_usb_event for debug panel
740                self.last_usb_event = Some((pedal, vec![]));
741                let _prev;
742                unsafe {
743                    _prev = PREV_PEDAL;
744                    PREV_PEDAL = pedal;
745                }
746                // Use shared controller pedal key injection logic
747                // Controller::inject_pedal_keys(&mut self.vm, prev, pedal);
748            }
749        }
750        let pedal_data: Vec<u8> = events
751            .iter()
752            .flat_map(|e: &varvara::Event| e.data.iter().map(|b| b.value))
753            .collect();
754        for value in pedal_data {
755            self.dev
756                .pressed(&mut self.vm, varvara::Key::Char(value), true);
757            self.dev.char(&mut self.vm, value);
758        }
759
760        ctx.request_repaint();
761        ctx.input(|i| {
762            while i.time >= self.next_frame {
763                self.next_frame += 0.0166667;
764                self.dev.redraw(&mut self.vm);
765            }
766            if i.raw.dropped_files.len() == 1 {
767                let target = &i.raw.dropped_files[0];
768                let r = if let Some(path) = &target.path {
769                    info!("loading ROM from {path:?}");
770                    self.load_rom_with_path(path)
771                } else if let Some(data) = &target.bytes {
772                    self.load_rom(data)
773                } else {
774                    Ok(())
775                };
776                if let Err(e) = r {
777                    error!("could not load ROM: {e:?}");
778                }
779            }
780            let shift_held = i.modifiers.shift;
781            for e in i.events.iter() {
782                match e {
783                    egui::Event::Text(s) => {
784                        for c in s.bytes() {
785                            self.dev.char(&mut self.vm, c);
786                        }
787                    }
788                    egui::Event::Key {
789                        key,
790                        pressed,
791                        repeat,
792                        ..
793                    } => {
794                        if let Some(k) = decode_key(*key, shift_held) {
795                            if *pressed {
796                                self.dev.pressed(&mut self.vm, k, *repeat);
797                            } else {
798                                self.dev.released(&mut self.vm, k);
799                            }
800                        }
801                    }
802                    _ => (),
803                }
804            }
805            for (b, k) in [
806                (i.modifiers.ctrl, Key::Ctrl),
807                (i.modifiers.alt, Key::Alt),
808                (i.modifiers.shift, Key::Shift),
809            ] {
810                if b {
811                    self.dev.pressed(&mut self.vm, k, false)
812                } else {
813                    self.dev.released(&mut self.vm, k)
814                }
815            }
816            let ptr = &i.pointer;
817            if let Some(p) = ptr.latest_pos() {
818                self.cursor_pos = Some((p.x / self.scale, p.y / self.scale));
819            }
820            let buttons = [
821                egui::PointerButton::Primary,
822                egui::PointerButton::Middle,
823                egui::PointerButton::Secondary,
824            ]
825            .into_iter()
826            .enumerate()
827            .map(|(i, b)| (ptr.button_down(b) as u8) << i)
828            .fold(0, |a, b| a | b);
829            let m = MouseState {
830                pos: self.cursor_pos.unwrap_or((0.0, 0.0)),
831                scroll: std::mem::take(&mut self.scroll),
832                buttons,
833            };
834            self.dev.mouse(&mut self.vm, m);
835            let m = TrackerState {
836                pos: self
837                    .cursor_pos
838                    .map(|(x, y)| (x + 16.0, y + 16.0))
839                    .unwrap_or((16.0, 16.0)),
840                scroll: std::mem::take(&mut self.scroll),
841                buttons,
842            };
843            self.dev.tracker(&mut self.vm, m);
844            i.time
845        });
846        self.dev.audio(&mut self.vm);
847        let out = self.dev.output(&self.vm);
848        if out.hide_mouse {
849            ctx.set_cursor_icon(egui::CursorIcon::None);
850        }
851        if self.size != out.size {
852            // Get current window size in logical points
853            let current_window_size = ctx.input(|i| {
854                i.viewport()
855                    .inner_rect
856                    .map_or(egui::Vec2::ZERO, |rect| rect.size())
857            });
858            let new_size =
859                egui::Vec2::new(out.size.0 as f32, out.size.1 as f32)
860                    * self.scale;
861            // let should_resize = new_size.x > current_window_size.x || new_size.y > current_window_size.y
862            //     || new_size.x < current_window_size.x || new_size.y < current_window_size.y;
863            // // Only resize if the new frame is larger, or if it's smaller than the window
864            // if should_resize {
865            // Only resize if the new frame is larger than the current window
866            if new_size.x > current_window_size.x
867                || new_size.y > current_window_size.y
868            {
869                info!("resizing window to {:?}", out.size);
870                self.size = out.size;
871                let mut size = new_size;
872                // Enforce proportional resizing if needed
873                if self.window_mode == "proportional" {
874                    let aspect = self.aspect_ratio;
875                    let w = size.x;
876                    let h = size.y;
877                    let (new_w, new_h) = if w / h > aspect {
878                        (h * aspect, h)
879                    } else {
880                        (w, w / aspect)
881                    };
882                    size = egui::Vec2::new(new_w, new_h);
883                }
884                ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(size));
885                if let Some(f) = self.resized.as_mut() {
886                    f(out.size.0, out.size.1);
887                }
888            }
889        }
890        let w = out.size.0 as usize;
891        let h = out.size.1 as usize;
892        let mut image =
893            egui::ColorImage::new([w, h], vec![egui::Color32::BLACK; w * h]);
894        for (i, o) in out.frame.chunks(4).zip(image.pixels.iter_mut()) {
895            *o = egui::Color32::from_rgba_unmultiplied(i[2], i[1], i[0], i[3]);
896        }
897        self.texture.set(image, egui::TextureOptions::NEAREST);
898        egui::CentralPanel::default()
899            .frame(egui::Frame::default().fill(egui::Color32::BLACK))
900            .show(ctx, |ui| {
901                let available = ui.available_size();
902                let frame_size = egui::Vec2::new(
903                    out.size.0 as f32 * self.scale,
904                    out.size.1 as f32 * self.scale,
905                );
906                let offset = egui::Vec2::new(
907                    (available.x - frame_size.x) * 0.5,
908                    (available.y - frame_size.y) * 0.5,
909                );
910                let top_left = ui.min_rect().min + offset;
911                let mut mesh = egui::Mesh::with_texture(self.texture.id());
912                mesh.add_rect_with_uv(
913                    egui::Rect {
914                        min: top_left,
915                        max: top_left + frame_size,
916                    },
917                    egui::Rect {
918                        min: egui::Pos2::new(0.0, 0.0),
919                        max: egui::Pos2::new(1.0, 1.0),
920                    },
921                    egui::Color32::WHITE,
922                );
923                ui.painter().add(egui::Shape::mesh(mesh));
924                // --- USB Debug Panel ---
925                #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
926                egui::CollapsingHeader::new("USB Pedal Debug").default_open(true).show(ui, |ui| {
927                    if let Some((pedal_state, ref data)) = self.last_usb_event {
928                        ui.label(format!("[USB] Last pedal event: state=0x{pedal_state:02X}, data={data:?}"));
929                    } else {
930                        ui.label("[USB] No pedal events received yet.");
931                    }
932                });
933                // Show auto ROM cycling info
934                if self.auto_rom_select && !self.auto_roms.is_empty() {
935                    ui.label(format!(
936                        "[AUTO ROM CYCLING] Switching ROM every 10s. ROMs loaded: {}",
937                        self.auto_roms.len()
938                    ));
939                }
940            });
941        out.check().ok();
942    }
943}
944
945pub fn decode_key(k: egui::Key, shift: bool) -> Option<Key> {
946    match (k, shift) {
947        (egui::Key::ArrowUp, _) => Some(Key::Up),
948        (egui::Key::ArrowDown, _) => Some(Key::Down),
949        (egui::Key::ArrowLeft, _) => Some(Key::Left),
950        (egui::Key::ArrowRight, _) => Some(Key::Right),
951        (egui::Key::Home, _) => Some(Key::Home),
952        (egui::Key::Num0, false) => Some(Key::Char(b'0')),
953        (egui::Key::Num0, true) => Some(Key::Char(b')')),
954        (egui::Key::Num1, false) => Some(Key::Char(b'1')),
955        (egui::Key::Num1, true) => Some(Key::Char(b'!')),
956        (egui::Key::Num2, false) => Some(Key::Char(b'2')),
957        (egui::Key::Num2, true) => Some(Key::Char(b'@')),
958        (egui::Key::Num3, false) => Some(Key::Char(b'3')),
959        (egui::Key::Num3, true) => Some(Key::Char(b'#')),
960        (egui::Key::Num4, false) => Some(Key::Char(b'4')),
961        (egui::Key::Num4, true) => Some(Key::Char(b'$')),
962        (egui::Key::Num5, false) => Some(Key::Char(b'5')),
963        (egui::Key::Num5, true) => Some(Key::Char(b'%')),
964        (egui::Key::Num6, false) => Some(Key::Char(b'6')),
965        (egui::Key::Num6, true) => Some(Key::Char(b'^')),
966        (egui::Key::Num7, false) => Some(Key::Char(b'7')),
967        (egui::Key::Num7, true) => Some(Key::Char(b'&')),
968        (egui::Key::Num8, false) => Some(Key::Char(b'8')),
969        (egui::Key::Num8, true) => Some(Key::Char(b'*')),
970        (egui::Key::Num9, false) => Some(Key::Char(b'9')),
971        (egui::Key::Num9, true) => Some(Key::Char(b'(')),
972        (egui::Key::A, false) => Some(Key::Char(b'a')),
973        (egui::Key::A, true) => Some(Key::Char(b'A')),
974        (egui::Key::B, false) => Some(Key::Char(b'b')),
975        (egui::Key::B, true) => Some(Key::Char(b'B')),
976        (egui::Key::C, false) => Some(Key::Char(b'c')),
977        (egui::Key::C, true) => Some(Key::Char(b'C')),
978        (egui::Key::D, false) => Some(Key::Char(b'd')),
979        (egui::Key::D, true) => Some(Key::Char(b'D')),
980        (egui::Key::E, false) => Some(Key::Char(b'e')),
981        (egui::Key::E, true) => Some(Key::Char(b'E')),
982        (egui::Key::F, false) => Some(Key::Char(b'f')),
983        (egui::Key::F, true) => Some(Key::Char(b'F')),
984        (egui::Key::G, false) => Some(Key::Char(b'g')),
985        (egui::Key::G, true) => Some(Key::Char(b'G')),
986        (egui::Key::H, false) => Some(Key::Char(b'h')),
987        (egui::Key::H, true) => Some(Key::Char(b'H')),
988        (egui::Key::I, false) => Some(Key::Char(b'i')),
989        (egui::Key::I, true) => Some(Key::Char(b'I')),
990        (egui::Key::J, false) => Some(Key::Char(b'j')),
991        (egui::Key::J, true) => Some(Key::Char(b'J')),
992        (egui::Key::K, false) => Some(Key::Char(b'k')),
993        (egui::Key::K, true) => Some(Key::Char(b'K')),
994        (egui::Key::L, false) => Some(Key::Char(b'l')),
995        (egui::Key::L, true) => Some(Key::Char(b'L')),
996        (egui::Key::M, false) => Some(Key::Char(b'm')),
997        (egui::Key::M, true) => Some(Key::Char(b'M')),
998        (egui::Key::N, false) => Some(Key::Char(b'n')),
999        (egui::Key::N, true) => Some(Key::Char(b'N')),
1000        (egui::Key::O, false) => Some(Key::Char(b'o')),
1001        (egui::Key::O, true) => Some(Key::Char(b'O')),
1002        (egui::Key::P, false) => Some(Key::Char(b'p')),
1003        (egui::Key::P, true) => Some(Key::Char(b'P')),
1004        (egui::Key::Q, false) => Some(Key::Char(b'q')),
1005        (egui::Key::Q, true) => Some(Key::Char(b'Q')),
1006        (egui::Key::R, false) => Some(Key::Char(b'r')),
1007        (egui::Key::R, true) => Some(Key::Char(b'R')),
1008        (egui::Key::S, false) => Some(Key::Char(b's')),
1009        (egui::Key::S, true) => Some(Key::Char(b'S')),
1010        (egui::Key::T, false) => Some(Key::Char(b't')),
1011        (egui::Key::T, true) => Some(Key::Char(b'T')),
1012        (egui::Key::U, false) => Some(Key::Char(b'u')),
1013        (egui::Key::U, true) => Some(Key::Char(b'U')),
1014        (egui::Key::V, false) => Some(Key::Char(b'v')),
1015        (egui::Key::V, true) => Some(Key::Char(b'V')),
1016        (egui::Key::W, false) => Some(Key::Char(b'w')),
1017        (egui::Key::W, true) => Some(Key::Char(b'W')),
1018        (egui::Key::X, false) => Some(Key::Char(b'x')),
1019        (egui::Key::X, true) => Some(Key::Char(b'X')),
1020        (egui::Key::Y, false) => Some(Key::Char(b'y')),
1021        (egui::Key::Y, true) => Some(Key::Char(b'Y')),
1022        (egui::Key::Z, false) => Some(Key::Char(b'z')),
1023        (egui::Key::Z, true) => Some(Key::Char(b'Z')),
1024        (egui::Key::Backtick, false) => Some(Key::Char(b'`')),
1025        (egui::Key::Backtick, true) => Some(Key::Char(b'~')),
1026        (egui::Key::Backslash, _) => Some(Key::Char(b'\\')),
1027        (egui::Key::Pipe, _) => Some(Key::Char(b'|')),
1028        (egui::Key::Comma, false) => Some(Key::Char(b',')),
1029        (egui::Key::Comma, true) => Some(Key::Char(b'<')),
1030        (egui::Key::Equals, _) => Some(Key::Char(b'=')),
1031        (egui::Key::Plus, _) => Some(Key::Char(b'+')),
1032        (egui::Key::OpenBracket, false) => Some(Key::Char(b'[')),
1033        (egui::Key::OpenBracket, true) => Some(Key::Char(b'{')),
1034        (egui::Key::Minus, false) => Some(Key::Char(b'-')),
1035        (egui::Key::Minus, true) => Some(Key::Char(b'_')),
1036        (egui::Key::Period, false) => Some(Key::Char(b'.')),
1037        (egui::Key::Period, true) => Some(Key::Char(b'>')),
1038        (egui::Key::CloseBracket, false) => Some(Key::Char(b']')),
1039        (egui::Key::CloseBracket, true) => Some(Key::Char(b'}')),
1040        (egui::Key::Semicolon, _) => Some(Key::Char(b';')),
1041        (egui::Key::Colon, _) => Some(Key::Char(b':')),
1042        (egui::Key::Slash, _) => Some(Key::Char(b'/')),
1043        (egui::Key::Questionmark, _) => Some(Key::Char(b'?')),
1044        (egui::Key::Space, _) => Some(Key::Char(b' ')),
1045        (egui::Key::Tab, _) => Some(Key::Char(b'\t')),
1046        (egui::Key::Enter, _) => Some(Key::Char(b'\r')),
1047        (egui::Key::Backspace, _) => Some(Key::Char(0x08)),
1048        _ => None,
1049    }
1050}
1051
1052/// Stub for audio_setup on wasm32
1053// #[cfg(target_arch = "wasm32")]
1054// pub fn audio_setup(
1055//     _data: [Arc<Mutex<varvara::StreamData>>; 4],
1056// ) -> Option<()> {
1057//     None
1058// }
1059/// Shared audio setup for Varvara audio streams
1060// #[cfg(not(target_arch = "wasm32"))]
1061pub fn audio_setup(
1062    data: [Arc<Mutex<varvara::StreamData>>; 4],
1063) -> Option<(cpal::Device, [cpal::Stream; 4])> {
1064    use cpal::traits::{DeviceTrait, HostTrait};
1065
1066    let host = cpal::default_host();
1067    let device = host
1068        .default_output_device()
1069        .expect("no output device available");
1070    let supported_configs_range = device
1071        .supported_output_configs()
1072        .expect("error while querying configs")
1073        .collect::<Vec<_>>();
1074
1075    let mut supported_config = None;
1076    let mut used_rate = 0;
1077    for &rate in &[48000, 44100] {
1078        supported_config = supported_configs_range
1079            .iter()
1080            .filter(|c| usize::from(c.channels()) == varvara::AUDIO_CHANNELS)
1081            .filter(|c| c.sample_format() == cpal::SampleFormat::F32)
1082            .find_map(|c| c.try_with_sample_rate(cpal::SampleRate(rate)));
1083        if supported_config.is_some() {
1084            used_rate = rate;
1085            break;
1086        }
1087    }
1088    let supported_config = match supported_config {
1089        Some(cfg) => cfg,
1090        None => {
1091            log::error!(
1092                "could not find supported audio config ({} channels, 48000 or 44100 Hz, f32)",
1093                varvara::AUDIO_CHANNELS
1094            );
1095            log::error!("available configs:");
1096            for c in &supported_configs_range {
1097                if c.min_sample_rate() == c.max_sample_rate() {
1098                    log::error!(
1099                        "  channels: {}, sample_rate: {} Hz, {}",
1100                        c.channels(),
1101                        c.min_sample_rate().0,
1102                        c.sample_format(),
1103                    );
1104                } else {
1105                    log::error!(
1106                        "  channels: {}, sample_rate: {} - {} Hz, {}",
1107                        c.channels(),
1108                        c.min_sample_rate().0,
1109                        c.max_sample_rate().0,
1110                        c.sample_format(),
1111                    );
1112                }
1113            }
1114            return None;
1115        }
1116    };
1117    // Set the sample rate in the audio engine
1118    varvara::set_sample_rate(used_rate);
1119    let config = supported_config.config();
1120
1121    let streams = data.map(|d| {
1122        let stream = device
1123            .build_output_stream(
1124                &config,
1125                move |data: &mut [f32], _opt: &cpal::OutputCallbackInfo| {
1126                    d.lock().unwrap().next(data);
1127                },
1128                move |err| {
1129                    panic!("{err}");
1130                },
1131                None,
1132            )
1133            .expect("could not build stream");
1134        cpal::traits::StreamTrait::play(&stream).unwrap();
1135        stream
1136    });
1137    Some((device, streams))
1138}
1139
1140/// Inject key events from egui input into the VM, handling press/release and modifiers
1141pub fn inject_key_events(
1142    vm: &mut ::uxn::Uxn,
1143    dev: &mut varvara::Varvara,
1144    events: &[egui::Event],
1145    modifiers: &egui::Modifiers,
1146    _repeat: bool,
1147) {
1148    use varvara::Key;
1149    let shift_held = modifiers.shift;
1150    for e in events {
1151        match e {
1152            egui::Event::Text(s) => {
1153                const RAW_CHARS: [u8; 16] = [
1154                    b'"', b'\'', b'{', b'}', b'_', b')', b'(', b'*', b'&',
1155                    b'^', b'%', b'$', b'#', b'@', b'!', b'~',
1156                ];
1157                for c in s.bytes() {
1158                    if RAW_CHARS.contains(&c) {
1159                        dev.char(vm, c);
1160                    }
1161                }
1162            }
1163            egui::Event::Key {
1164                key,
1165                pressed,
1166                repeat,
1167                ..
1168            } => {
1169                if let Some(k) = decode_key(*key, shift_held) {
1170                    if *pressed {
1171                        dev.pressed(vm, k, *repeat);
1172                    } else {
1173                        dev.released(vm, k);
1174                    }
1175                }
1176            }
1177            egui::Event::MouseWheel { delta, .. } => {
1178                dev.mouse(
1179                    vm,
1180                    varvara::MouseState {
1181                        pos: (0.0, 0.0),
1182                        scroll: (delta.x, -delta.y),
1183                        buttons: 0,
1184                    },
1185                );
1186            }
1187            _ => (),
1188        }
1189    }
1190    for (b, k) in [
1191        (modifiers.ctrl, Key::Ctrl),
1192        (modifiers.alt, Key::Alt),
1193        (modifiers.shift, Key::Shift),
1194    ] {
1195        if b {
1196            dev.pressed(vm, k, false)
1197        } else {
1198            dev.released(vm, k)
1199        }
1200    }
1201}