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}