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