embassy_agb/lib.rs
1#![no_std]
2// This appears to be needed for testing to work
3#![cfg_attr(any(test, feature = "testing"), no_main)]
4#![cfg_attr(any(test, feature = "testing"), feature(custom_test_frameworks))]
5#![cfg_attr(
6 any(test, feature = "testing"),
7 test_runner(agb::test_runner::test_runner)
8)]
9#![cfg_attr(
10 any(test, feature = "testing"),
11 reexport_test_harness_main = "test_main"
12)]
13#![warn(missing_docs)]
14
15//! # Embassy async support for agb
16//!
17//! This crate provides async/await support for Game Boy Advance development using the embassy executor.
18//! It integrates with the existing agb library to provide async APIs for display, input, sound, and timing.
19//!
20//! ## Features
21//!
22//! - Async display operations (VBlank waiting, DMA transfers)
23//! - Async input handling (button press events) with automatic polling
24//! - Async sound mixing
25//! - Embassy time integration with GBA timers
26//! - Task spawning and management
27//! - Automatic power management via Halt mode
28//!
29//! ## Example
30//!
31//! ```rust,no_run
32//! #![no_std]
33//! #![no_main]
34//!
35//! use embassy_agb::Spawner;
36//! use embassy_agb::agb::sound::mixer::Frequency;
37//! use agb::include_wav;
38//!
39//! static JUMP: agb::sound::mixer::SoundData = include_wav!("jump.wav");
40//!
41//! #[embassy_agb::main]
42//! async fn main(_spawner: Spawner) -> ! {
43//! let mut gba = embassy_agb::init(Default::default());
44//!
45//! // Get peripherals with convenient frame handling
46//! let mut peripherals = gba.peripherals(Frequency::Hz10512);
47//!
48//! loop {
49//! // wait_frame() returns events that occurred during the frame
50//! let events = peripherals.wait_frame().await;
51//!
52//! // Check button events from the frame context
53//! if events.is_pressed(agb::input::Button::A) {
54//! peripherals.play_sound(&JUMP);
55//! }
56//!
57//! // Or access peripherals directly for continuous state
58//! if peripherals.input.is_pressed(agb::input::Button::LEFT) {
59//! // Move left...
60//! }
61//!
62//! // Use frame counter for animations
63//! let animation_frame = (events.frame_count / 8) as usize;
64//! }
65//! }
66//! ```
67
68// Include generated code
69include!(concat!(env!("OUT_DIR"), "/_generated.rs"));
70
71#[cfg(feature = "executor")]
72pub use embassy_executor::Spawner;
73
74// Re-export our macros
75pub use embassy_agb_macros::{main, task};
76
77#[cfg(feature = "time")]
78pub use embassy_time as time;
79
80#[cfg(feature = "time")]
81pub use embassy_time::{Duration, Instant, Ticker, Timer};
82
83pub use embassy_futures as futures;
84pub use embassy_sync as sync;
85
86// Re-export agb for convenience
87pub use agb;
88
89/// Configuration types for embassy-agb
90pub mod config;
91pub use config::*;
92
93#[cfg(feature = "_time-driver")]
94mod time_driver;
95
96#[cfg(feature = "executor")]
97mod executor;
98#[cfg(feature = "executor")]
99pub use executor::*;
100
101/// Async display utilities
102pub mod display;
103pub mod input;
104/// Async sound utilities
105pub mod sound;
106/// Utility functions and macros
107pub mod utils;
108
109/// Internal utilities (do not use directly)
110#[doc(hidden)]
111pub mod _internal;
112
113/// Initialize the embassy-agb HAL with the given configuration.
114///
115/// This function must be called once before using any embassy-agb functionality.
116/// It initializes the underlying agb library and sets up embassy integration.
117///
118/// # Example
119///
120/// ```rust,no_run
121/// let gba = embassy_agb::init(Default::default());
122/// ```
123pub fn init(config: Config) -> InitializedGba {
124 // Get the agb instance from internal storage (set by macro)
125 let gba = unsafe { _internal::get_agb_instance() };
126
127 // Configure the time driver with user settings
128 #[cfg(feature = "_time-driver")]
129 time_driver::configure_timer_frequency(config.timer.overflow_amount);
130
131 // Take peripherals
132 let peripherals = Peripherals::take();
133
134 InitializedGba {
135 gba,
136 peripherals,
137 _config: config,
138 }
139}
140
141/// The initialized GBA with embassy integration
142pub struct InitializedGba {
143 gba: &'static mut agb::Gba,
144 #[allow(dead_code)]
145 peripherals: Peripherals,
146 _config: Config,
147}
148
149impl InitializedGba {
150 /// Get a convenient peripheral wrapper with automatic frame handling (recommended)
151 ///
152 /// This is the recommended high-level API for most games. It returns a `GbaPeripherals`
153 /// struct that bundles display, mixer, and input together with automatic per-frame updates.
154 ///
155 /// **Features:**
156 /// - Direct field access: `peripherals.display`, `peripherals.mixer`, `peripherals.input`
157 /// - Automatic frame handling: `wait_frame()` handles input updates, audio mixing, and VBlank
158 /// - Frame events: Returns button presses, releases, and frame counter
159 /// - Convenience methods: `play_sound()` and `play_sound_high_priority()`
160 ///
161 /// For advanced use cases requiring finer control, see [`split()`](Self::split).
162 ///
163 /// # Example
164 ///
165 /// ```rust,no_run
166 /// # use embassy_agb::agb::sound::mixer::Frequency;
167 /// # use agb::include_wav;
168 /// # static JUMP: agb::sound::mixer::SoundData = include_wav!("jump.wav");
169 /// # async fn example() {
170 /// let mut gba = embassy_agb::init(Default::default());
171 /// let mut peripherals = gba.peripherals(Frequency::Hz10512);
172 ///
173 /// loop {
174 /// let events = peripherals.wait_frame().await;
175 ///
176 /// if events.is_pressed(agb::input::Button::A) {
177 /// peripherals.play_sound(&JUMP);
178 /// }
179 /// }
180 /// # }
181 /// ```
182 pub fn peripherals(
183 &mut self,
184 mixer_frequency: agb::sound::mixer::Frequency,
185 ) -> GbaPeripherals<'_> {
186 GbaPeripherals::new(
187 &mut self.gba,
188 mixer_frequency,
189 input::InputConfig::default(),
190 )
191 }
192
193 /// Get peripherals with custom input polling configuration
194 ///
195 /// Same as [`peripherals()`](Self::peripherals) but allows customizing the input
196 /// polling rate (default is 60Hz).
197 pub fn peripherals_with_input_config(
198 &mut self,
199 mixer_frequency: agb::sound::mixer::Frequency,
200 input_config: input::InputConfig,
201 ) -> GbaPeripherals<'_> {
202 GbaPeripherals::new(&mut self.gba, mixer_frequency, input_config)
203 }
204
205 /// Split the GBA into display, mixer, and input peripherals
206 ///
207 /// This is the lower-level API that gives you separate components.
208 /// Consider using [`peripherals()`](InitializedGba::peripherals) for a more convenient API.
209 ///
210 /// # Example
211 ///
212 /// ```rust,no_run
213 /// # use embassy_agb::agb::sound::mixer::Frequency;
214 /// # async fn example() {
215 /// let mut gba = embassy_agb::init(Default::default());
216 /// let (mut mixer, display, mut input) = gba.split(Frequency::Hz10512);
217 ///
218 /// loop {
219 /// input.update();
220 /// mixer.frame();
221 /// display.wait_for_vblank().await;
222 /// }
223 /// # }
224 /// ```
225 pub fn split(
226 &mut self,
227 mixer_frequency: agb::sound::mixer::Frequency,
228 ) -> (
229 sound::AsyncMixer<'_>,
230 display::AsyncDisplay<'_>,
231 input::AsyncInput,
232 ) {
233 let mixer = sound::AsyncMixer::new(&mut self.gba.mixer, mixer_frequency);
234 let display = display::AsyncDisplay::new(&mut self.gba.graphics);
235 let input = input::AsyncInput::new();
236 (mixer, display, input)
237 }
238
239 /// Split the GBA into display, mixer, and input with custom input configuration
240 pub fn split_with_input_config(
241 &mut self,
242 mixer_frequency: agb::sound::mixer::Frequency,
243 input_config: input::InputConfig,
244 ) -> (
245 sound::AsyncMixer<'_>,
246 display::AsyncDisplay<'_>,
247 input::AsyncInput,
248 ) {
249 let mixer = sound::AsyncMixer::new(&mut self.gba.mixer, mixer_frequency);
250 let display = display::AsyncDisplay::new(&mut self.gba.graphics);
251 let input = input::AsyncInput::with_config(input_config);
252 (mixer, display, input)
253 }
254
255 /// Get the display peripheral for async operations
256 pub fn display(&mut self) -> display::AsyncDisplay<'_> {
257 display::AsyncDisplay::new(&mut self.gba.graphics)
258 }
259
260 /// Get the mixer peripheral for async operations
261 ///
262 /// Note: If you need to use display or input after creating the mixer,
263 /// use [`split()`](InitializedGba::split) instead to avoid borrow checker issues.
264 pub fn mixer(&mut self, frequency: agb::sound::mixer::Frequency) -> sound::AsyncMixer<'_> {
265 sound::AsyncMixer::new(&mut self.gba.mixer, frequency)
266 }
267
268 /// Get the input peripheral for async operations
269 pub fn input(&mut self) -> input::AsyncInput {
270 input::AsyncInput::new()
271 }
272
273 /// Get the input peripheral for async operations with custom configuration
274 pub fn input_with_config(&mut self, config: input::InputConfig) -> input::AsyncInput {
275 input::AsyncInput::with_config(config)
276 }
277
278 /// Get access to the underlying agb::Gba for compatibility
279 pub fn agb(&mut self) -> &mut agb::Gba {
280 self.gba
281 }
282}
283
284/// Frame events returned by [`GbaPeripherals::wait_frame()`]
285///
286/// Provides information about what happened during the frame.
287///
288/// ## What's Included
289///
290/// - **Button presses**: Buttons that transitioned from released to pressed
291/// - **Button releases**: Buttons that transitioned from pressed to released
292/// - **Frame counter**: Auto-incrementing counter for animations and timing
293#[derive(Debug, Clone, Copy, Default)]
294pub struct FrameEvents {
295 /// Bit flags for buttons that were just pressed this frame
296 pressed: u16,
297 /// Bit flags for buttons that were just released this frame
298 released: u16,
299 /// Frame counter (wraps at u32::MAX)
300 pub frame_count: u32,
301}
302
303impl FrameEvents {
304 /// Check if a specific button was just pressed this frame
305 pub fn is_pressed(&self, button: agb::input::Button) -> bool {
306 (self.pressed & button.bits() as u16) != 0
307 }
308
309 /// Check if a specific button was just released this frame
310 pub fn is_released(&self, button: agb::input::Button) -> bool {
311 (self.released & button.bits() as u16) != 0
312 }
313
314 /// Check if any button was pressed this frame
315 pub fn any_pressed(&self) -> bool {
316 self.pressed != 0
317 }
318
319 /// Check if any button was released this frame
320 pub fn any_released(&self) -> bool {
321 self.released != 0
322 }
323
324 /// Get all buttons that were pressed this frame as a bitmask
325 pub fn pressed_buttons(&self) -> u16 {
326 self.pressed
327 }
328
329 /// Get all buttons that were released this frame as a bitmask
330 pub fn released_buttons(&self) -> u16 {
331 self.released
332 }
333}
334
335/// High-level peripheral wrapper with automatic frame handling
336///
337/// This struct bundles the GBA's display, sound mixer, and input together with
338/// automatic per-frame updates. It follows Embassy's design pattern of providing
339/// direct field access plus convenient helper methods.
340///
341/// ## Design
342///
343/// - **Public fields**: Access `display`, `mixer`, and `input` directly
344/// - **Frame synchronization**: `wait_frame()` handles all per-frame updates automatically
345/// - **Event-driven**: Returns frame events (button presses/releases, frame counter)
346/// - **Zero overhead**: Just a thin wrapper with smart defaults
347///
348/// ## Usage Pattern
349///
350/// 1. Call `wait_frame()` once per game loop iteration
351/// 2. Handle button events from the returned `FrameEvents`
352/// 3. Access peripherals directly when needed (e.g., `peripherals.mixer`)
353/// 4. Use convenience methods like `play_sound()` for common operations
354///
355/// # Example
356///
357/// ```rust,no_run
358/// # use embassy_agb::agb::sound::mixer::{Frequency, SoundChannel};
359/// # use agb::include_wav;
360/// # static JUMP: agb::sound::mixer::SoundData = include_wav!("jump.wav");
361/// # async fn example() {
362/// let mut gba = embassy_agb::init(Default::default());
363/// let mut peripherals = gba.peripherals(Frequency::Hz10512);
364///
365/// loop {
366/// // wait_frame() returns events that occurred during the frame
367/// let events = peripherals.wait_frame().await;
368///
369/// // Check button events from the returned context
370/// if events.is_pressed(agb::input::Button::A) {
371/// peripherals.play_sound(&JUMP);
372/// }
373///
374/// // Access peripherals directly for continuous state
375/// if peripherals.input.is_pressed(agb::input::Button::LEFT) {
376/// // Move left...
377/// }
378///
379/// // Use frame counter for animations
380/// let anim_frame = (events.frame_count / 8) as usize;
381/// }
382/// # }
383/// ```
384pub struct GbaPeripherals<'a> {
385 /// Display peripheral for VBlank and rendering
386 pub display: display::AsyncDisplay<'a>,
387 /// Sound mixer for audio playback
388 pub mixer: sound::AsyncMixer<'a>,
389 /// Input peripheral for button handling
390 pub input: input::AsyncInput,
391 frame_count: u32,
392 prev_button_state: u16,
393}
394
395impl<'a> GbaPeripherals<'a> {
396 fn new(
397 gba: &'a mut agb::Gba,
398 mixer_frequency: agb::sound::mixer::Frequency,
399 input_config: input::InputConfig,
400 ) -> Self {
401 Self {
402 mixer: sound::AsyncMixer::new(&mut gba.mixer, mixer_frequency),
403 display: display::AsyncDisplay::new(&mut gba.graphics),
404 input: input::AsyncInput::with_config(input_config),
405 frame_count: 0,
406 prev_button_state: 0,
407 }
408 }
409
410 /// Wait for the next frame, automatically handling all per-frame updates
411 ///
412 /// This method:
413 /// 1. Updates input state (detects button changes)
414 /// 2. Processes one frame of audio mixing
415 /// 3. Waits for VBlank (~16.7ms at 60Hz)
416 /// 4. Returns frame events (button changes, frame count, etc.)
417 ///
418 /// Call this once per frame in your game loop.
419 ///
420 /// # Example
421 ///
422 /// ```rust,no_run
423 /// # async fn example(mut peripherals: embassy_agb::GbaPeripherals<'_>) {
424 /// loop {
425 /// let events = peripherals.wait_frame().await;
426 ///
427 /// if events.is_pressed(agb::input::Button::A) {
428 /// // Button A was just pressed this frame
429 /// }
430 ///
431 /// // Use frame_count for animations
432 /// let animation_frame = (events.frame_count / 8) as usize;
433 /// }
434 /// # }
435 /// ```
436 pub async fn wait_frame(&mut self) -> FrameEvents {
437 self.input.update();
438
439 // Get current button state as raw bits
440 let current_state = self.input.button_state_bits();
441
442 // Calculate button changes
443 let pressed = current_state & !self.prev_button_state;
444 let released = !current_state & self.prev_button_state;
445
446 self.prev_button_state = current_state;
447
448 self.mixer.frame();
449 self.display.wait_for_vblank().await;
450
451 let events = FrameEvents {
452 pressed,
453 released,
454 frame_count: self.frame_count,
455 };
456
457 self.frame_count = self.frame_count.wrapping_add(1);
458
459 events
460 }
461
462 /// Play a sound effect with default priority
463 ///
464 /// Convenience method that creates a `SoundChannel` and plays it through the mixer.
465 /// Returns `Ok(channel_id)` if the sound starts playing, or `Err(SoundError)`
466 /// if all channels are busy.
467 ///
468 /// # Example
469 ///
470 /// ```rust,no_run
471 /// # use agb::include_wav;
472 /// # static JUMP: agb::sound::mixer::SoundData = include_wav!("jump.wav");
473 /// # async fn example(mut peripherals: embassy_agb::GbaPeripherals<'_>) {
474 /// let events = peripherals.wait_frame().await;
475 ///
476 /// if events.is_pressed(agb::input::Button::A) {
477 /// peripherals.play_sound(&JUMP);
478 /// }
479 /// # }
480 /// ```
481 pub fn play_sound(
482 &mut self,
483 sound: &'static agb::sound::mixer::SoundData,
484 ) -> Result<agb::sound::mixer::ChannelId, sound::SoundError> {
485 let channel = agb::sound::mixer::SoundChannel::new(*sound);
486 self.mixer.play_sound(channel)
487 }
488
489 /// Play a sound effect with high priority
490 ///
491 /// High priority sounds will replace low priority sounds if all channels are busy.
492 /// Use this for important sounds like background music or critical sound effects.
493 ///
494 /// # Example
495 ///
496 /// ```rust,no_run
497 /// # use agb::include_wav;
498 /// # static BGM: agb::sound::mixer::SoundData = include_wav!("music.wav");
499 /// # async fn example(mut peripherals: embassy_agb::GbaPeripherals<'_>) {
500 /// // Play background music with high priority so it doesn't get interrupted
501 /// peripherals.play_sound_high_priority(&BGM);
502 /// # }
503 /// ```
504 pub fn play_sound_high_priority(
505 &mut self,
506 sound: &'static agb::sound::mixer::SoundData,
507 ) -> Result<agb::sound::mixer::ChannelId, sound::SoundError> {
508 let channel = agb::sound::mixer::SoundChannel::new_high_priority(*sound);
509 self.mixer.play_sound(channel)
510 }
511}
512
513/// Enable automatic input polling with the given polling rate.
514///
515/// This function should be called once at startup to automatically spawn
516/// the input polling task. If not called, input methods will still work
517/// but will use polling-based approach instead of interrupt-driven.
518///
519/// # Example
520///
521/// ```rust,no_run
522/// use embassy_agb::input::PollingRate;
523///
524/// #[embassy_agb::main]
525/// async fn main(spawner: Spawner) -> ! {
526/// let mut gba = embassy_agb::init(Default::default());
527///
528/// // Enable automatic input polling at 60Hz
529/// embassy_agb::enable_input_polling(&spawner, PollingRate::Hz60);
530///
531/// let mut input = gba.input();
532/// // ... rest of your code
533/// }
534/// ```
535#[cfg(all(feature = "time", feature = "executor"))]
536pub fn enable_input_polling(spawner: &Spawner, rate: input::PollingRate) {
537 let config = input::InputConfig::from(rate);
538 spawner.must_spawn(input::input_polling_task(config));
539}