520 lines
16 KiB
Rust
520 lines
16 KiB
Rust
// from https://github.com/rust-console/gba/blob/main/examples/hello.rs
|
|
#![no_std]
|
|
#![no_main]
|
|
#![feature(ascii_char)]
|
|
#![feature(panic_info_message)]
|
|
// rustc is simply wrong about core::fmt::Write being unused,
|
|
// it's used in the panic handler.
|
|
#![allow(unused_imports)]
|
|
|
|
mod halfwidth;
|
|
|
|
use core::fmt::Write;
|
|
|
|
use gba::prelude::*;
|
|
use voladdress::{Safe, VolAddress};
|
|
|
|
#[macro_use]
|
|
extern crate build_const;
|
|
build_const!("lsdpack_bc");
|
|
|
|
#[allow(unused_must_use)]
|
|
#[panic_handler]
|
|
fn panic_handler(info: &core::panic::PanicInfo) -> ! {
|
|
let mut text_painter = halfwidth::TextPainter::new();
|
|
text_painter.setup_display();
|
|
|
|
text_painter.write_str("panic at ");
|
|
if let Some(location) = info.location() {
|
|
write!(&mut text_painter, "{}:{}", location.file(), location.line());
|
|
} else {
|
|
text_painter.write_str("unknown location");
|
|
};
|
|
if let Some(msg) = info.message() {
|
|
write!(&mut text_painter, ":\n");
|
|
core::fmt::write(&mut text_painter, *msg);
|
|
}
|
|
if let Some(s) = info.payload().downcast_ref::<&str>() {
|
|
text_painter.write_str("\n");
|
|
text_painter.write_str(s);
|
|
}
|
|
#[cfg(debug_assertions)]
|
|
if let Ok(mut logger) = MgbaBufferedLogger::try_new(MgbaMessageLevel::Fatal) {
|
|
for _ in 0..4 {
|
|
VBlankIntrWait();
|
|
}
|
|
if let Some(args) = info.message() {
|
|
logger.write_fmt(*args);
|
|
} else {
|
|
writeln!(logger, "{info}");
|
|
}
|
|
}
|
|
loop {
|
|
VBlankIntrWait();
|
|
}
|
|
}
|
|
|
|
#[link_section = ".ewram"]
|
|
static FRAME_KEYS: GbaCell<KeyInput> = GbaCell::new(KeyInput::new());
|
|
|
|
#[link_section = ".iwram"]
|
|
static mut LSDJ_SONGS: Lsdpack = Lsdpack::new(&LSDPACK_DATA, &LSDPACK_SONG_POSITIONS);
|
|
|
|
#[link_section = ".iwram"]
|
|
extern "C" fn irq_handler(bits: IrqBits) {
|
|
if bits.vblank() {
|
|
// We'll read the keys during vblank and store it for later.
|
|
FRAME_KEYS.write(KEYINPUT.read());
|
|
}
|
|
if bits.timer0() {
|
|
unsafe {
|
|
LSDJ_SONGS.tick();
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[repr(u8)]
|
|
#[derive(Copy, Clone)]
|
|
enum LsdpackCmd {
|
|
// special commands (not just plain register writes)
|
|
EndTick = 0,
|
|
SampleStart = 1,
|
|
SongStop = 2,
|
|
NextBank = 3,
|
|
AmpDecPu0 = 4,
|
|
AmpDecPu1 = 5,
|
|
AmpDecNoi = 6,
|
|
PitchPu0 = 7,
|
|
PitchPu1 = 8,
|
|
PitchWav = 9,
|
|
SampleNext = 10,
|
|
|
|
_Invalid0B = 0x0b,
|
|
_Invalid0C = 0x0c,
|
|
_Invalid0D = 0x0d,
|
|
_Invalid0E = 0x0e,
|
|
_Invalid0F = 0x0f,
|
|
|
|
// ch1 pulse
|
|
Pu0Sweep = 0x10,
|
|
Pu0LengthWave = 0x11,
|
|
Pu0Env = 0x12,
|
|
Pu0PitchLsb = 0x13,
|
|
Pu0PitchMsb = 0x14,
|
|
|
|
_Invalid15 = 0x15,
|
|
|
|
// ch2 pulse
|
|
Pu1LengthWave = 0x16,
|
|
Pu1Env = 0x17,
|
|
Pu1PitchLsb = 0x18,
|
|
Pu1PitchMsb = 0x19,
|
|
|
|
// ch3 wave
|
|
WavOnOff = 0x1a,
|
|
WavLength = 0x1b,
|
|
WavEnv = 0x1c,
|
|
WavPitchLsb = 0x1d,
|
|
WavPitchMsb = 0x1e,
|
|
|
|
_Invalid1F = 0x1f,
|
|
|
|
// ch4 noise
|
|
NoiLength = 0x20,
|
|
NoiEnv = 0x21,
|
|
NoiWave = 0x22,
|
|
NoiTrig = 0x23,
|
|
|
|
// general
|
|
ChannelVolume = 0x24,
|
|
Pan = 0x25,
|
|
SoundOffOn = 0x26,
|
|
|
|
_Invalid27 = 0x27,
|
|
_Invalid28 = 0x28,
|
|
_Invalid29 = 0x29,
|
|
_Invalid2A = 0x2a,
|
|
_Invalid2B = 0x2b,
|
|
_Invalid2C = 0x2c,
|
|
_Invalid2D = 0x2d,
|
|
_Invalid2E = 0x2e,
|
|
_Invalid2F = 0x2f,
|
|
|
|
// wave pattern
|
|
WavePattern0 = 0x30,
|
|
WavePattern1 = 0x31,
|
|
WavePattern2 = 0x32,
|
|
WavePattern3 = 0x33,
|
|
WavePattern4 = 0x34,
|
|
WavePattern5 = 0x35,
|
|
WavePattern6 = 0x36,
|
|
WavePattern7 = 0x37,
|
|
WavePattern8 = 0x38,
|
|
WavePattern9 = 0x39,
|
|
WavePatternA = 0x3a,
|
|
WavePatternB = 0x3b,
|
|
WavePatternC = 0x3c,
|
|
WavePatternD = 0x3d,
|
|
WavePatternE = 0x3e,
|
|
WavePatternF = 0x3f,
|
|
}
|
|
|
|
impl From<u8> for LsdpackCmd {
|
|
fn from(val: u8) -> Self {
|
|
assert!(val <= 0x3f);
|
|
unsafe { core::mem::transmute(val) }
|
|
}
|
|
}
|
|
|
|
struct Lsdpack {
|
|
data: &'static [u8],
|
|
setlist: &'static [usize],
|
|
current_ptr: usize,
|
|
sample_ptr: usize,
|
|
repeat_cmd: Option<(LsdpackCmd, bool)>,
|
|
repeat_cmd_counter: usize,
|
|
sample_pitch: u16,
|
|
stopped: bool,
|
|
}
|
|
|
|
impl Lsdpack {
|
|
const fn new(data: &'static [u8], setlist: &'static [usize]) -> Self {
|
|
Self {
|
|
data,
|
|
setlist,
|
|
current_ptr: setlist[0],
|
|
sample_ptr: 0,
|
|
repeat_cmd: None,
|
|
repeat_cmd_counter: 0,
|
|
sample_pitch: 0,
|
|
stopped: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Lsdpack {
|
|
fn play_song(&mut self, song_id: usize) {
|
|
self.current_ptr = self.setlist[song_id];
|
|
self.sample_ptr = 0;
|
|
self.repeat_cmd = None;
|
|
self.repeat_cmd_counter = 0;
|
|
self.stopped = false;
|
|
}
|
|
|
|
#[link_section = ".iwram"]
|
|
fn next_byte(&mut self) -> u8 {
|
|
let val = self.data[self.current_ptr];
|
|
self.current_ptr += 1;
|
|
val
|
|
}
|
|
|
|
#[link_section = ".iwram"]
|
|
fn write_next_samples(&mut self) {
|
|
let ptr = self.sample_ptr;
|
|
self.sample_ptr += 16;
|
|
//let sound_enabled = SOUND_ENABLED.read();
|
|
//SOUND_ENABLED.write(sound_enabled.with_wave_playing(false));
|
|
let lrvol = LEFT_RIGHT_VOLUME.read();
|
|
LEFT_RIGHT_VOLUME.write(lrvol.with_wave_left(false).with_wave_right(false));
|
|
WAVE_BANK.write(WaveBank::new().with_enabled(false));
|
|
WAVE_RAM.index(0).write(u32::from_le_bytes(
|
|
TryFrom::try_from(&self.data[ptr..ptr + 4]).unwrap(),
|
|
));
|
|
WAVE_RAM.index(1).write(u32::from_le_bytes(
|
|
TryFrom::try_from(&self.data[ptr + 4..ptr + 8]).unwrap(),
|
|
));
|
|
WAVE_RAM.index(2).write(u32::from_le_bytes(
|
|
TryFrom::try_from(&self.data[ptr + 8..ptr + 12]).unwrap(),
|
|
));
|
|
WAVE_RAM.index(3).write(u32::from_le_bytes(
|
|
TryFrom::try_from(&self.data[ptr + 12..ptr + 16]).unwrap(),
|
|
));
|
|
WAVE_BANK.write(WaveBank::new().with_enabled(true));
|
|
//SOUND_ENABLED.write(sound_enabled);
|
|
LEFT_RIGHT_VOLUME.write(lrvol);
|
|
WAVE_FREQ.write(WaveFrequency::new().with_length(self.sample_pitch));
|
|
}
|
|
|
|
#[link_section = ".iwram"]
|
|
fn tick(&mut self) {
|
|
if self.stopped {
|
|
return;
|
|
}
|
|
let mut ongoing = true;
|
|
while ongoing {
|
|
if let Some((cmd, flagged_ongoing)) = self.repeat_cmd {
|
|
if self.repeat_cmd_counter > 0 {
|
|
self.repeat_cmd_counter -= 1;
|
|
ongoing = flagged_ongoing;
|
|
ongoing &= self.apply_cmd(cmd);
|
|
continue;
|
|
} else {
|
|
self.repeat_cmd = None;
|
|
}
|
|
}
|
|
let (cmd, flagged_ongoing) = self.load_new_cmd_from_stream();
|
|
ongoing = flagged_ongoing;
|
|
ongoing &= self.apply_cmd(cmd);
|
|
}
|
|
}
|
|
|
|
#[link_section = ".iwram"]
|
|
fn load_new_cmd_from_stream(&mut self) -> (LsdpackCmd, bool) {
|
|
const FLAG_REPEAT: u8 = 0x40;
|
|
const FLAG_END_TICK: u8 = 0x80;
|
|
let mut cmd = self.next_byte();
|
|
let mut ongoing = true;
|
|
if cmd & FLAG_END_TICK != 0 {
|
|
cmd &= !FLAG_END_TICK;
|
|
ongoing = false;
|
|
}
|
|
if cmd & FLAG_REPEAT != 0 {
|
|
cmd &= !FLAG_REPEAT;
|
|
self.repeat_cmd = Some((cmd.into(), ongoing));
|
|
self.repeat_cmd_counter = self.next_byte() as usize;
|
|
}
|
|
(cmd.into(), ongoing)
|
|
}
|
|
|
|
#[link_section = ".iwram"]
|
|
fn write_lsb<T>(&mut self, voladdr: VolAddress<T, Safe, Safe>) {
|
|
let voladdr = unsafe { voladdr.cast::<u16>() };
|
|
voladdr.write((voladdr.read() & 0xFF00) | (self.next_byte() as u16));
|
|
}
|
|
|
|
#[link_section = ".iwram"]
|
|
fn write_msb<T>(&mut self, voladdr: VolAddress<T, Safe, Safe>) {
|
|
let voladdr = unsafe { voladdr.cast::<u16>() };
|
|
voladdr.write((voladdr.read() & 0xFF) | ((self.next_byte() as u16) << 8));
|
|
}
|
|
|
|
#[link_section = ".iwram"]
|
|
fn write_u16le<T>(&mut self, voladdr: VolAddress<T, Safe, Safe>) {
|
|
let voladdr = unsafe { voladdr.cast::<u16>() };
|
|
let halfword = u16::from_le_bytes([self.next_byte(), self.next_byte()]);
|
|
voladdr.write(halfword);
|
|
}
|
|
|
|
#[link_section = ".iwram"]
|
|
fn apply_cmd(&mut self, cmd: LsdpackCmd) -> bool {
|
|
use LsdpackCmd::*;
|
|
match cmd {
|
|
// special commands:
|
|
EndTick => return false,
|
|
SampleStart => {
|
|
let _sample_bank = self.next_byte(); // TODO?
|
|
self.sample_ptr = u16::from_le_bytes([self.next_byte(), self.next_byte()]) as usize;
|
|
// big endian order for the pitch here? don't know why
|
|
self.sample_pitch = u16::from_be_bytes([self.next_byte(), self.next_byte()]);
|
|
self.write_next_samples(); // also writes pitch
|
|
}
|
|
SongStop => {
|
|
self.stopped = true;
|
|
return false;
|
|
}
|
|
NextBank => { /* lol */ }
|
|
AmpDecPu0 => {
|
|
// https://github.com/LIJI32/SameBoy/blob/27b5935b8d0e0af988bcac8e55e92703af24f335/Core/apu.c#L341
|
|
let base = TONE1_PATTERN.read();
|
|
TONE1_PATTERN.write(
|
|
base.with_volume(0)
|
|
.with_step_increasing(true)
|
|
.with_step_time(1),
|
|
); // $FF12 <- $9
|
|
TONE1_PATTERN.write(
|
|
base.with_volume(1)
|
|
.with_step_increasing(false)
|
|
.with_step_time(1),
|
|
); // $FF12 <- $11
|
|
TONE1_PATTERN.write(
|
|
base.with_volume(1)
|
|
.with_step_increasing(true)
|
|
.with_step_time(0),
|
|
); // $FF12 <- $18
|
|
}
|
|
AmpDecPu1 => {
|
|
let base = TONE2_PATTERN.read();
|
|
TONE2_PATTERN.write(
|
|
base.with_volume(0)
|
|
.with_step_increasing(true)
|
|
.with_step_time(1),
|
|
); // $FF17 <- $9
|
|
TONE2_PATTERN.write(
|
|
base.with_volume(1)
|
|
.with_step_increasing(false)
|
|
.with_step_time(1),
|
|
); // $FF17 <- $11
|
|
TONE2_PATTERN.write(
|
|
base.with_volume(1)
|
|
.with_step_increasing(true)
|
|
.with_step_time(0),
|
|
); // $FF17 <- $18
|
|
}
|
|
AmpDecNoi => {
|
|
let base = NOISE_LEN_ENV.read();
|
|
NOISE_LEN_ENV.write(
|
|
base.with_volume(0)
|
|
.with_step_increasing(true)
|
|
.with_step_time(1),
|
|
); // $FF21 <- $9
|
|
NOISE_LEN_ENV.write(
|
|
base.with_volume(1)
|
|
.with_step_increasing(false)
|
|
.with_step_time(1),
|
|
); // $FF21 <- $11
|
|
NOISE_LEN_ENV.write(
|
|
base.with_volume(1)
|
|
.with_step_increasing(true)
|
|
.with_step_time(0),
|
|
); // $FF21 <- $18
|
|
}
|
|
PitchPu0 => self.write_u16le(TONE1_FREQUENCY),
|
|
PitchPu1 => self.write_u16le(TONE2_FREQUENCY),
|
|
PitchWav => self.write_u16le(WAVE_FREQ),
|
|
SampleNext => self.write_next_samples(),
|
|
|
|
// general register writes:
|
|
Pu0Sweep => unsafe { TONE1_SWEEP.cast() }.write(self.next_byte()),
|
|
Pu0LengthWave => self.write_lsb(TONE1_PATTERN),
|
|
Pu0Env => self.write_msb(TONE1_PATTERN),
|
|
Pu0PitchLsb => self.write_lsb(TONE1_FREQUENCY),
|
|
Pu0PitchMsb => self.write_msb(TONE1_FREQUENCY),
|
|
Pu1LengthWave => self.write_lsb(TONE2_PATTERN),
|
|
Pu1Env => self.write_msb(TONE2_PATTERN),
|
|
Pu1PitchLsb => self.write_lsb(TONE2_FREQUENCY),
|
|
Pu1PitchMsb => self.write_msb(TONE2_FREQUENCY),
|
|
WavOnOff => WAVE_BANK.write(WaveBank::new().with_enabled(self.next_byte() != 0)),
|
|
WavLength => self.write_lsb(WAVE_LEN_VOLUME),
|
|
WavEnv => self.write_msb(WAVE_LEN_VOLUME),
|
|
WavPitchLsb => self.write_lsb(WAVE_FREQ),
|
|
WavPitchMsb => self.write_msb(WAVE_FREQ),
|
|
NoiLength => self.write_lsb(NOISE_LEN_ENV),
|
|
NoiEnv => self.write_msb(NOISE_LEN_ENV),
|
|
NoiWave => self.write_lsb(NOISE_FREQ),
|
|
NoiTrig => self.write_msb(NOISE_FREQ),
|
|
ChannelVolume => self.write_lsb(LEFT_RIGHT_VOLUME),
|
|
Pan => self.write_msb(LEFT_RIGHT_VOLUME),
|
|
SoundOffOn => unsafe { SOUND_ENABLED.cast() }.write(self.next_byte()),
|
|
WavePattern0 => todo!(),
|
|
WavePattern1 => todo!(),
|
|
WavePattern2 => todo!(),
|
|
WavePattern3 => todo!(),
|
|
WavePattern4 => todo!(),
|
|
WavePattern5 => todo!(),
|
|
WavePattern6 => todo!(),
|
|
WavePattern7 => todo!(),
|
|
WavePattern8 => todo!(),
|
|
WavePattern9 => todo!(),
|
|
WavePatternA => todo!(),
|
|
WavePatternB => todo!(),
|
|
WavePatternC => todo!(),
|
|
WavePatternD => todo!(),
|
|
WavePatternE => todo!(),
|
|
WavePatternF => todo!(),
|
|
_Invalid0B | _Invalid0C | _Invalid0D | _Invalid0E | _Invalid0F | _Invalid15
|
|
| _Invalid1F | _Invalid27 | _Invalid28 | _Invalid29 | _Invalid2A | _Invalid2B
|
|
| _Invalid2C | _Invalid2D | _Invalid2E | _Invalid2F => unimplemented!(),
|
|
}
|
|
true
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "C" fn main() -> ! {
|
|
RUST_IRQ_HANDLER.write(Some(irq_handler));
|
|
DISPSTAT.write(DisplayStatus::new().with_irq_vblank(true));
|
|
IE.write(IrqBits::VBLANK.with_timer0(true));
|
|
IME.write(true);
|
|
|
|
//TIMER0_RELOAD.write((((16777216 / 1) / 360) as u16).wrapping_neg());
|
|
TIMER0_RELOAD.write((((280896 / 1) / 6) as u16).wrapping_neg());
|
|
TIMER0_CONTROL.write(
|
|
TimerControl::new()
|
|
.with_overflow_irq(true)
|
|
.with_cascade(false)
|
|
.with_scale(TimerScale::_1)
|
|
.with_enabled(true),
|
|
);
|
|
|
|
SOUNDBIAS.write(
|
|
SoundBias::new()
|
|
.with_bias_level(0x100)
|
|
.with_sample_cycle(SampleCycle::_6bit),
|
|
);
|
|
|
|
unsafe {
|
|
LSDJ_SONGS.play_song(0);
|
|
}
|
|
|
|
if let Ok(mut logger) = MgbaBufferedLogger::try_new(MgbaMessageLevel::Debug) {
|
|
writeln!(logger, "hello!").ok();
|
|
|
|
let fx_u: Fixed<u32, 8> = Fixed::<u32, 8>::wrapping_from(7) + Fixed::<u32, 8>::from_raw(12);
|
|
writeln!(logger, "fixed unsigned: {fx_u:?}").ok();
|
|
|
|
let fx_i1: Fixed<i32, 8> =
|
|
Fixed::<i32, 8>::wrapping_from(8) + Fixed::<i32, 8>::from_raw(15);
|
|
writeln!(logger, "fixed signed positive: {fx_i1:?}").ok();
|
|
|
|
let fx_i2: Fixed<i32, 8> = Fixed::<i32, 8>::wrapping_from(0)
|
|
- Fixed::<i32, 8>::wrapping_from(3)
|
|
- Fixed::<i32, 8>::from_raw(17);
|
|
writeln!(logger, "fixed signed negative: {fx_i2:?}").ok();
|
|
}
|
|
|
|
{
|
|
// get our tile data into memory.
|
|
Cga8x8Thick.bitunpack_4bpp(CHARBLOCK0_4BPP.as_region(), 0);
|
|
}
|
|
|
|
{
|
|
// set up the tilemap
|
|
let tsb = TEXT_SCREENBLOCKS.get_frame(31).unwrap();
|
|
for y in 0..16 {
|
|
let row = tsb.get_row(y).unwrap();
|
|
for (x, addr) in row.iter().enumerate().take(16) {
|
|
let te = TextEntry::new().with_tile((y * 16 + x) as u16);
|
|
addr.write(te);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
// Set BG0 to use the tilemap we just made, and set it to be shown.
|
|
BG0CNT.write(BackgroundControl::new().with_screenblock(31));
|
|
DISPCNT.write(DisplayControl::new().with_show_bg0(true));
|
|
}
|
|
|
|
let mut x_off = 0_u32;
|
|
let mut y_off = 0_u32;
|
|
let mut backdrop_color = Color(0);
|
|
loop {
|
|
VBlankIntrWait();
|
|
// show current frame
|
|
BACKDROP_COLOR.write(backdrop_color);
|
|
BG0HOFS.write(x_off as u16);
|
|
BG0VOFS.write(y_off as u16);
|
|
|
|
// prep next frame
|
|
let k = FRAME_KEYS.read();
|
|
backdrop_color = Color(k.to_u16());
|
|
if k.up() {
|
|
y_off = y_off.wrapping_add(1);
|
|
}
|
|
if k.down() {
|
|
y_off = y_off.wrapping_sub(1);
|
|
}
|
|
if k.left() {
|
|
x_off = x_off.wrapping_add(1);
|
|
}
|
|
if k.right() {
|
|
x_off = x_off.wrapping_sub(1);
|
|
}
|
|
|
|
if k.start() {
|
|
panic!("pressed start")
|
|
}
|
|
}
|
|
}
|