From df6d13f7ff2d6c09c6b6ced3e3c28452006f0d0e Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Mon, 20 Jun 2022 23:04:52 -0700 Subject: [PATCH 1/2] Add basic stdout I/O and facades for the standard library macros - The hello-ipl3font example will initialize the IS Viewer 64 as an I/O backend if detected. - Run the example with `cen64 -is-viewer` to see the console output. --- Cargo.toml | 1 + examples/hello-ipl3font/src/main.rs | 20 +++++- examples/n64lib/Cargo.toml | 5 ++ examples/n64lib/src/io.rs | 22 +++++++ examples/n64lib/src/io/isviewer.rs | 96 +++++++++++++++++++++++++++++ examples/n64lib/src/lib.rs | 4 +- examples/n64lib/src/prelude.rs | 1 + src/io.rs | 75 ++++++++++++++++++++++ src/lib.rs | 2 + src/prelude.rs | 6 +- 10 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 examples/n64lib/src/io.rs create mode 100644 examples/n64lib/src/io/isviewer.rs create mode 100644 examples/n64lib/src/prelude.rs create mode 100644 src/io.rs diff --git a/Cargo.toml b/Cargo.toml index 741d1f4..9682948 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ edition = "2018" [dependencies] libm = "0.2" +no-stdout = "0.1" [profile.dev] panic = "abort" diff --git a/examples/hello-ipl3font/src/main.rs b/examples/hello-ipl3font/src/main.rs index ec08bcc..ac25f13 100644 --- a/examples/hello-ipl3font/src/main.rs +++ b/examples/hello-ipl3font/src/main.rs @@ -2,7 +2,7 @@ #![no_main] #![no_std] -use n64lib::{ipl3font, vi}; +use n64lib::{ipl3font, prelude::*, vi}; // Colors are 5:5:5:1 RGB with a 16-bit color depth. #[allow(clippy::unusual_byte_groupings)] @@ -10,8 +10,26 @@ const WHITE: u16 = 0b11111_11111_11111_1; #[no_mangle] fn main() { + println!("It is safe to print without initializing `stdout`, you just won't see this!"); + + let io_backend = n64lib::io::init(); + + println!("I/O initialized with {:?}", io_backend); + println!(); + + println!("Now that `stdout` has been configured..."); + eprintln!("These macros work about how you expect!"); + println!(); + println!("Supports formatting: {:#06x}", WHITE); + dbg!(WHITE); + println!(); + vi::init(); ipl3font::draw_str_centered(WHITE, "Hello, world!"); vi::swap_buffer(); + + println!("Panic also works :)"); + println!("Returning from main will panic and halt... Let's do that now!"); + println!(); } diff --git a/examples/n64lib/Cargo.toml b/examples/n64lib/Cargo.toml index 84bf82e..0e8a857 100644 --- a/examples/n64lib/Cargo.toml +++ b/examples/n64lib/Cargo.toml @@ -4,5 +4,10 @@ version = "0.1.0" authors = ["Jay Oster "] edition = "2021" +[features] +default = ["io-isviewer64"] +io-isviewer64 = [] + [dependencies] +no-stdout = "0.1" rrt0 = { path = "../../" } diff --git a/examples/n64lib/src/io.rs b/examples/n64lib/src/io.rs new file mode 100644 index 0000000..5900e53 --- /dev/null +++ b/examples/n64lib/src/io.rs @@ -0,0 +1,22 @@ +#[cfg(feature = "io-isviewer64")] +pub mod isviewer; + +/// Specify which I/O backend is automatically chosen by [`init`]. +#[derive(Debug)] +pub enum IoBackend { + /// No suitable I/O backend detected. + None, + + /// Intelligent Systems Viewer 64. + IsViewer64, +} + +/// Initialize basic I/O. +pub fn init() -> IoBackend { + #[cfg(feature = "io-isviewer64")] + if isviewer::init() { + return IoBackend::IsViewer64; + } + + IoBackend::None +} diff --git a/examples/n64lib/src/io/isviewer.rs b/examples/n64lib/src/io/isviewer.rs new file mode 100644 index 0000000..c400799 --- /dev/null +++ b/examples/n64lib/src/io/isviewer.rs @@ -0,0 +1,96 @@ +use core::{ + fmt::{self, Write}, + ptr::{read_volatile, write_volatile}, +}; +use no_stdout::StdOut; + +struct Stream; + +const IS64_MAGIC: *mut u32 = 0xB3FF_0000 as *mut u32; +const IS64_SEND: *mut u32 = 0xB3FF_0014 as *mut u32; +const IS64_BUFFER: *mut u32 = 0xB3FF_0020 as *mut u32; + +// Rough estimate based on Cen64 +const BUFFER_SIZE: usize = 0x1000 - 0x20; + +impl Write for &Stream { + fn write_str(&mut self, s: &str) -> fmt::Result { + print(s); + Ok(()) + } +} + +impl StdOut for Stream { + // Defer to the `Write` impl with a little reborrow trick. + fn write_fmt(&self, args: fmt::Arguments) -> fmt::Result { + fmt::write(&mut &*self, args)?; + Ok(()) + } + + // The rest are not required for no-stdout to operate, but they are required to build. + fn write_bytes(&self, _bytes: &[u8]) -> fmt::Result { + todo!(); + } + + fn write_str(&self, _s: &str) -> fmt::Result { + todo!(); + } + + fn flush(&self) -> fmt::Result { + todo!(); + } +} + +/// Check if Intelligent Systems Viewer 64 is available. +fn is_is64() -> bool { + let magic = u32::from_be_bytes(*b"IS64"); + + // SAFETY: It is always safe to read and write the magic value; static memory-mapped address. + unsafe { + write_volatile(IS64_MAGIC, magic); + read_volatile(IS64_MAGIC) == magic + } +} + +/// Print a string to IS Viewer 64. +/// +/// # Panics +/// +/// Asserts that the maximum string length is just under 4KB. +fn print(string: &str) { + assert!(string.len() < BUFFER_SIZE); + + let bytes = string.as_bytes(); + + // Write one word at a time + // It's ugly, but it optimizes really well! + for (i, chunk) in bytes.chunks(4).enumerate() { + let val = match *chunk { + [a, b, c, d] => (a as u32) << 24 | (b as u32) << 16 | (c as u32) << 8 | (d as u32), + [a, b, c] => (a as u32) << 24 | (b as u32) << 16 | (c as u32) << 8, + [a, b] => (a as u32) << 24 | (b as u32) << 16, + [a] => (a as u32) << 24, + _ => unreachable!(), + }; + + // SAFETY: Bounds checking has already been performed. + unsafe { write_volatile(IS64_BUFFER.add(i), val) }; + } + + // Write the string length + // SAFETY: It is always safe to write the length; static memory-mapped address. + unsafe { write_volatile(IS64_SEND, bytes.len() as u32) }; +} + +/// Initialize global I/O for IS Viewer 64. +/// +/// Returns `true` when IS Viewer 64 has been detected. +pub fn init() -> bool { + if is_is64() { + let _ = no_stdout::init(&Stream); + + return true; + } + + false +} diff --git a/examples/n64lib/src/lib.rs b/examples/n64lib/src/lib.rs index 47d5151..39646c4 100644 --- a/examples/n64lib/src/lib.rs +++ b/examples/n64lib/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] +pub mod io; pub mod ipl3font; +pub mod prelude; pub mod vi; - -pub use rrt0::prelude::*; diff --git a/examples/n64lib/src/prelude.rs b/examples/n64lib/src/prelude.rs new file mode 100644 index 0000000..9f286c7 --- /dev/null +++ b/examples/n64lib/src/prelude.rs @@ -0,0 +1 @@ +pub use rrt0::prelude::*; diff --git a/src/io.rs b/src/io.rs new file mode 100644 index 0000000..1113005 --- /dev/null +++ b/src/io.rs @@ -0,0 +1,75 @@ +/// A `no_std` implementation of [`std::dbg!`]. +/// +/// I/O must be configured by a higher-level platform crate using [`no_stdout::init`]. +/// +/// [`std::dbg!`]: https://doc.rust-lang.org/std/macro.dbg.html +#[macro_export] +macro_rules! dbg { + () => ($crate::eprintln!("[{}:{}]", file!(), line!())); + + ($arg:expr $(,)?) => {{ + // Use of `match` here is intentional because it affects the lifetimes of temporaries + // See: https://stackoverflow.com/a/48732525/1063961 + match $arg { + val => { + $crate::eprintln!("[{}:{}] {} = {:#?}", file!(), line!(), stringify!($arg), &val); + + val + } + } + }}; + + ($($arg:expr),+ $(,)?) => {($($crate::dbg!($arg),)+)}; +} + +/// A `no_std` implementation of [`std::print!`]. +/// +/// I/O must be configured by a higher-level platform crate using [`no_stdout::init`]. +/// +/// [`std::print!`]: https://doc.rust-lang.org/std/macro.print.html +#[macro_export] +macro_rules! print { + ($($args:expr),+ $(,)?) => ({ + $crate::prelude::stdout() + .write_fmt(format_args!($($args),+)) + .ok(); + }); +} + +/// A `no_std` implementation of [`std::println!`]. +/// +/// I/O must be configured by a higher-level platform crate using [`no_stdout::init`]. +/// +/// [`std::println!`]: https://doc.rust-lang.org/std/macro.println.html +#[macro_export] +macro_rules! println { + ($($args:expr),+ $(,)?) => ($crate::print!("{}\n", format_args!($($args),+))); + + () => ($crate::print!("\n")); +} + +/// A `no_std` implementation of [`std::eprint!`]. +/// +/// I/O must be configured by a higher-level platform crate using [`no_stdout::init`]. +/// +/// [`std::eprint!`]: https://doc.rust-lang.org/std/macro.eprint.html +#[macro_export] +macro_rules! eprint { + ($($args:expr),+ $(,)?) => ({ + $crate::prelude::stdout() + .write_fmt(format_args!($($args),+)) + .ok(); + }); +} + +/// A `no_std` implementation of [`std::eprintln!`]. +/// +/// I/O must be configured by a higher-level platform crate using [`no_stdout::init`]. +/// +/// [`std::eprintln!`]: https://doc.rust-lang.org/std/macro.eprintln.html +#[macro_export] +macro_rules! eprintln { + ($($args:expr),+ $(,)?) => ($crate::eprint!("{}\n", format_args!($($args),+))); + + () => ($crate::eprint!("\n")); +} diff --git a/src/lib.rs b/src/lib.rs index 1aabebd..c0bd12d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,14 @@ #![cfg_attr(target_vendor = "nintendo64", feature(asm_experimental_arch))] #![no_std] +mod io; mod math; mod platforms; pub mod prelude; pub use crate::platforms::*; +/// This will be called by entrypoint.s if the main function returns. #[no_mangle] fn panic_main() -> ! { panic!("Main cannot return"); diff --git a/src/prelude.rs b/src/prelude.rs index 58a40b5..1f466af 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,9 +1,13 @@ +pub use crate::{dbg, eprint, eprintln, print, println}; use core::panic::PanicInfo; +pub use no_stdout::stdout; /// This function is called on panic. #[cfg_attr(target_vendor = "nintendo64", panic_handler)] #[no_mangle] -fn panic(_panic_info: &PanicInfo<'_>) -> ! { +fn panic(panic_info: &PanicInfo<'_>) -> ! { + eprintln!("Application: {}", panic_info); + #[allow(clippy::empty_loop)] loop {} } From fa19fe6f71f92e61f0357e33606e77191eb8c909 Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Sun, 26 Jun 2022 08:22:37 -0700 Subject: [PATCH 2/2] Fix IS-Viewer 64 protocol - This implements the real IS-Viewer 64 protocol, not the "HomebrewStdout" protocol. --- examples/n64lib/src/io/isviewer.rs | 105 ++++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 15 deletions(-) diff --git a/examples/n64lib/src/io/isviewer.rs b/examples/n64lib/src/io/isviewer.rs index c400799..535beb4 100644 --- a/examples/n64lib/src/io/isviewer.rs +++ b/examples/n64lib/src/io/isviewer.rs @@ -1,5 +1,6 @@ use core::{ fmt::{self, Write}, + mem::size_of, ptr::{read_volatile, write_volatile}, }; use no_stdout::StdOut; @@ -7,11 +8,12 @@ use no_stdout::StdOut; struct Stream; const IS64_MAGIC: *mut u32 = 0xB3FF_0000 as *mut u32; -const IS64_SEND: *mut u32 = 0xB3FF_0014 as *mut u32; +const IS64_READ_HEAD: *mut u32 = 0xB3FF_0004 as *mut u32; +const IS64_WRITE_HEAD: *mut u32 = 0xB3FF_0014 as *mut u32; const IS64_BUFFER: *mut u32 = 0xB3FF_0020 as *mut u32; -// Rough estimate based on Cen64 -const BUFFER_SIZE: usize = 0x1000 - 0x20; +// Based on Cen64 +const BUFFER_SIZE: usize = 0x10000 - 0x20; impl Write for &Stream { fn write_str(&mut self, s: &str) -> fmt::Result { @@ -48,6 +50,9 @@ fn is_is64() -> bool { // SAFETY: It is always safe to read and write the magic value; static memory-mapped address. unsafe { write_volatile(IS64_MAGIC, magic); + write_volatile(IS64_READ_HEAD, 0); + write_volatile(IS64_WRITE_HEAD, 0); + read_volatile(IS64_MAGIC) == magic } } @@ -56,30 +61,100 @@ fn is_is64() -> bool { /// /// # Panics /// -/// Asserts that the maximum string length is just under 4KB. +/// Asserts that the maximum string length is just under 64KB. fn print(string: &str) { assert!(string.len() < BUFFER_SIZE); + // SAFETY: It is always safe to get the write head; static memory-mapped address. + let read_head = unsafe { read_volatile(IS64_READ_HEAD) } as usize; + let mut write_head = unsafe { read_volatile(IS64_WRITE_HEAD) } as usize; + + // Ensure there is enough free space in the ring buffer to store the string. + let free_space = if read_head > write_head { + read_head - write_head + } else { + BUFFER_SIZE - write_head + read_head + }; + if free_space < string.len() { + return; + } + + let word_size = size_of::(); + let mask = word_size - 1; + let bytes = string.as_bytes(); + let start = write_head & mask; + let align = (word_size - start) & mask; + let len = align.min(bytes.len()); + + if start > 0 { + // Slow path: Combine string bytes with existing word data in the buffer. + let shift = ((align - len) * 8) as u32; + let (val, data_mask) = match bytes[..len] { + [a, b, c] => ((a as u32) << 16 | (b as u32) << 8 | (c as u32), 0xff00_0000), + [a, b] => ( + ((a as u32) << 8 | (b as u32)) << shift, + 0xffff_ffff ^ (0xffff << shift), + ), + [a] => ((a as u32) << shift, 0xffff_ffff ^ (0xff << shift)), + _ => unreachable!(), + }; + + let offset = (write_head & !mask) / word_size; + + // SAFETY: Bounds checking has already been performed. + unsafe { combine(offset, data_mask, val) }; + + write_head += len; + } + + // Get the string remainder, this aligns the output buffer to a word boundary. + // It may be an empty slice. + let bytes = &bytes[len..]; // Write one word at a time // It's ugly, but it optimizes really well! - for (i, chunk) in bytes.chunks(4).enumerate() { - let val = match *chunk { - [a, b, c, d] => (a as u32) << 24 | (b as u32) << 16 | (c as u32) << 8 | (d as u32), - [a, b, c] => (a as u32) << 24 | (b as u32) << 16 | (c as u32) << 8, - [a, b] => (a as u32) << 24 | (b as u32) << 16, - [a] => (a as u32) << 24, - _ => unreachable!(), + for (i, chunk) in bytes.chunks(word_size).enumerate() { + let (val, data_mask) = match *chunk { + [a, b, c, d] => ( + (a as u32) << 24 | (b as u32) << 16 | (c as u32) << 8 | (d as u32), + 0x0000_0000, + ), + [a, b, c] => ( + (a as u32) << 24 | (b as u32) << 16 | (c as u32) << 8, + 0x0000_00ff, + ), + [a, b] => ((a as u32) << 24 | (b as u32) << 16, 0x0000_ffff), + [a] => ((a as u32) << 24, 0x00ff_ffff), + _ => break, }; - // SAFETY: Bounds checking has already been performed. - unsafe { write_volatile(IS64_BUFFER.add(i), val) }; + let offset = (write_head / word_size + i) % (BUFFER_SIZE / word_size); + + // Combine existing word data in the buffer when writing an incomplete word. + if chunk.len() < word_size { + // SAFETY: Bounds checking has already been performed. + unsafe { combine(offset, data_mask, val) }; + } else { + // SAFETY: Bounds checking has already been performed. + unsafe { write_volatile(IS64_BUFFER.add(offset), val) }; + } } // Write the string length - // SAFETY: It is always safe to write the length; static memory-mapped address. - unsafe { write_volatile(IS64_SEND, bytes.len() as u32) }; + let write_head = ((write_head + bytes.len()) % BUFFER_SIZE) as u32; + // SAFETY: It is always safe to update the write head; static memory-mapped address. + unsafe { write_volatile(IS64_WRITE_HEAD, write_head) }; +} + +/// Combine a word value with the existing word data at the given offset. +/// +/// # Safety +/// +/// The caller is responsible for bounds checking the offset. +unsafe fn combine(offset: usize, mask: u32, val: u32) { + let word = read_volatile(IS64_BUFFER.add(offset)) & mask; + write_volatile(IS64_BUFFER.add(offset), word | val); } /// Initialize global I/O for IS Viewer 64.