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