Skip to main content

gb_hram/
lib.rs

1#![no_std]
2
3//! Typed handles to the Game Boy's High RAM (HRAM, `0xFF80..=0xFFFE`).
4//!
5//! HRAM is a 127-byte region the CPU can still reach while an OAM DMA holds the
6//! bus, and the `ldh` instructions address it in a single byte. Cells declared
7//! with the [`hram!`] macro are read and written with the **immediate** `ldh (n)`
8//! form (2 bytes / 3 cycles): the linker assigns each cell a fixed HRAM address
9//! and the low byte is baked into the instruction. That is faster and smaller
10//! than the 3-byte / 4-cycle absolute `ld (nn)` used for WRAM.
11//!
12//! # Which kind to declare
13//!
14//! An access one byte wide is a single instruction, and the CPU takes interrupts
15//! only between instructions, so it cannot be observed half done. A wider access
16//! is several instructions and an interrupt landing in the middle leaves the
17//! reader with a value that never existed. The three kinds differ in what they do
18//! about that.
19//!
20//! | Kind | Width | Access |
21//! |------|-------|--------|
22//! | [`HramAtomicCell<T>`] | one byte | `get()` / `set()` |
23//! | [`HramCell<T>`] | up to [`MAX_BYTES`] | `get(cs)` / `set(cs, v)` |
24//! | [`HramArea<N>`] | any | raw pointers |
25//!
26//! [`HramCell`] takes a [`CriticalSection`] because nothing else can make a
27//! multi-instruction access indivisible on this CPU. Obtaining the token is the
28//! caller's business; a cell reached only from the main loop, never from an
29//! interrupt handler, still needs one.
30//!
31//! Both cells hold a [`CellValue`], which under the `bank-safe` feature also has to
32//! survive a bank switch.
33//!
34//! # Examples
35//!
36//! ```ignore
37//! #![feature(asm_experimental_arch)]
38//! use gb_hram::hram;
39//!
40//! hram! {
41//!     /// Frames elapsed, bumped by the VBlank handler.
42//!     pub static FRAME: HramAtomicCell<u8>;
43//!     static SCROLL: HramCell<ScrollState>;
44//!     static OAM_DMA: HramArea<13>;
45//! }
46//!
47//! fn tick() {
48//!     FRAME.set(FRAME.get().wrapping_add(1));   // ldh a,(n) / ldh (n),a
49//! }
50//! ```
51//!
52//! Write `static NAME as "symbol": ...;` to export the storage under a fixed
53//! symbol, to share a cell with C or assembly. The symbol is emitted with the
54//! target's usual prefix.
55//!
56//! The accessors emit `ldh` inline asm in the calling crate, so that crate needs
57//! `#![feature(asm_experimental_arch)]`.
58//!
59//! # Zero initialisation
60//!
61//! HRAM is `NOLOAD`, so nothing is loaded from ROM, and a conforming runtime is
62//! required to clear `0xFF80..=0xFFFE` before `main`. A cell whose type has a
63//! valid all-zero bit pattern therefore starts at `0` and may be read before it
64//! is first written; any other type must be written first.
65//!
66//! # Feature flags
67#![doc = document_features::document_features!()]
68
69use core::cell::UnsafeCell;
70use core::mem::MaybeUninit;
71
72pub use critical_section::CriticalSection;
73
74/// What an HRAM cell may hold.
75///
76/// With the `bank-safe` feature a value must also be
77/// [`BankSafe`](gb_bank_safe::BankSafe): a cell outlives any bank switch, so a
78/// pointer to banked code left in one would dangle.
79#[cfg(feature = "bank-safe")]
80pub trait CellValue: Copy + gb_bank_safe::BankSafe {}
81#[cfg(feature = "bank-safe")]
82impl<T: Copy + gb_bank_safe::BankSafe> CellValue for T {}
83
84/// What an HRAM cell may hold. See the `bank-safe` feature.
85#[cfg(not(feature = "bank-safe"))]
86pub trait CellValue: Copy {}
87#[cfg(not(feature = "bank-safe"))]
88impl<T: Copy> CellValue for T {}
89
90/// Read and write an HRAM cell under a [`CriticalSection`].
91///
92/// Implemented by every handle the [`hram!`] macro generates. An
93/// [`HramAtomicCell`] handle implements it too, so code holding a token can
94/// reach either kind through one interface.
95pub trait HramAccess {
96    /// The stored type.
97    type Value: CellValue;
98
99    /// Read the cell.
100    ///
101    /// HRAM starts zeroed, so a `Value` whose all-zero bit pattern is valid may
102    /// be read before its first write.
103    fn get_cs(&self, cs: CriticalSection<'_>) -> Self::Value;
104
105    /// Write the cell.
106    fn set_cs(&self, cs: CriticalSection<'_>, value: Self::Value);
107
108    /// The cell's address, for raw access or to feed an `ldh`-based routine.
109    fn as_ptr(&self) -> *mut Self::Value;
110}
111
112/// Read and write a one-byte HRAM cell without a [`CriticalSection`].
113///
114/// Implemented by the handles for [`HramAtomicCell`] declarations. A one-byte
115/// `ldh` is a single instruction, so no token is needed to make it indivisible.
116pub trait HramAtomicAccess: HramAccess {
117    /// Read the cell.
118    fn get(&self) -> Self::Value;
119
120    /// Write the cell.
121    fn set(&self, value: Self::Value);
122}
123
124/// Common imports for HRAM access (`use gb_hram::prelude::*`).
125pub mod prelude {
126    pub use crate::{HramAccess, HramAtomicAccess};
127}
128
129/// The widest [`HramCell`] value an access unrolls into straight-line `ldh`s.
130/// HRAM is only 127 bytes, so cells are small; a wider type is a compile error.
131pub const MAX_BYTES: usize = 8;
132
133/// Storage for a one-byte cell, accessed without a [`CriticalSection`].
134///
135/// Declare it with [`hram!`]; the accessors live on the handle that macro
136/// generates, not on this type. A wider type is a compile error.
137#[repr(transparent)]
138pub struct HramAtomicCell<T: CellValue>(UnsafeCell<MaybeUninit<T>>);
139
140// The Game Boy is single-core, so there is no cross-thread aliasing, and a
141// one-byte access cannot tear against an interrupt handler.
142unsafe impl<T: CellValue> Sync for HramAtomicCell<T> {}
143
144impl<T: CellValue> HramAtomicCell<T> {
145    /// Reserve a cell.
146    ///
147    /// # Safety
148    ///
149    /// The cell must come to rest at an address in `0xFF80..=0xFFFE`. [`hram!`]
150    /// guarantees this; a hand-placed cell needs `#[link_section = "_HRAM.*"]`.
151    pub const unsafe fn uninit() -> Self {
152        Self(UnsafeCell::new(MaybeUninit::uninit()))
153    }
154
155    /// The cell's address as a raw pointer.
156    pub const fn ptr(&self) -> *mut T {
157        self.0.get() as *mut T
158    }
159}
160
161/// Storage for a cell read and written under a [`CriticalSection`].
162///
163/// Declare it with [`hram!`]; the accessors live on the handle that macro
164/// generates, not on this type. An access wider than one byte is several `ldh`
165/// instructions, and the token is the proof that no interrupt lands between them.
166#[repr(transparent)]
167pub struct HramCell<T: CellValue>(UnsafeCell<MaybeUninit<T>>);
168
169unsafe impl<T: CellValue> Sync for HramCell<T> {}
170
171impl<T: CellValue> HramCell<T> {
172    /// Reserve a cell.
173    ///
174    /// # Safety
175    ///
176    /// The cell must come to rest at an address in `0xFF80..=0xFFFE`. [`hram!`]
177    /// guarantees this; a hand-placed cell needs `#[link_section = "_HRAM.*"]`.
178    pub const unsafe fn uninit() -> Self {
179        Self(UnsafeCell::new(MaybeUninit::uninit()))
180    }
181
182    /// The cell's address as a raw pointer.
183    pub const fn ptr(&self) -> *mut T {
184        self.0.get() as *mut T
185    }
186}
187
188/// Internal: unroll immediate `ldh a, (STORAGE+i)` loads. Not public API.
189#[doc(hidden)]
190#[macro_export]
191macro_rules! __hram_imm_load {
192    ($storage:path, $dst:ident, $ty:ty, $($i:literal),*) => {$(
193        if $i < ::core::mem::size_of::<$ty>() {
194            let byte: u8;
195            ::core::arch::asm!(
196                ::core::concat!("ldh a, ({s} + ", $i, ")"),
197                s = sym $storage,
198                out("a") byte,
199                options(nostack, preserves_flags, readonly),
200            );
201            $dst.add($i).write(byte);
202        }
203    )*};
204}
205
206/// Internal: unroll immediate `ldh (STORAGE+i), a` stores. Not public API.
207#[doc(hidden)]
208#[macro_export]
209macro_rules! __hram_imm_store {
210    ($storage:path, $src:ident, $ty:ty, $($i:literal),*) => {$(
211        if $i < ::core::mem::size_of::<$ty>() {
212            ::core::arch::asm!(
213                ::core::concat!("ldh ({s} + ", $i, "), a"),
214                s = sym $storage,
215                in("a") $src.add($i).read(),
216                options(nostack, preserves_flags),
217            );
218        }
219    )*};
220}
221
222/// A raw, fixed-size region of High RAM, addressed through pointers.
223///
224/// The home for a routine that must execute from HRAM (an OAM DMA trampoline) or
225/// a scratch buffer. Declare it with [`hram!`]. HRAM is `NOLOAD`, so the runtime
226/// zero-initialises it at startup; fill it at runtime.
227#[repr(transparent)]
228pub struct HramArea<const N: usize>(UnsafeCell<MaybeUninit<[u8; N]>>);
229
230unsafe impl<const N: usize> Sync for HramArea<N> {}
231
232impl<const N: usize> HramArea<N> {
233    /// Reserve an area.
234    ///
235    /// # Safety
236    ///
237    /// The area must come to rest in High RAM (`0xFF80..=0xFFFE`); [`hram!`]
238    /// guarantees this.
239    pub const unsafe fn uninit() -> Self {
240        Self(UnsafeCell::new(MaybeUninit::uninit()))
241    }
242
243    /// A pointer to the start of the area.
244    pub const fn as_ptr(&self) -> *const u8 {
245        self.0.get() as *const u8
246    }
247
248    /// A mutable pointer to the start of the area.
249    pub const fn as_mut_ptr(&self) -> *mut u8 {
250        self.0.get() as *mut u8
251    }
252
253    /// A mutable pointer to the area as a fixed-size byte array.
254    pub const fn as_array_ptr(&self) -> *mut [u8; N] {
255        self.0.get() as *mut [u8; N]
256    }
257
258    /// The area's length in bytes (`N`).
259    pub const fn len(&self) -> usize {
260        N
261    }
262
263    /// Whether the area is zero bytes.
264    pub const fn is_empty(&self) -> bool {
265        N == 0
266    }
267}
268
269/// Declare static HRAM cells and areas, each at a linker-assigned High RAM
270/// address.
271///
272/// The declared type selects the kind and must be written as one of the three
273/// names below, unqualified; the macro matches on it syntactically, so a path or
274/// an alias does not work.
275///
276/// ```ignore
277/// hram! {
278///     pub static FRAME: HramAtomicCell<u8>;
279///     static SCROLL: HramCell<ScrollState>;
280///     static OAM_DMA: HramArea<13>;
281///     static CURRENT_BANK as "_current_bank": HramAtomicCell<u8>;
282/// }
283/// ```
284///
285/// Each cell becomes a same-named constant carrying the accessors. See the crate
286/// docs for which kind to reach for.
287#[macro_export]
288macro_rules! hram {
289    () => {};
290
291    (
292        $(#[$attr:meta])*
293        $vis:vis static $name:ident $(as $sym:literal)?: HramArea<$n:tt>;
294        $($rest:tt)*
295    ) => {
296        $(#[$attr])*
297        $(#[unsafe(export_name = $sym)])?
298        #[unsafe(link_section = ::core::concat!("_HRAM.", ::core::stringify!($name)))]
299        $vis static $name: $crate::HramArea<{ $n }> = unsafe { $crate::HramArea::uninit() };
300        $crate::hram! { $($rest)* }
301    };
302
303    (
304        $(#[$attr:meta])*
305        $vis:vis static $name:ident $(as $sym:literal)?: HramAtomicCell<$ty:ty>;
306        $($rest:tt)*
307    ) => {
308        #[doc(hidden)]
309        #[allow(non_snake_case)]
310        $vis mod $name {
311            use super::*;
312
313            const _: () = assert!(
314                ::core::mem::size_of::<$ty>() == 1,
315                "HramAtomicCell holds one byte; use HramCell for a wider type",
316            );
317
318            $(#[unsafe(export_name = $sym)])?
319            #[unsafe(link_section = ::core::concat!("_HRAM.", ::core::stringify!($name)))]
320            static STORAGE: $crate::HramAtomicCell<$ty> =
321                unsafe { $crate::HramAtomicCell::uninit() };
322
323            /// The cell's handle (zero-sized): carries the immediate `ldh` accessors.
324            pub struct Handle;
325
326            impl $crate::HramAccess for Handle {
327                type Value = $ty;
328
329                #[inline]
330                fn get_cs(&self, _cs: $crate::CriticalSection<'_>) -> $ty {
331                    <Self as $crate::HramAtomicAccess>::get(self)
332                }
333
334                #[inline]
335                fn set_cs(&self, _cs: $crate::CriticalSection<'_>, value: $ty) {
336                    <Self as $crate::HramAtomicAccess>::set(self, value)
337                }
338
339                #[inline]
340                fn as_ptr(&self) -> *mut $ty {
341                    STORAGE.ptr()
342                }
343            }
344
345            impl $crate::HramAtomicAccess for Handle {
346                #[inline]
347                fn get(&self) -> $ty {
348                    unsafe {
349                        let mut out = ::core::mem::MaybeUninit::<$ty>::uninit();
350                        let dst = out.as_mut_ptr().cast::<u8>();
351                        $crate::__hram_imm_load!(STORAGE, dst, $ty, 0);
352                        out.assume_init()
353                    }
354                }
355
356                #[inline]
357                fn set(&self, value: $ty) {
358                    unsafe {
359                        let src = (&value as *const $ty).cast::<u8>();
360                        $crate::__hram_imm_store!(STORAGE, src, $ty, 0);
361                    }
362                }
363            }
364        }
365
366        $(#[$attr])*
367        #[allow(non_upper_case_globals)]
368        $vis const $name: $name::Handle = $name::Handle;
369
370        $crate::hram! { $($rest)* }
371    };
372
373    (
374        $(#[$attr:meta])*
375        $vis:vis static $name:ident $(as $sym:literal)?: HramCell<$ty:ty>;
376        $($rest:tt)*
377    ) => {
378        #[doc(hidden)]
379        #[allow(non_snake_case)]
380        $vis mod $name {
381            use super::*;
382
383            const _: () = assert!(
384                ::core::mem::size_of::<$ty>() <= $crate::MAX_BYTES,
385                "HramCell is wider than MAX_BYTES",
386            );
387
388            $(#[unsafe(export_name = $sym)])?
389            #[unsafe(link_section = ::core::concat!("_HRAM.", ::core::stringify!($name)))]
390            static STORAGE: $crate::HramCell<$ty> = unsafe { $crate::HramCell::uninit() };
391
392            /// The cell's handle (zero-sized): carries the immediate `ldh` accessors.
393            pub struct Handle;
394
395            impl $crate::HramAccess for Handle {
396                type Value = $ty;
397
398                #[inline]
399                fn get_cs(&self, _cs: $crate::CriticalSection<'_>) -> $ty {
400                    unsafe {
401                        let mut out = ::core::mem::MaybeUninit::<$ty>::uninit();
402                        let dst = out.as_mut_ptr().cast::<u8>();
403                        $crate::__hram_imm_load!(STORAGE, dst, $ty, 0, 1, 2, 3, 4, 5, 6, 7);
404                        out.assume_init()
405                    }
406                }
407
408                #[inline]
409                fn set_cs(&self, _cs: $crate::CriticalSection<'_>, value: $ty) {
410                    unsafe {
411                        let src = (&value as *const $ty).cast::<u8>();
412                        $crate::__hram_imm_store!(STORAGE, src, $ty, 0, 1, 2, 3, 4, 5, 6, 7);
413                    }
414                }
415
416                #[inline]
417                fn as_ptr(&self) -> *mut $ty {
418                    STORAGE.ptr()
419                }
420            }
421        }
422
423        $(#[$attr])*
424        #[allow(non_upper_case_globals)]
425        $vis const $name: $name::Handle = $name::Handle;
426
427        $crate::hram! { $($rest)* }
428    };
429}