Skip to main content

cardinal_varvara/
lib.rs

1//! The Varvara computer system
2#![warn(missing_docs)]
3#[cfg(feature = "uses_gilrs")]
4use crate::controller_gilrs::ControllerGilrs;
5use log::warn;
6use std::collections::HashMap;
7use std::fs::File;
8use std::io::{self, Read};
9use std::{
10    io::Write,
11    sync::{Arc, Mutex},
12};
13/// Audio handler implementation
14mod audio;
15mod console;
16/// Controller device and input handling for the Varvara system.
17pub mod controller;
18mod controller_device;
19/// Gilrs controller device support for the Varvara system (enabled with the `uses_gilrs` feature).
20#[cfg(feature = "uses_gilrs")]
21pub mod controller_gilrs;
22/// USB controller device support for the Varvara system (enabled with the `uses_usb` feature).
23#[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
24pub mod controller_usb;
25
26#[cfg(any(not(feature = "uses_usb"), target_arch = "wasm32"))]
27#[path = "controller_usb_stub.rs"]
28pub mod controller_usb;
29
30mod datetime;
31mod file;
32mod mouse;
33mod screen;
34mod system;
35mod tracker;
36
37pub use audio::set_sample_rate;
38pub use audio::StreamData;
39pub use audio::CHANNELS as AUDIO_CHANNELS;
40pub use console::spawn_worker as spawn_console_worker;
41pub use controller::Key;
42pub use mouse::MouseState;
43pub use tracker::TrackerState;
44
45use uxn::{Device, Ports, Uxn};
46
47/// Holds ROM data and optional symbol information for Uxn.
48#[derive(Clone)]
49pub struct RomData {
50    /// The ROM data as a vector of bytes.
51    pub rom: Vec<u8>,
52    /// Optional symbol information as a vector of bytes.
53    pub sym: Option<Vec<u8>>,
54}
55
56/// Write to execute before calling the event vector
57#[derive(Copy, Clone, Debug)]
58pub struct EventData {
59    /// The device address associated with the event.
60    pub addr: u8,
61    /// The value associated with the event (e.g., key code or pedal state).
62    pub value: u8,
63    /// Whether to clear the event after handling.
64    pub clear: bool,
65}
66
67/// Internal events, accumulated by devices then applied to the CPU
68#[derive(Copy, Clone, Debug, Default)]
69pub struct Event {
70    /// Tuple of `(address, value)` to write in in device memory
71    pub data: Option<EventData>,
72
73    /// Vector to trigger
74    pub vector: u16,
75}
76
77/// Output from Varvara::update, which may modify the GUI
78pub struct Output<'a> {
79    /// Current window size
80    pub size: (u16, u16),
81
82    /// Current screen contents, as RGBA values
83    pub frame: &'a [u8],
84
85    /// The system's mouse cursor should be hidden
86    pub hide_mouse: bool,
87
88    /// Outgoing console characters sent to the `write` port
89    pub stdout: Vec<u8>,
90
91    /// Outgoing console characters sent to the `error` port
92    pub stderr: Vec<u8>,
93
94    /// Request to exit with the given error code
95    pub exit: Option<i32>,
96}
97
98impl Output<'_> {
99    /// Prints `stdout` and `stderr` to the console
100    pub fn print(&self) -> std::io::Result<()> {
101        if !self.stdout.is_empty() {
102            let mut stdout = std::io::stdout().lock();
103            stdout.write_all(&self.stdout)?;
104            stdout.flush()?;
105        }
106        if !self.stderr.is_empty() {
107            let mut stderr = std::io::stderr().lock();
108            stderr.write_all(&self.stderr)?;
109            stderr.flush()?;
110        }
111        Ok(())
112    }
113
114    /// Checks the results
115    ///
116    /// `stdout` and `stderr` are printed, and `exit(..)` is called if it has
117    /// been requested by the VM.
118    pub fn check(&self) -> std::io::Result<()> {
119        self.print()?;
120        if let Some(e) = self.exit {
121            log::info!("requested exit ({e})");
122
123            #[cfg(not(target_arch = "wasm32"))]
124            std::process::exit(e);
125
126            #[cfg(target_arch = "wasm32")]
127            return Err(std::io::Error::other("exit requested"));
128        }
129        Ok(())
130    }
131}
132
133/// Handle to the Varvara system
134pub struct Varvara {
135    /// System device (timers, exit, etc)
136    pub system: system::System,
137    /// Console device (stdout, stderr, args)
138    pub console: console::Console,
139    /// Datetime device (clock, date)
140    pub datetime: datetime::Datetime,
141    /// Audio device (sound output)
142    pub audio: audio::Audio,
143    /// Screen device (framebuffer, size)
144    pub screen: screen::Screen,
145    /// Mouse device (position, buttons, scroll)
146    pub mouse: mouse::Mouse,
147    /// File device (file I/O)
148    pub file: file::File,
149    /// Controller device (keyboard input)
150    pub controller: Box<dyn controller::ControllerDevice>,
151    /// Tracker device (position, buttons, scroll)
152    pub tracker: tracker::Tracker,
153    /// Flags indicating if we've already printed a warning about a missing dev
154    pub already_warned: [bool; 16],
155    /// Use USB controller (only present if feature = "uses_usb")
156    #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
157    pub uses_usb: bool,
158    /// Optional symbol map for vector labels
159    pub symbols: Option<HashMap<u16, String>>,
160    /// Last processed vector for deduplication
161    pub last_vector: u16,
162}
163
164impl Default for Varvara {
165    fn default() -> Self {
166        #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
167        {
168            let mut v = Self::new(true);
169            v.symbols = None;
170            v.last_vector = 0;
171            v
172        }
173        #[cfg(any(not(feature = "uses_usb"), target_arch = "wasm32"))]
174        {
175            let mut v = Self::new();
176            v.symbols = None;
177            v.last_vector = 0;
178            v
179        }
180    }
181}
182
183impl Device for Varvara {
184    fn deo(&mut self, vm: &mut Uxn, target: u8) -> bool {
185        match target & 0xF0 {
186            system::SystemPorts::BASE => self.system.deo(vm, target),
187            console::ConsolePorts::BASE => self.console.deo(vm, target),
188            datetime::DatetimePorts::BASE => self.datetime.deo(vm, target),
189            screen::ScreenPorts::BASE => self.screen.deo(vm, target),
190            mouse::MousePorts::BASE => self.mouse.set_active(),
191            tracker::TrackerPorts::BASE => self.tracker.set_active(),
192            f if file::FilePorts::matches(f) => self.file.deo(vm, target),
193            controller::ControllerPorts::BASE => (),
194            a if audio::AudioPorts::matches(a) => self.audio.deo(vm, target),
195
196            // Default case
197            t => self.warn_missing(t),
198        }
199        !self.system.should_exit()
200    }
201    fn dei(&mut self, vm: &mut Uxn, target: u8) {
202        match target & 0xF0 {
203            system::SystemPorts::BASE => self.system.dei(vm, target),
204            console::ConsolePorts::BASE => self.console.dei(vm, target),
205            datetime::DatetimePorts::BASE => self.datetime.dei(vm, target),
206            screen::ScreenPorts::BASE => self.screen.dei(vm, target),
207            mouse::MousePorts::BASE => self.mouse.set_active(),
208            tracker::TrackerPorts::BASE => self.tracker.set_active(),
209            f if file::FilePorts::matches(f) => (),
210            controller::ControllerPorts::BASE => (),
211            a if audio::AudioPorts::matches(a) => self.audio.dei(vm, target),
212
213            // Default case
214            t => self.warn_missing(t),
215        }
216    }
217}
218
219impl Varvara {
220    /// Loads a .sym file into the Varvara instance
221    pub fn load_symbols_into_self(&mut self, path: &str) -> std::io::Result<()> {
222        let map = Self::load_symbols(path)?;
223        self.symbols = Some(map);
224        Ok(())
225    }
226
227    /// Load a ROM from a file path and attempt to load a .sys symbol file if present
228    pub fn load_sym_with_rom_path<P: AsRef<std::path::Path>>(&mut self, path: P) {
229        let path = path.as_ref();
230        println!("[DEBUG][VARVARA] Loading symbols from {path:?}");
231        if path.exists() {
232            println!("[DEBUG][VARVARA] Found ROM at {path:?}");
233            // Look for a .sys file with the same file name as the ROM, e.g. orca.rom -> orca.rom.sys
234            if let Some(file_name) = path.file_name() {
235                use std::ffi::OsString;
236                use std::path::PathBuf;
237                let mut sys_file = OsString::from(file_name);
238                sys_file.push(".sym");
239                let mut sys_path = PathBuf::from(path);
240                sys_path.set_file_name(sys_file);
241                #[cfg(windows)]
242                let mut sys_path_str = sys_path.to_string_lossy().to_string();
243                #[cfg(windows)]
244                {
245                    sys_path_str = sys_path_str.replace("/", "\\");
246                }
247                #[cfg(not(windows))]
248                let sys_path_str = sys_path.to_string_lossy().to_string();
249                println!("[DEBUG][VARVARA] Attempting to load symbols from {sys_path_str}");
250                if sys_path.exists() {
251                    let _ = self.load_symbols_into_self(&sys_path_str);
252                    println!("[DEBUG][VARVARA] Loaded symbols from {sys_path_str}");
253                }
254            }
255        }
256    }
257
258    /// Loads a .sym file from a byte vector and returns a map of address -> label
259    pub fn load_symbols_from_vec(&mut self, data: &[u8]) -> io::Result<HashMap<u16, String>> {
260        let map = Self::parse_symbols_from_bytes(data)?;
261        self.symbols = Some(map.clone());
262        Ok(map)
263    }
264    /// Returns a mutable reference to the USB controller, if it exists.
265    #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
266    pub fn controller_usb_mut(&mut self) -> Option<&mut controller_usb::ControllerUsb> {
267        self.controller
268            .as_mut()
269            .as_any()
270            .downcast_mut::<controller_usb::ControllerUsb>()
271    }
272
273    /// Looks up a label for a given vector address
274    pub fn vector_to_label(&self, vector: u16) -> &str {
275        if let Some(ref map) = self.symbols {
276            map.get(&vector).map(|s| s.as_str()).unwrap_or("unknown")
277        } else {
278            "unknown"
279        }
280    }
281    /// Builds a new instance of the Varvara peripherals
282    #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
283    pub fn new(uses_usb: bool) -> Self {
284        log::info!("[Varvara::new] (USB) uses_usb={}", uses_usb);
285        let controller: Box<dyn controller::ControllerDevice> = if uses_usb {
286            #[cfg(all(feature = "uses_gilrs", not(target_arch = "wasm32")))]
287            {
288                log::info!("[Varvara::new] USB+gilrs: constructing ControllerUsb with gilrs");
289                Box::new(controller_usb::ControllerUsb {
290                    rx: controller_usb::spawn_usb_controller_thread(
291                        controller_usb::UsbDeviceConfig::default(),
292                    ),
293                    last_pedal: None,
294                    controller: controller::Controller::default(),
295                    gilrs: Some(ControllerGilrs::new(controller::Controller::default())),
296                })
297            }
298            #[cfg(all(not(feature = "uses_gilrs"), not(target_arch = "wasm32")))]
299            {
300                log::info!("[Varvara::new] USB: constructing ControllerUsb without gilrs");
301                Box::new(controller_usb::ControllerUsb {
302                    rx: controller_usb::spawn_usb_controller_thread(
303                        controller_usb::UsbDeviceConfig::default(),
304                    ),
305                    last_pedal: None,
306                    controller: controller::Controller::default(),
307                })
308            }
309            #[cfg(target_arch = "wasm32")]
310            {
311                log::info!("[Varvara::new] USB: WASM stub controller");
312                Box::new(controller::Controller::default())
313            }
314        } else {
315            #[cfg(all(feature = "uses_gilrs", not(target_arch = "wasm32")))]
316            {
317                log::info!("[Varvara::new] non-USB+gilrs: constructing ControllerGilrs");
318                use crate::controller_gilrs::ControllerGilrs;
319                Box::new(ControllerGilrs::new(controller::Controller::default()))
320            }
321            #[cfg(any(not(feature = "uses_gilrs"), target_arch = "wasm32"))]
322            {
323                log::info!("[Varvara::new] non-USB: WASM stub controller");
324                Box::new(controller::Controller::default())
325            }
326        };
327        Self {
328            console: console::Console::new(),
329            system: system::System::new(),
330            datetime: datetime::Datetime,
331            audio: audio::Audio::new(),
332            screen: screen::Screen::new(),
333            mouse: mouse::Mouse::new(),
334            file: file::File::new(),
335            controller,
336            tracker: tracker::Tracker::new(),
337            already_warned: [false; 16],
338            uses_usb,
339            symbols: None,
340            last_vector: 0,
341        }
342    }
343
344    /// Builds a new instance of the Varvara peripherals (non-USB/wasm version).
345    #[cfg(any(not(feature = "uses_usb"), target_arch = "wasm32"))]
346    pub fn new() -> Self {
347        // #[cfg(all(feature = "uses_gilrs", not(target_arch = "wasm32")))]
348        // {
349        //     #[allow(unused_imports)]
350        //     use controller_gilrs::ControllerGilrs;
351        // }
352        log::info!("[Varvara::new] (non-USB/wasm) constructing");
353        Self {
354            console: console::Console::new(),
355            system: system::System::new(),
356            datetime: datetime::Datetime,
357            audio: audio::Audio::new(),
358            screen: screen::Screen::new(),
359            mouse: mouse::Mouse::new(),
360            file: file::File::new(),
361            controller: {
362                #[cfg(all(feature = "uses_gilrs", not(target_arch = "wasm32")))]
363                {
364                    log::info!("[Varvara::new] (non-USB/wasm) ControllerGilrs");
365                    Box::new(ControllerGilrs::new(controller::Controller::default()))
366                }
367                #[cfg(any(not(feature = "uses_gilrs"), target_arch = "wasm32"))]
368                {
369                    log::info!("[Varvara::new] (non-USB/wasm) WASM stub controller");
370                    Box::new(controller::Controller::default())
371                }
372            },
373            tracker: tracker::Tracker::new(),
374            already_warned: [false; 16],
375            symbols: None,
376            last_vector: 0,
377        }
378    }
379
380    /// Resets the CPU, loading extra data into expansion memory
381    #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
382    pub fn reset(&mut self, extra: &[u8]) {
383        self.system.reset(extra);
384        self.console = console::Console::new();
385        self.audio.reset();
386        self.screen = screen::Screen::new();
387        self.mouse = mouse::Mouse::new();
388        self.file = file::File::new();
389
390        self.controller = if self.uses_usb {
391            #[cfg(all(
392                feature = "uses_usb",
393                not(target_arch = "wasm32"),
394                feature = "uses_gilrs"
395            ))]
396            {
397                Box::new(controller_usb::ControllerUsb {
398                    rx: controller_usb::spawn_usb_controller_thread(
399                        controller_usb::UsbDeviceConfig::default(),
400                    ),
401                    last_pedal: None,
402                    controller: controller::Controller::default(),
403                    gilrs: Some(ControllerGilrs::new(controller::Controller::default())),
404                })
405            }
406            #[cfg(all(
407                feature = "uses_usb",
408                not(target_arch = "wasm32"),
409                not(feature = "uses_gilrs")
410            ))]
411            {
412                Box::new(controller_usb::ControllerUsb {
413                    rx: controller_usb::spawn_usb_controller_thread(
414                        controller_usb::UsbDeviceConfig::default(),
415                    ),
416                    last_pedal: None,
417                    controller: controller::Controller::default(),
418                })
419            }
420            #[cfg(target_arch = "wasm32")]
421            {
422                // Always use stub controller for WASM
423                Box::new(controller::Controller::default())
424            }
425        } else {
426            #[cfg(all(feature = "uses_gilrs", not(target_arch = "wasm32")))]
427            {
428                Box::new(ControllerGilrs::new(controller::Controller::default()))
429            }
430            #[cfg(any(not(feature = "uses_gilrs"), target_arch = "wasm32"))]
431            {
432                // Always use stub controller for WASM
433                Box::new(controller::Controller::default())
434            }
435        };
436        self.tracker = tracker::Tracker::new();
437        self.already_warned.fill(false);
438    }
439
440    /// Resets the CPU, loading extra data into expansion memory.
441    #[cfg(any(not(feature = "uses_usb"), target_arch = "wasm32"))]
442    pub fn reset(&mut self, extra: &[u8]) {
443        self.system.reset(extra);
444        self.console = console::Console::new();
445        self.audio.reset();
446        self.screen = screen::Screen::new();
447        self.mouse = mouse::Mouse::new();
448        self.file = file::File::new();
449
450        self.controller = {
451            #[cfg(feature = "uses_gilrs")]
452            {
453                Box::new(ControllerGilrs::new(controller::Controller::default()))
454            }
455            #[cfg(not(feature = "uses_gilrs"))]
456            {
457                Box::new(controller::Controller::default())
458            }
459        };
460        self.tracker = tracker::Tracker::new();
461        self.already_warned.fill(false);
462    }
463
464    /// Checks whether the SHIFT key is currently down
465    fn warn_missing(&mut self, t: u8) {
466        if !self.already_warned[usize::from(t >> 4)] {
467            warn!("unimplemented device {t:#02x}");
468            self.already_warned[usize::from(t >> 4)] = true;
469        }
470    }
471
472    /// Calls the screen vector
473    ///
474    /// This function must be called at 60 Hz
475    pub fn redraw(&mut self, vm: &mut Uxn) {
476        let e = self.screen.update(vm);
477        self.process_event(vm, e);
478    }
479
480    /// Sets initial value for `Console/type` based on the presense of arguments
481    ///
482    /// This should be called before running the reset vector
483    pub fn init_args(&mut self, vm: &mut Uxn, args: &[String]) {
484        self.console.set_has_args(vm, !args.is_empty());
485    }
486
487    /// Returns the current output state of the system
488    ///
489    /// This is not idempotent; the output is taken from various accumulators
490    /// and will be empty if this is called multiple times.
491    #[must_use]
492    pub fn output(&mut self, vm: &Uxn) -> Output<'_> {
493        Output {
494            size: self.screen.size(),
495            frame: self.screen.frame(vm),
496            hide_mouse: self.mouse.active(),
497            stdout: self.console.stdout(),
498            stderr: self.console.stderr(),
499            exit: self.system.exit(),
500        }
501    }
502
503    /// Sends arguments to the console device
504    ///
505    /// Leaves the console type set to `stdin`, and returns the current output
506    /// state of the system
507    pub fn send_args(&mut self, vm: &mut Uxn, args: &[String]) -> Output<'_> {
508        for (i, a) in args.iter().enumerate() {
509            self.console.set_type(vm, console::Type::Argument);
510            for c in a.bytes() {
511                self.process_event(vm, self.console.update(vm, c));
512            }
513
514            let ty = if i == args.len() - 1 {
515                console::Type::ArgumentEnd
516            } else {
517                console::Type::ArgumentSpacer
518            };
519            self.console.set_type(vm, ty);
520            self.process_event(vm, self.console.update(vm, b'\n'));
521        }
522        self.console.set_type(vm, console::Type::Stdin);
523        self.output(vm)
524    }
525
526    /// Send a character from the keyboard (controller) device
527    /// Send a character from the keyboard (controller) device
528    pub fn char(&mut self, vm: &mut Uxn, k: u8) {
529        println!("Sending character: {k}");
530        let e = self.controller.char(vm, k);
531        println!("Processing event: {e:?}");
532        self.process_event(vm, e);
533    }
534
535    /// Press a key on the controller device
536    /// Press a key on the controller device
537    pub fn pressed(&mut self, vm: &mut Uxn, k: Key, repeat: bool) {
538        // Only send pressed events for non-character keys
539        println!("Pressed key: {k:?}");
540        if let Key::Char(k) = k {
541            // Do nothing, character keys are handled by char()
542            self.char(vm, k);
543        } else if let Some(e) = self.controller.pressed(vm, k, repeat) {
544            println!("Processing event: {e:?}");
545            self.process_event(vm, e);
546        }
547    }
548
549    /// Release a key on the controller device
550    pub fn released(&mut self, vm: &mut Uxn, k: Key) {
551        if let Some(e) = self.controller.released(vm, k) {
552            self.process_event(vm, e);
553        }
554    }
555
556    /// Send a character from the console device
557    pub fn console(&mut self, vm: &mut Uxn, c: u8) {
558        let e = self.console.update(vm, c);
559        self.process_event(vm, e);
560    }
561
562    /// Updates the mouse state
563    pub fn mouse(&mut self, vm: &mut Uxn, m: MouseState) {
564        if let Some(e) = self.mouse.update(vm, m) {
565            self.process_event(vm, e);
566        }
567    }
568
569    /// Processes pending audio events
570    pub fn audio(&mut self, vm: &mut Uxn) {
571        for i in 0..audio::DEV_COUNT {
572            if let Some(e) = self.audio.update(vm, usize::from(i)) {
573                self.process_event(vm, e);
574            }
575        }
576    }
577
578    /// Updates the tracker state
579    pub fn tracker(&mut self, vm: &mut Uxn, m: tracker::TrackerState) {
580        if let Some(e) = self.tracker.update(vm, m) {
581            self.process_event(vm, e);
582        }
583    }
584
585    /// Processes a single vector event
586    ///
587    /// Events with an unassigned vector (i.e. 0) are ignored
588    pub fn process_event(&mut self, vm: &mut Uxn, e: Event) {
589        if e.vector != 0 {
590            let label = self.vector_to_label(e.vector);
591            let skip_labels = ["timer/on-play", "on-mouse"];
592            let skip_print = skip_labels.contains(&label);
593
594            if self.last_vector != e.vector && !skip_print {
595                println!(
596                    "[VARVARA][process_event] vector: 0x{:04x} [{}], data: {:?}",
597                    e.vector, label, e.data
598                );
599                self.last_vector = e.vector;
600            }
601
602            if let Some(d) = e.data {
603                if !skip_print {
604                    println!("[VARVARA][process_event] write_dev_mem addr: 0x{:02x}, value: 0x{:02x} ('{}')", d.addr, d.value, d.value as char);
605                }
606                vm.write_dev_mem(d.addr, d.value);
607            }
608            vm.run(self, e.vector);
609            if let Some(d) = e.data {
610                if d.clear {
611                    if !skip_print {
612                        println!("[VARVARA][process_event] clear addr: 0x{:02x}", d.addr);
613                    }
614                    vm.write_dev_mem(d.addr, 0);
615                }
616            }
617        }
618    }
619
620    /// Returns the set of audio stream data handles
621    pub fn audio_streams(&self) -> [Arc<Mutex<audio::StreamData>>; 4] {
622        [0, 1, 2, 3].map(|i| self.audio.stream(i))
623    }
624
625    /// Sets the global mute flag for audio
626    pub fn audio_set_muted(&mut self, m: bool) {
627        self.audio.set_muted(m)
628    }
629
630    /// Loads a .sym file and returns a map of address -> label
631    pub fn load_symbols(path: &str) -> io::Result<HashMap<u16, String>> {
632        let mut file = File::open(path)?;
633        let mut buf = Vec::new();
634        file.read_to_end(&mut buf)?;
635        println!(
636            "[DEBUG] Loaded symbols from {path:?}, length: {}",
637            buf.len()
638        );
639        let mut map = HashMap::new();
640        let mut i = 0;
641        while i + 2 < buf.len() {
642            let addr = ((buf[i] as u16) << 8) | (buf[i + 1] as u16);
643            i += 2;
644            let mut end = i;
645            while end < buf.len() && buf[end] != 0 {
646                end += 1;
647            }
648            let label = String::from_utf8_lossy(&buf[i..end]).to_string();
649            map.insert(addr, label);
650            i = end + 1;
651        }
652        Ok(map)
653    }
654
655    /// Parse a .sym file from a byte slice and return a map of address -> label
656    pub fn parse_symbols_from_bytes(
657        data: &[u8],
658    ) -> std::io::Result<std::collections::HashMap<u16, String>> {
659        let mut map = std::collections::HashMap::new();
660        let mut i = 0;
661        while i + 2 < data.len() {
662            let addr = ((data[i] as u16) << 8) | (data[i + 1] as u16);
663            i += 2;
664            let mut end = i;
665            while end < data.len() && data[end] != 0 {
666                end += 1;
667            }
668            let label = String::from_utf8_lossy(&data[i..end]).to_string();
669            map.insert(addr, label);
670            i = end + 1;
671        }
672        Ok(map)
673    }
674}