From df6d13f7ff2d6c09c6b6ced3e3c28452006f0d0e Mon Sep 17 00:00:00 2001
From: Jay Oster <jay@kodewerx.org>
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 <jay@kodewerx.org>"]
 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 <jay@kodewerx.org>
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::<u32>();
+    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.