Initial commit
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# Build output
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Generated from the ROM by ../recompile.py. These are derived from the
|
||||||
|
# copyrighted game, so keep them out of the repo; regenerate with:
|
||||||
|
# python3 ../recompile.py "../Daedalian Opus (USA).gb" src/game.rs assets/rom_data.bin
|
||||||
|
/src/game.rs
|
||||||
|
/assets/rom_data.bin
|
||||||
|
|
||||||
|
# Headless test output
|
||||||
|
*.wav
|
||||||
|
|
||||||
|
# Editors / OS
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
Generated
+2724
File diff suppressed because it is too large
Load Diff
+13
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "daedalian"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Daedalian Opus, statically recompiled from its Game Boy disassembly"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
winit = "0.30"
|
||||||
|
pixels = "0.17"
|
||||||
|
cpal = "0.18"
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 1
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Daedalian Opus — recompiled to Rust
|
||||||
|
|
||||||
|
A proof of concept: the Game Boy game's code, statically translated from the
|
||||||
|
disassembly into Rust, running on a tiny hand-written hardware layer.
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Controls: arrows = D-pad, Z = A, X = B, Enter = Start, Right Shift/Backspace = Select, Esc = quit.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
- `src/game.rs` is **generated** by `../recompile.py` from the traced
|
||||||
|
disassembly: every SM83 instruction becomes Rust statements, grouped into
|
||||||
|
basic blocks (`match m.pc { 0x0164 => { ... } }`), each commented with the
|
||||||
|
original assembly. No SM83 code is decoded at runtime. The game's stack lives
|
||||||
|
in emulated memory because the game manipulates return addresses itself.
|
||||||
|
- `assets/rom_data.bin` is the ROM with every code byte zeroed — only the
|
||||||
|
data (levels, graphics, music, text) is used. The runtime warns if the game
|
||||||
|
ever reads a code byte as data.
|
||||||
|
- `src/machine.rs`: registers/flags/ALU, memory map, joypad, OAM DMA, the
|
||||||
|
vblank interrupt, and a once-per-frame LCD renderer.
|
||||||
|
- `src/apu.rs`: the four sound channels, synthesized in step with the
|
||||||
|
recompiled code; played through `cpal`.
|
||||||
|
- `src/main.rs`: `winit` window + `pixels` framebuffer, 59.73 Hz pacing.
|
||||||
|
|
||||||
|
Regenerate after changing the recompiler:
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 ../recompile.py "../Daedalian Opus (USA).gb" src/game.rs assets/rom_data.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Headless testing
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run -- --headless OUT_DIR 120 start:3 90 shot:title peek:C0A1:2 goto:44:54
|
||||||
|
```
|
||||||
|
|
||||||
|
Tokens: `N` run N frames, `BUTTON:N` hold a button N frames, `shot:NAME` save a
|
||||||
|
PNG, `peek:ADDR:N` print memory, `goto:YY:XX` steer the puzzle cursor. All
|
||||||
|
audio is written to `OUT_DIR/audio.wav`.
|
||||||
+361
@@ -0,0 +1,361 @@
|
|||||||
|
//! Game Boy APU: two pulse channels (one with sweep), the wave channel, and
|
||||||
|
//! the noise channel, mixed to stereo f32 samples. Registers are updated as
|
||||||
|
//! the game writes them; samples are synthesized up to the current CPU cycle
|
||||||
|
//! before every register write, so timing follows the recompiled code.
|
||||||
|
|
||||||
|
use crate::machine::CPU_HZ;
|
||||||
|
|
||||||
|
const DUTY: [[u8; 8]; 4] = [
|
||||||
|
[0, 0, 0, 0, 0, 0, 0, 1],
|
||||||
|
[1, 0, 0, 0, 0, 0, 0, 1],
|
||||||
|
[1, 0, 0, 0, 0, 1, 1, 1],
|
||||||
|
[0, 1, 1, 1, 1, 1, 1, 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Envelope {
|
||||||
|
volume: u8,
|
||||||
|
increase: bool,
|
||||||
|
period: u8,
|
||||||
|
timer: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Envelope {
|
||||||
|
fn load(&mut self, reg: u8) {
|
||||||
|
self.volume = reg >> 4;
|
||||||
|
self.increase = reg & 0x08 != 0;
|
||||||
|
self.period = reg & 0x07;
|
||||||
|
self.timer = self.period;
|
||||||
|
}
|
||||||
|
fn clock(&mut self) {
|
||||||
|
if self.period == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.timer > 1 {
|
||||||
|
self.timer -= 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.timer = self.period;
|
||||||
|
if self.increase && self.volume < 15 {
|
||||||
|
self.volume += 1;
|
||||||
|
} else if !self.increase && self.volume > 0 {
|
||||||
|
self.volume -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Pulse {
|
||||||
|
on: bool,
|
||||||
|
length: u16,
|
||||||
|
length_enable: bool,
|
||||||
|
env: Envelope,
|
||||||
|
freq: u16,
|
||||||
|
phase: f32,
|
||||||
|
// sweep (channel 1 only)
|
||||||
|
sweep_timer: u8,
|
||||||
|
sweep_enabled: bool,
|
||||||
|
shadow_freq: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Wave {
|
||||||
|
on: bool,
|
||||||
|
length: u16,
|
||||||
|
length_enable: bool,
|
||||||
|
freq: u16,
|
||||||
|
phase: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Noise {
|
||||||
|
on: bool,
|
||||||
|
length: u16,
|
||||||
|
length_enable: bool,
|
||||||
|
env: Envelope,
|
||||||
|
lfsr: u16,
|
||||||
|
acc: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Apu {
|
||||||
|
regs: [u8; 0x30], // $FF10-$FF3F
|
||||||
|
sq1: Pulse,
|
||||||
|
sq2: Pulse,
|
||||||
|
wave: Wave,
|
||||||
|
noise: Noise,
|
||||||
|
sample_rate: u32,
|
||||||
|
samples_made: u64,
|
||||||
|
last_cycle: u64,
|
||||||
|
seq_cycles: u64,
|
||||||
|
seq_step: u8,
|
||||||
|
hp_l: (f32, f32),
|
||||||
|
hp_r: (f32, f32),
|
||||||
|
/// interleaved stereo samples waiting to be played
|
||||||
|
pub out: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apu {
|
||||||
|
pub fn new(sample_rate: u32) -> Self {
|
||||||
|
let mut a = Apu {
|
||||||
|
regs: [0; 0x30],
|
||||||
|
sq1: Pulse::default(),
|
||||||
|
sq2: Pulse::default(),
|
||||||
|
wave: Wave::default(),
|
||||||
|
noise: Noise::default(),
|
||||||
|
sample_rate,
|
||||||
|
samples_made: 0,
|
||||||
|
last_cycle: 0,
|
||||||
|
seq_cycles: 0,
|
||||||
|
seq_step: 0,
|
||||||
|
hp_l: (0.0, 0.0),
|
||||||
|
hp_r: (0.0, 0.0),
|
||||||
|
out: Vec::new(),
|
||||||
|
};
|
||||||
|
a.regs[0x16] = 0xF1; // NR52: on
|
||||||
|
a.regs[0x14] = 0x77; // NR50
|
||||||
|
a.regs[0x15] = 0xF3; // NR51
|
||||||
|
a
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reg(&self, addr: u16) -> u8 {
|
||||||
|
self.regs[(addr - 0xFF10) as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(&mut self, addr: u16, cycles: u64) -> u8 {
|
||||||
|
self.catch_up(cycles);
|
||||||
|
if addr == 0xFF26 {
|
||||||
|
return (self.reg(0xFF26) & 0x80)
|
||||||
|
| 0x70
|
||||||
|
| self.sq1.on as u8
|
||||||
|
| (self.sq2.on as u8) << 1
|
||||||
|
| (self.wave.on as u8) << 2
|
||||||
|
| (self.noise.on as u8) << 3;
|
||||||
|
}
|
||||||
|
self.reg(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&mut self, addr: u16, v: u8, cycles: u64) {
|
||||||
|
self.catch_up(cycles);
|
||||||
|
let powered = self.reg(0xFF26) & 0x80 != 0;
|
||||||
|
if !powered && addr != 0xFF26 && addr < 0xFF30 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.regs[(addr - 0xFF10) as usize] = v;
|
||||||
|
match addr {
|
||||||
|
0xFF11 => self.sq1.length = 64 - (v & 0x3F) as u16,
|
||||||
|
0xFF12 => if v & 0xF8 == 0 { self.sq1.on = false },
|
||||||
|
0xFF13 => self.sq1.freq = (self.sq1.freq & 0x700) | v as u16,
|
||||||
|
0xFF14 => {
|
||||||
|
self.sq1.freq = (self.sq1.freq & 0xFF) | ((v as u16 & 7) << 8);
|
||||||
|
self.sq1.length_enable = v & 0x40 != 0;
|
||||||
|
if v & 0x80 != 0 {
|
||||||
|
let (env, nr10) = (self.reg(0xFF12), self.reg(0xFF10));
|
||||||
|
Self::trigger_pulse(&mut self.sq1, env);
|
||||||
|
self.sq1.shadow_freq = self.sq1.freq;
|
||||||
|
let period = (nr10 >> 4) & 7;
|
||||||
|
self.sq1.sweep_timer = if period == 0 { 8 } else { period };
|
||||||
|
self.sq1.sweep_enabled = period != 0 || nr10 & 7 != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xFF16 => self.sq2.length = 64 - (v & 0x3F) as u16,
|
||||||
|
0xFF17 => if v & 0xF8 == 0 { self.sq2.on = false },
|
||||||
|
0xFF18 => self.sq2.freq = (self.sq2.freq & 0x700) | v as u16,
|
||||||
|
0xFF19 => {
|
||||||
|
self.sq2.freq = (self.sq2.freq & 0xFF) | ((v as u16 & 7) << 8);
|
||||||
|
self.sq2.length_enable = v & 0x40 != 0;
|
||||||
|
if v & 0x80 != 0 {
|
||||||
|
let env = self.reg(0xFF17);
|
||||||
|
Self::trigger_pulse(&mut self.sq2, env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xFF1A => if v & 0x80 == 0 { self.wave.on = false },
|
||||||
|
0xFF1B => self.wave.length = 256 - v as u16,
|
||||||
|
0xFF1D => self.wave.freq = (self.wave.freq & 0x700) | v as u16,
|
||||||
|
0xFF1E => {
|
||||||
|
self.wave.freq = (self.wave.freq & 0xFF) | ((v as u16 & 7) << 8);
|
||||||
|
self.wave.length_enable = v & 0x40 != 0;
|
||||||
|
if v & 0x80 != 0 {
|
||||||
|
self.wave.on = self.reg(0xFF1A) & 0x80 != 0;
|
||||||
|
if self.wave.length == 0 {
|
||||||
|
self.wave.length = 256;
|
||||||
|
}
|
||||||
|
self.wave.phase = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xFF20 => self.noise.length = 64 - (v & 0x3F) as u16,
|
||||||
|
0xFF21 => if v & 0xF8 == 0 { self.noise.on = false },
|
||||||
|
0xFF23 => {
|
||||||
|
self.noise.length_enable = v & 0x40 != 0;
|
||||||
|
if v & 0x80 != 0 {
|
||||||
|
let env = self.reg(0xFF21);
|
||||||
|
self.noise.on = env & 0xF8 != 0;
|
||||||
|
if self.noise.length == 0 {
|
||||||
|
self.noise.length = 64;
|
||||||
|
}
|
||||||
|
self.noise.env.load(env);
|
||||||
|
self.noise.lfsr = 0x7FFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xFF26 => {
|
||||||
|
if v & 0x80 == 0 {
|
||||||
|
for r in &mut self.regs[..0x16] {
|
||||||
|
*r = 0;
|
||||||
|
}
|
||||||
|
self.sq1.on = false;
|
||||||
|
self.sq2.on = false;
|
||||||
|
self.wave.on = false;
|
||||||
|
self.noise.on = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trigger_pulse(p: &mut Pulse, env: u8) {
|
||||||
|
p.on = env & 0xF8 != 0;
|
||||||
|
if p.length == 0 {
|
||||||
|
p.length = 64;
|
||||||
|
}
|
||||||
|
p.env.load(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sweep_calc(&mut self) -> u16 {
|
||||||
|
let nr10 = self.reg(0xFF10);
|
||||||
|
let delta = self.sq1.shadow_freq >> (nr10 & 7);
|
||||||
|
let f = if nr10 & 0x08 != 0 {
|
||||||
|
self.sq1.shadow_freq.wrapping_sub(delta)
|
||||||
|
} else {
|
||||||
|
self.sq1.shadow_freq + delta
|
||||||
|
};
|
||||||
|
if f > 2047 {
|
||||||
|
self.sq1.on = false;
|
||||||
|
}
|
||||||
|
f
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 512 Hz frame sequencer: lengths, sweep, envelopes.
|
||||||
|
fn sequencer_step(&mut self) {
|
||||||
|
let step = self.seq_step;
|
||||||
|
self.seq_step = (self.seq_step + 1) & 7;
|
||||||
|
if step % 2 == 0 {
|
||||||
|
for (on, len, en) in [
|
||||||
|
(&mut self.sq1.on, &mut self.sq1.length, self.sq1.length_enable),
|
||||||
|
(&mut self.sq2.on, &mut self.sq2.length, self.sq2.length_enable),
|
||||||
|
(&mut self.wave.on, &mut self.wave.length, self.wave.length_enable),
|
||||||
|
(&mut self.noise.on, &mut self.noise.length, self.noise.length_enable),
|
||||||
|
] {
|
||||||
|
if en && *len > 0 {
|
||||||
|
*len -= 1;
|
||||||
|
if *len == 0 {
|
||||||
|
*on = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if step == 2 || step == 6 {
|
||||||
|
let nr10 = self.reg(0xFF10);
|
||||||
|
let period = (nr10 >> 4) & 7;
|
||||||
|
if self.sq1.sweep_timer > 0 {
|
||||||
|
self.sq1.sweep_timer -= 1;
|
||||||
|
}
|
||||||
|
if self.sq1.sweep_timer == 0 {
|
||||||
|
self.sq1.sweep_timer = if period == 0 { 8 } else { period };
|
||||||
|
if self.sq1.sweep_enabled && period != 0 {
|
||||||
|
let f = self.sweep_calc();
|
||||||
|
if f <= 2047 && nr10 & 7 != 0 {
|
||||||
|
self.sq1.shadow_freq = f;
|
||||||
|
self.sq1.freq = f;
|
||||||
|
self.sweep_calc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if step == 7 {
|
||||||
|
self.sq1.env.clock();
|
||||||
|
self.sq2.env.clock();
|
||||||
|
self.noise.env.clock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synthesize samples up to CPU cycle `cycles`.
|
||||||
|
pub fn catch_up(&mut self, cycles: u64) {
|
||||||
|
let rate = self.sample_rate as f32;
|
||||||
|
let target = cycles * self.sample_rate as u64 / CPU_HZ;
|
||||||
|
while self.samples_made < target {
|
||||||
|
self.samples_made += 1;
|
||||||
|
let cyc = self.samples_made * CPU_HZ / self.sample_rate as u64;
|
||||||
|
// frame sequencer runs every 8192 CPU cycles
|
||||||
|
self.seq_cycles += cyc - self.last_cycle;
|
||||||
|
self.last_cycle = cyc;
|
||||||
|
while self.seq_cycles >= 8192 {
|
||||||
|
self.seq_cycles -= 8192;
|
||||||
|
self.sequencer_step();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ch = [0.0f32; 4];
|
||||||
|
let duty1 = (self.reg(0xFF11) >> 6) as usize;
|
||||||
|
let duty2 = (self.reg(0xFF16) >> 6) as usize;
|
||||||
|
for (i, (p, duty)) in [(&mut self.sq1, duty1), (&mut self.sq2, duty2)].into_iter().enumerate() {
|
||||||
|
let hz = 131072.0 / (2048 - p.freq) as f32;
|
||||||
|
p.phase = (p.phase + hz * 8.0 / rate) % 8.0;
|
||||||
|
if p.on {
|
||||||
|
let high = DUTY[duty][p.phase as usize] != 0;
|
||||||
|
ch[i] = p.env.volume as f32 / 15.0 * if high { 1.0 } else { -1.0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let hz = 65536.0 / (2048 - self.wave.freq) as f32;
|
||||||
|
self.wave.phase = (self.wave.phase + hz * 32.0 / rate) % 32.0;
|
||||||
|
let shift = match (self.reg(0xFF1C) >> 5) & 3 {
|
||||||
|
0 => 8,
|
||||||
|
n => n - 1,
|
||||||
|
};
|
||||||
|
if self.wave.on && shift < 8 {
|
||||||
|
let idx = self.wave.phase as usize;
|
||||||
|
let byte = self.regs[0x20 + idx / 2];
|
||||||
|
let nib = if idx % 2 == 0 { byte >> 4 } else { byte & 0x0F };
|
||||||
|
ch[2] = (nib >> shift) as f32 / 7.5 - 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let nr43 = self.reg(0xFF22);
|
||||||
|
let div = match nr43 & 7 { 0 => 0.5, d => d as f32 };
|
||||||
|
let hz = 524288.0 / div / (1u32 << ((nr43 >> 4) + 1)) as f32;
|
||||||
|
self.noise.acc += hz / rate;
|
||||||
|
while self.noise.acc >= 1.0 {
|
||||||
|
self.noise.acc -= 1.0;
|
||||||
|
let x = (self.noise.lfsr ^ (self.noise.lfsr >> 1)) & 1;
|
||||||
|
self.noise.lfsr = (self.noise.lfsr >> 1) | (x << 14);
|
||||||
|
if nr43 & 0x08 != 0 {
|
||||||
|
self.noise.lfsr = (self.noise.lfsr & !0x40) | (x << 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.noise.on {
|
||||||
|
let high = self.noise.lfsr & 1 == 0;
|
||||||
|
ch[3] = self.noise.env.volume as f32 / 15.0 * if high { 1.0 } else { -1.0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nr50 = self.reg(0xFF24);
|
||||||
|
let nr51 = self.reg(0xFF25);
|
||||||
|
let (mut l, mut r) = (0.0, 0.0);
|
||||||
|
for (i, v) in ch.iter().enumerate() {
|
||||||
|
if nr51 & (0x10 << i) != 0 { l += v; }
|
||||||
|
if nr51 & (0x01 << i) != 0 { r += v; }
|
||||||
|
}
|
||||||
|
l *= ((nr50 >> 4) & 7) as f32 + 1.0;
|
||||||
|
r *= (nr50 & 7) as f32 + 1.0;
|
||||||
|
let scale = 0.25 / 8.0;
|
||||||
|
let l = Self::highpass(&mut self.hp_l, l * scale);
|
||||||
|
let r = Self::highpass(&mut self.hp_r, r * scale);
|
||||||
|
self.out.push(l);
|
||||||
|
self.out.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highpass(state: &mut (f32, f32), x: f32) -> f32 {
|
||||||
|
let y = x - state.0 + 0.996 * state.1;
|
||||||
|
*state = (x, y);
|
||||||
|
y
|
||||||
|
}
|
||||||
|
}
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
//! Headless runner for testing: `daedalian --headless OUT_DIR SCRIPT...`
|
||||||
|
//! Script tokens: `N` run N frames; `BUTTON:N` hold a button for N frames
|
||||||
|
//! (up down left right a b start select); `shot:NAME` save OUT_DIR/NAME.png.
|
||||||
|
//! All audio is written to OUT_DIR/audio.wav.
|
||||||
|
|
||||||
|
use crate::machine::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn crc32(data: &[u8]) -> u32 {
|
||||||
|
let mut crc = 0xFFFF_FFFFu32;
|
||||||
|
for &b in data {
|
||||||
|
crc ^= b as u32;
|
||||||
|
for _ in 0..8 {
|
||||||
|
crc = if crc & 1 != 0 { 0xEDB8_8320 ^ (crc >> 1) } else { crc >> 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
!crc
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_png(path: &Path, w: usize, h: usize, gray: &[u8]) {
|
||||||
|
let mut raw = Vec::new();
|
||||||
|
for y in 0..h {
|
||||||
|
raw.push(0);
|
||||||
|
raw.extend_from_slice(&gray[y * w..(y + 1) * w]);
|
||||||
|
}
|
||||||
|
// zlib stream with stored (uncompressed) deflate blocks
|
||||||
|
let mut z = vec![0x78, 0x01];
|
||||||
|
for (i, chunk) in raw.chunks(65535).enumerate() {
|
||||||
|
let last = (i + 1) * 65535 >= raw.len();
|
||||||
|
z.push(last as u8);
|
||||||
|
z.extend_from_slice(&(chunk.len() as u16).to_le_bytes());
|
||||||
|
z.extend_from_slice(&(!(chunk.len() as u16)).to_le_bytes());
|
||||||
|
z.extend_from_slice(chunk);
|
||||||
|
}
|
||||||
|
let (mut a, mut b) = (1u32, 0u32);
|
||||||
|
for &x in &raw {
|
||||||
|
a = (a + x as u32) % 65521;
|
||||||
|
b = (b + a) % 65521;
|
||||||
|
}
|
||||||
|
z.extend_from_slice(&(b << 16 | a).to_be_bytes());
|
||||||
|
let mut png = b"\x89PNG\r\n\x1a\n".to_vec();
|
||||||
|
let mut chunk = |t: &[u8], d: &[u8]| {
|
||||||
|
png.extend_from_slice(&(d.len() as u32).to_be_bytes());
|
||||||
|
let mut td = t.to_vec();
|
||||||
|
td.extend_from_slice(d);
|
||||||
|
png.extend_from_slice(&td);
|
||||||
|
png.extend_from_slice(&crc32(&td).to_be_bytes());
|
||||||
|
};
|
||||||
|
let mut ihdr = Vec::new();
|
||||||
|
ihdr.extend_from_slice(&(w as u32).to_be_bytes());
|
||||||
|
ihdr.extend_from_slice(&(h as u32).to_be_bytes());
|
||||||
|
ihdr.extend_from_slice(&[8, 0, 0, 0, 0]);
|
||||||
|
chunk(b"IHDR", &ihdr);
|
||||||
|
chunk(b"IDAT", &z);
|
||||||
|
chunk(b"IEND", &[]);
|
||||||
|
fs::write(path, png).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_wav(path: &Path, rate: u32, samples: &[f32]) {
|
||||||
|
let mut d = Vec::new();
|
||||||
|
let data_len = samples.len() as u32 * 2;
|
||||||
|
d.extend_from_slice(b"RIFF");
|
||||||
|
d.extend_from_slice(&(36 + data_len).to_le_bytes());
|
||||||
|
d.extend_from_slice(b"WAVEfmt ");
|
||||||
|
d.extend_from_slice(&16u32.to_le_bytes());
|
||||||
|
d.extend_from_slice(&1u16.to_le_bytes());
|
||||||
|
d.extend_from_slice(&2u16.to_le_bytes());
|
||||||
|
d.extend_from_slice(&rate.to_le_bytes());
|
||||||
|
d.extend_from_slice(&(rate * 4).to_le_bytes());
|
||||||
|
d.extend_from_slice(&4u16.to_le_bytes());
|
||||||
|
d.extend_from_slice(&16u16.to_le_bytes());
|
||||||
|
d.extend_from_slice(b"data");
|
||||||
|
d.extend_from_slice(&data_len.to_le_bytes());
|
||||||
|
for s in samples {
|
||||||
|
d.extend_from_slice(&((s.clamp(-1.0, 1.0) * 32767.0) as i16).to_le_bytes());
|
||||||
|
}
|
||||||
|
fs::write(path, d).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(rom: &[u8], args: &[String]) {
|
||||||
|
let out = Path::new(&args[0]);
|
||||||
|
fs::create_dir_all(out).unwrap();
|
||||||
|
let rate = 44_100;
|
||||||
|
let mut m = Machine::new(rom, rate);
|
||||||
|
let mut audio = Vec::new();
|
||||||
|
let mut frames = 0u64;
|
||||||
|
let run = |m: &mut Machine, n: u64, audio: &mut Vec<f32>, frames: &mut u64| {
|
||||||
|
for _ in 0..n {
|
||||||
|
m.run_frame();
|
||||||
|
audio.append(&mut m.apu.out);
|
||||||
|
*frames += 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for tok in &args[1..] {
|
||||||
|
if let Ok(n) = tok.parse::<u64>() {
|
||||||
|
run(&mut m, n, &mut audio, &mut frames);
|
||||||
|
} else if let Some(name) = tok.strip_prefix("shot:") {
|
||||||
|
let gray: Vec<u8> = m.framebuffer.iter().map(|&s| [0xFF, 0xAA, 0x55, 0x00][s as usize]).collect();
|
||||||
|
write_png(&out.join(format!("{name}.png")), SCREEN_W, SCREEN_H, &gray);
|
||||||
|
println!("frame {frames}: {name}.png (pc ${:04X})", m.pc);
|
||||||
|
} else if let Some(spec) = tok.strip_prefix("goto:") {
|
||||||
|
// steer the puzzle cursor (wPieceY/wPieceX at $C0A1/$C0A2) to a position
|
||||||
|
let (ty, tx) = spec.split_once(':').unwrap();
|
||||||
|
let (ty, tx) = (u8::from_str_radix(ty, 16).unwrap(), u8::from_str_radix(tx, 16).unwrap());
|
||||||
|
for _ in 0..100 {
|
||||||
|
let (cy, cx) = (m.r8(0xC0A1), m.r8(0xC0A2));
|
||||||
|
let bit = if cy < ty { BTN_DOWN } else if cy > ty { BTN_UP }
|
||||||
|
else if cx < tx { BTN_RIGHT } else if cx > tx { BTN_LEFT } else { break };
|
||||||
|
m.buttons |= bit;
|
||||||
|
run(&mut m, 1, &mut audio, &mut frames);
|
||||||
|
m.buttons &= !bit;
|
||||||
|
run(&mut m, 5, &mut audio, &mut frames);
|
||||||
|
}
|
||||||
|
} else if let Some(spec) = tok.strip_prefix("peek:") {
|
||||||
|
let (addr, n) = spec.split_once(':').unwrap_or((spec, "1"));
|
||||||
|
let addr = u16::from_str_radix(addr, 16).unwrap();
|
||||||
|
let vals: Vec<String> = (0..n.parse::<u16>().unwrap())
|
||||||
|
.map(|i| format!("{:02X}", m.r8(addr + i)))
|
||||||
|
.collect();
|
||||||
|
println!("frame {frames}: ${addr:04X}: {}", vals.join(" "));
|
||||||
|
} else if let Some((btn, n)) = tok.split_once(':') {
|
||||||
|
let bit = match btn {
|
||||||
|
"up" => BTN_UP, "down" => BTN_DOWN, "left" => BTN_LEFT, "right" => BTN_RIGHT,
|
||||||
|
"a" => BTN_A, "b" => BTN_B, "start" => BTN_START, "select" => BTN_SELECT,
|
||||||
|
_ => panic!("unknown token {tok}"),
|
||||||
|
};
|
||||||
|
m.buttons |= bit;
|
||||||
|
run(&mut m, n.parse().unwrap(), &mut audio, &mut frames);
|
||||||
|
m.buttons &= !bit;
|
||||||
|
} else {
|
||||||
|
panic!("unknown token {tok}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write_wav(&out.join("audio.wav"), rate, &audio);
|
||||||
|
println!("{frames} frames, {:.1}s of audio", audio.len() as f32 / 2.0 / rate as f32);
|
||||||
|
}
|
||||||
+399
@@ -0,0 +1,399 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
//! The hardware the recompiled code runs against: SM83 registers and flags,
|
||||||
|
//! the memory map, the LCD (rendered once per frame at vblank), the joypad,
|
||||||
|
//! OAM DMA, the vblank interrupt, and the APU (in apu.rs).
|
||||||
|
|
||||||
|
use crate::apu::Apu;
|
||||||
|
use crate::game;
|
||||||
|
|
||||||
|
pub const SCREEN_W: usize = 160;
|
||||||
|
pub const SCREEN_H: usize = 144;
|
||||||
|
pub const CPU_HZ: u64 = 4_194_304;
|
||||||
|
const CYCLES_PER_LINE: u32 = 456;
|
||||||
|
const CYCLES_PER_FRAME: u32 = 70_224;
|
||||||
|
const VBLANK_START: u32 = 144 * CYCLES_PER_LINE;
|
||||||
|
|
||||||
|
// joypad bits (1 = pressed)
|
||||||
|
pub const BTN_RIGHT: u8 = 0x01;
|
||||||
|
pub const BTN_LEFT: u8 = 0x02;
|
||||||
|
pub const BTN_UP: u8 = 0x04;
|
||||||
|
pub const BTN_DOWN: u8 = 0x08;
|
||||||
|
pub const BTN_A: u8 = 0x10;
|
||||||
|
pub const BTN_B: u8 = 0x20;
|
||||||
|
pub const BTN_SELECT: u8 = 0x40;
|
||||||
|
pub const BTN_START: u8 = 0x80;
|
||||||
|
|
||||||
|
pub struct Machine {
|
||||||
|
pub a: u8,
|
||||||
|
pub f: u8,
|
||||||
|
pub b: u8,
|
||||||
|
pub c: u8,
|
||||||
|
pub d: u8,
|
||||||
|
pub e: u8,
|
||||||
|
pub h: u8,
|
||||||
|
pub l: u8,
|
||||||
|
pub sp: u16,
|
||||||
|
pub pc: u16,
|
||||||
|
pub ime: bool,
|
||||||
|
halted: bool,
|
||||||
|
|
||||||
|
mem: Vec<u8>,
|
||||||
|
pub cycles: u64,
|
||||||
|
frame_cycle: u32,
|
||||||
|
vblank_fired: bool,
|
||||||
|
frame_ready: bool,
|
||||||
|
|
||||||
|
pub buttons: u8,
|
||||||
|
pub apu: Apu,
|
||||||
|
/// shade index 0 (lightest) .. 3 (darkest) per pixel
|
||||||
|
pub framebuffer: Vec<u8>,
|
||||||
|
warned_code_read: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Machine {
|
||||||
|
pub fn new(rom_data: &[u8], sample_rate: u32) -> Self {
|
||||||
|
let mut mem = vec![0u8; 0x10000];
|
||||||
|
mem[..0x8000].copy_from_slice(&rom_data[..0x8000]);
|
||||||
|
let mut m = Machine {
|
||||||
|
// register state left by the boot ROM
|
||||||
|
a: 0x01, f: 0xB0, b: 0x00, c: 0x13, d: 0x00, e: 0xD8, h: 0x01, l: 0x4D,
|
||||||
|
sp: 0xFFFE,
|
||||||
|
pc: 0x0100,
|
||||||
|
ime: false,
|
||||||
|
halted: false,
|
||||||
|
mem,
|
||||||
|
cycles: 0,
|
||||||
|
frame_cycle: 0,
|
||||||
|
vblank_fired: false,
|
||||||
|
frame_ready: false,
|
||||||
|
buttons: 0,
|
||||||
|
apu: Apu::new(sample_rate),
|
||||||
|
framebuffer: vec![0; SCREEN_W * SCREEN_H],
|
||||||
|
warned_code_read: false,
|
||||||
|
};
|
||||||
|
m.mem[0xFF40] = 0x91;
|
||||||
|
m.mem[0xFF47] = 0xFC;
|
||||||
|
m.mem[0xFF00] = 0xCF;
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run recompiled code until the next vblank (one frame).
|
||||||
|
pub fn run_frame(&mut self) {
|
||||||
|
self.frame_ready = false;
|
||||||
|
while !self.frame_ready {
|
||||||
|
let pending = self.mem[0xFFFF] & self.mem[0xFF0F] & 0x1F;
|
||||||
|
if self.halted {
|
||||||
|
if pending != 0 {
|
||||||
|
self.halted = false;
|
||||||
|
} else {
|
||||||
|
self.tick(4);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.ime && pending != 0 {
|
||||||
|
// only vblank is ever enabled by this game
|
||||||
|
let bit = pending.trailing_zeros() as u16;
|
||||||
|
self.mem[0xFF0F] &= !(1 << bit);
|
||||||
|
self.ime = false;
|
||||||
|
let pc = self.pc;
|
||||||
|
self.push(pc);
|
||||||
|
self.pc = 0x40 + bit * 8;
|
||||||
|
self.tick(20);
|
||||||
|
}
|
||||||
|
game::step(self);
|
||||||
|
}
|
||||||
|
let cycles = self.cycles;
|
||||||
|
self.apu.catch_up(cycles);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self, t: u32) {
|
||||||
|
self.cycles += t as u64;
|
||||||
|
self.frame_cycle += t;
|
||||||
|
loop {
|
||||||
|
if !self.vblank_fired && self.frame_cycle >= VBLANK_START {
|
||||||
|
self.vblank_fired = true;
|
||||||
|
if self.mem[0xFF40] & 0x80 != 0 {
|
||||||
|
self.mem[0xFF0F] |= 0x01;
|
||||||
|
}
|
||||||
|
self.render();
|
||||||
|
self.frame_ready = true;
|
||||||
|
} else if self.frame_cycle >= CYCLES_PER_FRAME {
|
||||||
|
self.frame_cycle -= CYCLES_PER_FRAME;
|
||||||
|
self.vblank_fired = false;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn halt(&mut self) {
|
||||||
|
self.halted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- memory bus ----------------
|
||||||
|
|
||||||
|
pub fn r8(&mut self, addr: u16) -> u8 {
|
||||||
|
match addr {
|
||||||
|
0x0000..=0x7FFF => {
|
||||||
|
if !self.warned_code_read
|
||||||
|
&& !(0x1141..0x114B).contains(&addr) // OAM DMA routine, copied to HRAM
|
||||||
|
&& game::CODE_RANGES.iter().any(|&(s, e)| addr >= s && addr < e)
|
||||||
|
{
|
||||||
|
self.warned_code_read = true;
|
||||||
|
eprintln!("warning: game read code byte ${:04X} as data (pc ${:04X})", addr, self.pc);
|
||||||
|
}
|
||||||
|
self.mem[addr as usize]
|
||||||
|
}
|
||||||
|
0xE000..=0xFDFF => self.mem[addr as usize - 0x2000],
|
||||||
|
0xFF00 => {
|
||||||
|
let sel = self.mem[0xFF00] & 0x30;
|
||||||
|
let mut low = 0x0F;
|
||||||
|
if sel & 0x10 == 0 {
|
||||||
|
low &= !(self.buttons & 0x0F);
|
||||||
|
}
|
||||||
|
if sel & 0x20 == 0 {
|
||||||
|
low &= !(self.buttons >> 4);
|
||||||
|
}
|
||||||
|
0xC0 | sel | low
|
||||||
|
}
|
||||||
|
0xFF44 => (self.frame_cycle / CYCLES_PER_LINE) as u8,
|
||||||
|
0xFF10..=0xFF3F => {
|
||||||
|
let cycles = self.cycles;
|
||||||
|
self.apu.read(addr, cycles)
|
||||||
|
}
|
||||||
|
_ => self.mem[addr as usize],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn w8(&mut self, addr: u16, v: u8) {
|
||||||
|
match addr {
|
||||||
|
0x0000..=0x7FFF => {} // ROM-only cart: writes do nothing
|
||||||
|
0xE000..=0xFDFF => self.mem[addr as usize - 0x2000] = v,
|
||||||
|
0xFF00 => self.mem[0xFF00] = v & 0x30,
|
||||||
|
0xFF04 | 0xFF44 => {}
|
||||||
|
0xFF46 => {
|
||||||
|
let src = (v as usize) << 8;
|
||||||
|
for i in 0..0xA0 {
|
||||||
|
self.mem[0xFE00 + i] = self.mem[src + i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xFF10..=0xFF3F => {
|
||||||
|
let cycles = self.cycles;
|
||||||
|
self.apu.write(addr, v, cycles);
|
||||||
|
}
|
||||||
|
_ => self.mem[addr as usize] = v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, v: u16) {
|
||||||
|
self.sp = self.sp.wrapping_sub(1);
|
||||||
|
self.w8(self.sp, (v >> 8) as u8);
|
||||||
|
self.sp = self.sp.wrapping_sub(1);
|
||||||
|
self.w8(self.sp, v as u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop(&mut self) -> u16 {
|
||||||
|
let lo = self.r8(self.sp) as u16;
|
||||||
|
self.sp = self.sp.wrapping_add(1);
|
||||||
|
let hi = self.r8(self.sp) as u16;
|
||||||
|
self.sp = self.sp.wrapping_add(1);
|
||||||
|
hi << 8 | lo
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- register pairs ----------------
|
||||||
|
|
||||||
|
pub fn af(&self) -> u16 { (self.a as u16) << 8 | self.f as u16 }
|
||||||
|
pub fn bc(&self) -> u16 { (self.b as u16) << 8 | self.c as u16 }
|
||||||
|
pub fn de(&self) -> u16 { (self.d as u16) << 8 | self.e as u16 }
|
||||||
|
pub fn hl(&self) -> u16 { (self.h as u16) << 8 | self.l as u16 }
|
||||||
|
pub fn set_af(&mut self, v: u16) { self.a = (v >> 8) as u8; self.f = v as u8 & 0xF0; }
|
||||||
|
pub fn set_bc(&mut self, v: u16) { self.b = (v >> 8) as u8; self.c = v as u8; }
|
||||||
|
pub fn set_de(&mut self, v: u16) { self.d = (v >> 8) as u8; self.e = v as u8; }
|
||||||
|
pub fn set_hl(&mut self, v: u16) { self.h = (v >> 8) as u8; self.l = v as u8; }
|
||||||
|
|
||||||
|
// ---------------- flags ----------------
|
||||||
|
|
||||||
|
pub fn zf(&self) -> bool { self.f & 0x80 != 0 }
|
||||||
|
pub fn cf(&self) -> bool { self.f & 0x10 != 0 }
|
||||||
|
fn hf(&self) -> bool { self.f & 0x20 != 0 }
|
||||||
|
fn nf(&self) -> bool { self.f & 0x40 != 0 }
|
||||||
|
fn flags(&mut self, z: bool, n: bool, h: bool, c: bool) {
|
||||||
|
self.f = (z as u8) << 7 | (n as u8) << 6 | (h as u8) << 5 | (c as u8) << 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- ALU ----------------
|
||||||
|
|
||||||
|
pub fn add(&mut self, v: u8) {
|
||||||
|
let (a, r) = (self.a, self.a as u16 + v as u16);
|
||||||
|
self.a = r as u8;
|
||||||
|
self.flags(self.a == 0, false, (a & 0xF) + (v & 0xF) > 0xF, r > 0xFF);
|
||||||
|
}
|
||||||
|
pub fn adc(&mut self, v: u8) {
|
||||||
|
let c = self.cf() as u16;
|
||||||
|
let (a, r) = (self.a, self.a as u16 + v as u16 + c);
|
||||||
|
self.a = r as u8;
|
||||||
|
self.flags(self.a == 0, false, (a & 0xF) as u16 + (v & 0xF) as u16 + c > 0xF, r > 0xFF);
|
||||||
|
}
|
||||||
|
pub fn sub(&mut self, v: u8) {
|
||||||
|
let a = self.a;
|
||||||
|
self.a = a.wrapping_sub(v);
|
||||||
|
self.flags(self.a == 0, true, (a & 0xF) < (v & 0xF), a < v);
|
||||||
|
}
|
||||||
|
pub fn sbc(&mut self, v: u8) {
|
||||||
|
let c = self.cf() as u16;
|
||||||
|
let a = self.a;
|
||||||
|
let r = (a as u16).wrapping_sub(v as u16).wrapping_sub(c);
|
||||||
|
self.a = r as u8;
|
||||||
|
self.flags(self.a == 0, true, ((a & 0xF) as u16) < (v & 0xF) as u16 + c, (a as u16) < v as u16 + c);
|
||||||
|
}
|
||||||
|
pub fn and(&mut self, v: u8) { self.a &= v; self.flags(self.a == 0, false, true, false); }
|
||||||
|
pub fn xor(&mut self, v: u8) { self.a ^= v; self.flags(self.a == 0, false, false, false); }
|
||||||
|
pub fn or(&mut self, v: u8) { self.a |= v; self.flags(self.a == 0, false, false, false); }
|
||||||
|
pub fn cp(&mut self, v: u8) {
|
||||||
|
let a = self.a;
|
||||||
|
self.flags(a == v, true, (a & 0xF) < (v & 0xF), a < v);
|
||||||
|
}
|
||||||
|
pub fn inc(&mut self, v: u8) -> u8 {
|
||||||
|
let r = v.wrapping_add(1);
|
||||||
|
let c = self.cf();
|
||||||
|
self.flags(r == 0, false, v & 0xF == 0xF, c);
|
||||||
|
r
|
||||||
|
}
|
||||||
|
pub fn dec(&mut self, v: u8) -> u8 {
|
||||||
|
let r = v.wrapping_sub(1);
|
||||||
|
let c = self.cf();
|
||||||
|
self.flags(r == 0, true, v & 0xF == 0, c);
|
||||||
|
r
|
||||||
|
}
|
||||||
|
pub fn add_hl(&mut self, v: u16) {
|
||||||
|
let hl = self.hl();
|
||||||
|
let r = hl as u32 + v as u32;
|
||||||
|
let z = self.zf();
|
||||||
|
self.flags(z, false, (hl & 0xFFF) + (v & 0xFFF) > 0xFFF, r > 0xFFFF);
|
||||||
|
self.set_hl(r as u16);
|
||||||
|
}
|
||||||
|
/// sp + signed e, with the flags of add sp,e / ld hl,sp+e
|
||||||
|
pub fn add_sp(&mut self, e: i8) -> u16 {
|
||||||
|
let sp = self.sp;
|
||||||
|
let v = e as i16 as u16;
|
||||||
|
self.flags(false, false, (sp & 0xF) + (v & 0xF) > 0xF, (sp & 0xFF) + (v & 0xFF) > 0xFF);
|
||||||
|
sp.wrapping_add(v)
|
||||||
|
}
|
||||||
|
pub fn rlca(&mut self) { let c = self.a >> 7; self.a = self.a.rotate_left(1); self.flags(false, false, false, c != 0); }
|
||||||
|
pub fn rrca(&mut self) { let c = self.a & 1; self.a = self.a.rotate_right(1); self.flags(false, false, false, c != 0); }
|
||||||
|
pub fn rla(&mut self) { let c = self.a >> 7; self.a = self.a << 1 | self.cf() as u8; self.flags(false, false, false, c != 0); }
|
||||||
|
pub fn rra(&mut self) { let c = self.a & 1; self.a = self.a >> 1 | (self.cf() as u8) << 7; self.flags(false, false, false, c != 0); }
|
||||||
|
pub fn daa(&mut self) {
|
||||||
|
let mut a = self.a;
|
||||||
|
let mut c = self.cf();
|
||||||
|
if !self.nf() {
|
||||||
|
if c || a > 0x99 { a = a.wrapping_add(0x60); c = true; }
|
||||||
|
if self.hf() || a & 0x0F > 0x09 { a = a.wrapping_add(0x06); }
|
||||||
|
} else {
|
||||||
|
if c { a = a.wrapping_sub(0x60); }
|
||||||
|
if self.hf() { a = a.wrapping_sub(0x06); }
|
||||||
|
}
|
||||||
|
self.a = a;
|
||||||
|
let n = self.nf();
|
||||||
|
self.flags(a == 0, n, false, c);
|
||||||
|
}
|
||||||
|
pub fn cpl(&mut self) { self.a = !self.a; self.f |= 0x60; }
|
||||||
|
pub fn scf(&mut self) { self.f = (self.f & 0x80) | 0x10; }
|
||||||
|
pub fn ccf(&mut self) { self.f = (self.f & 0x80) | if self.cf() { 0 } else { 0x10 }; }
|
||||||
|
|
||||||
|
// CB-prefixed
|
||||||
|
pub fn rlc(&mut self, v: u8) -> u8 { let r = v.rotate_left(1); self.flags(r == 0, false, false, v & 0x80 != 0); r }
|
||||||
|
pub fn rrc(&mut self, v: u8) -> u8 { let r = v.rotate_right(1); self.flags(r == 0, false, false, v & 1 != 0); r }
|
||||||
|
pub fn rl(&mut self, v: u8) -> u8 { let r = v << 1 | self.cf() as u8; self.flags(r == 0, false, false, v & 0x80 != 0); r }
|
||||||
|
pub fn rr(&mut self, v: u8) -> u8 { let r = v >> 1 | (self.cf() as u8) << 7; self.flags(r == 0, false, false, v & 1 != 0); r }
|
||||||
|
pub fn sla(&mut self, v: u8) -> u8 { let r = v << 1; self.flags(r == 0, false, false, v & 0x80 != 0); r }
|
||||||
|
pub fn sra(&mut self, v: u8) -> u8 { let r = (v as i8 >> 1) as u8; self.flags(r == 0, false, false, v & 1 != 0); r }
|
||||||
|
pub fn swap(&mut self, v: u8) -> u8 { let r = v.rotate_left(4); self.flags(r == 0, false, false, false); r }
|
||||||
|
pub fn srl(&mut self, v: u8) -> u8 { let r = v >> 1; self.flags(r == 0, false, false, v & 1 != 0); r }
|
||||||
|
pub fn bit(&mut self, n: u8, v: u8) {
|
||||||
|
let c = self.cf();
|
||||||
|
self.flags(v & (1 << n) == 0, false, true, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- LCD ----------------
|
||||||
|
|
||||||
|
fn tile_pixel(&self, tile_addr: usize, x: usize, y: usize) -> u8 {
|
||||||
|
let lo = self.mem[tile_addr + y * 2];
|
||||||
|
let hi = self.mem[tile_addr + y * 2 + 1];
|
||||||
|
((lo >> (7 - x)) & 1) | (((hi >> (7 - x)) & 1) << 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bg_tile_addr(&self, tile: u8) -> usize {
|
||||||
|
if self.mem[0xFF40] & 0x10 != 0 {
|
||||||
|
0x8000 + tile as usize * 16
|
||||||
|
} else {
|
||||||
|
(0x9000i32 + (tile as i8 as i32) * 16) as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the whole frame from the current VRAM/OAM/register state.
|
||||||
|
fn render(&mut self) {
|
||||||
|
let lcdc = self.mem[0xFF40];
|
||||||
|
if lcdc & 0x80 == 0 {
|
||||||
|
self.framebuffer.fill(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let shade = |pal: u8, c: u8| (pal >> (c * 2)) & 3;
|
||||||
|
let bgp = self.mem[0xFF47];
|
||||||
|
let (scy, scx) = (self.mem[0xFF42] as usize, self.mem[0xFF43] as usize);
|
||||||
|
let (wy, wx) = (self.mem[0xFF4A] as usize, self.mem[0xFF4B] as usize);
|
||||||
|
let mut bg_color = vec![0u8; SCREEN_W * SCREEN_H];
|
||||||
|
for y in 0..SCREEN_H {
|
||||||
|
for x in 0..SCREEN_W {
|
||||||
|
let mut c = 0;
|
||||||
|
if lcdc & 0x01 != 0 {
|
||||||
|
let (map, mx, my) = if lcdc & 0x20 != 0 && y >= wy && x + 7 >= wx {
|
||||||
|
(if lcdc & 0x40 != 0 { 0x9C00 } else { 0x9800 }, x + 7 - wx, y - wy)
|
||||||
|
} else {
|
||||||
|
(if lcdc & 0x08 != 0 { 0x9C00 } else { 0x9800 }, (x + scx) & 255, (y + scy) & 255)
|
||||||
|
};
|
||||||
|
let tile = self.mem[map + (my / 8) * 32 + mx / 8];
|
||||||
|
c = self.tile_pixel(self.bg_tile_addr(tile), mx % 8, my % 8);
|
||||||
|
}
|
||||||
|
bg_color[y * SCREEN_W + x] = c;
|
||||||
|
self.framebuffer[y * SCREEN_W + x] = shade(bgp, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lcdc & 0x02 != 0 {
|
||||||
|
let tall = lcdc & 0x04 != 0;
|
||||||
|
let height = if tall { 16 } else { 8 };
|
||||||
|
// lower OAM index wins: draw from the end
|
||||||
|
for i in (0..40).rev() {
|
||||||
|
let o = 0xFE00 + i * 4;
|
||||||
|
let sy = self.mem[o] as i32 - 16;
|
||||||
|
let sx = self.mem[o + 1] as i32 - 8;
|
||||||
|
let mut tile = self.mem[o + 2];
|
||||||
|
let attr = self.mem[o + 3];
|
||||||
|
if tall {
|
||||||
|
tile &= 0xFE;
|
||||||
|
}
|
||||||
|
let pal = if attr & 0x10 != 0 { self.mem[0xFF49] } else { self.mem[0xFF48] };
|
||||||
|
for ty in 0..height {
|
||||||
|
let py = sy + ty as i32;
|
||||||
|
if py < 0 || py >= SCREEN_H as i32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let row = if attr & 0x40 != 0 { height - 1 - ty } else { ty };
|
||||||
|
for tx in 0..8 {
|
||||||
|
let px = sx + tx as i32;
|
||||||
|
if px < 0 || px >= SCREEN_W as i32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let col = if attr & 0x20 != 0 { 7 - tx } else { tx };
|
||||||
|
let c = self.tile_pixel(0x8000 + tile as usize * 16, col, row);
|
||||||
|
let idx = py as usize * SCREEN_W + px as usize;
|
||||||
|
if c == 0 || (attr & 0x80 != 0 && bg_color[idx] != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.framebuffer[idx] = shade(pal, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+203
@@ -0,0 +1,203 @@
|
|||||||
|
//! Daedalian Opus, running as recompiled Rust.
|
||||||
|
//!
|
||||||
|
//! Controls: arrows = D-pad, Z = A, X = B, Enter = Start, Right Shift / Backspace = Select
|
||||||
|
|
||||||
|
mod apu;
|
||||||
|
mod game;
|
||||||
|
mod headless;
|
||||||
|
mod machine;
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||||
|
use pixels::{Pixels, SurfaceTexture};
|
||||||
|
use winit::application::ApplicationHandler;
|
||||||
|
use winit::dpi::LogicalSize;
|
||||||
|
use winit::event::{ElementState, WindowEvent};
|
||||||
|
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||||
|
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||||
|
use winit::window::{Window, WindowId};
|
||||||
|
|
||||||
|
use machine::*;
|
||||||
|
|
||||||
|
const ROM_DATA: &[u8] = include_bytes!("../assets/rom_data.bin");
|
||||||
|
const FRAME: Duration = Duration::from_nanos(1_000_000_000 * 70_224 / CPU_HZ);
|
||||||
|
const SHADES: [[u8; 4]; 4] = [
|
||||||
|
[0xFF, 0xFF, 0xFF, 0xFF],
|
||||||
|
[0xAA, 0xAA, 0xAA, 0xFF],
|
||||||
|
[0x55, 0x55, 0x55, 0xFF],
|
||||||
|
[0x00, 0x00, 0x00, 0xFF],
|
||||||
|
];
|
||||||
|
|
||||||
|
type AudioQueue = Arc<Mutex<VecDeque<f32>>>;
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
window: Option<Arc<Window>>,
|
||||||
|
pixels: Option<Pixels<'static>>,
|
||||||
|
machine: Machine,
|
||||||
|
next_frame: Instant,
|
||||||
|
audio: AudioQueue,
|
||||||
|
audio_rate: u32,
|
||||||
|
_stream: Option<cpal::Stream>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_audio(queue: AudioQueue) -> Option<(cpal::Stream, u32)> {
|
||||||
|
let device = cpal::default_host().default_output_device()?;
|
||||||
|
let config = device.default_output_config().ok()?;
|
||||||
|
if config.sample_format() != cpal::SampleFormat::F32 {
|
||||||
|
eprintln!("audio: unsupported sample format {:?}, running silent", config.sample_format());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let rate = config.sample_rate();
|
||||||
|
let channels = config.channels() as usize;
|
||||||
|
let stream = device
|
||||||
|
.build_output_stream(
|
||||||
|
config.config(),
|
||||||
|
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
||||||
|
let mut q = queue.lock().unwrap();
|
||||||
|
for frame in data.chunks_mut(channels) {
|
||||||
|
let l = q.pop_front().unwrap_or(0.0);
|
||||||
|
let r = q.pop_front().unwrap_or(l);
|
||||||
|
for (i, s) in frame.iter_mut().enumerate() {
|
||||||
|
*s = if i % 2 == 0 { l } else { r };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|e| eprintln!("audio error: {e}"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
stream.play().ok()?;
|
||||||
|
Some((stream, rate))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn key(&mut self, code: KeyCode, pressed: bool) {
|
||||||
|
let bit = match code {
|
||||||
|
KeyCode::ArrowRight => BTN_RIGHT,
|
||||||
|
KeyCode::ArrowLeft => BTN_LEFT,
|
||||||
|
KeyCode::ArrowUp => BTN_UP,
|
||||||
|
KeyCode::ArrowDown => BTN_DOWN,
|
||||||
|
KeyCode::KeyZ => BTN_A,
|
||||||
|
KeyCode::KeyX => BTN_B,
|
||||||
|
KeyCode::ShiftRight | KeyCode::Backspace => BTN_SELECT,
|
||||||
|
KeyCode::Enter => BTN_START,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
if pressed {
|
||||||
|
self.machine.buttons |= bit;
|
||||||
|
} else {
|
||||||
|
self.machine.buttons &= !bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_due_frames(&mut self) {
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut ran = 0;
|
||||||
|
while self.next_frame <= now && ran < 4 {
|
||||||
|
self.machine.run_frame();
|
||||||
|
self.next_frame += FRAME;
|
||||||
|
ran += 1;
|
||||||
|
}
|
||||||
|
if self.next_frame < now {
|
||||||
|
self.next_frame = now; // fell behind; don't try to catch up forever
|
||||||
|
}
|
||||||
|
let samples = std::mem::take(&mut self.machine.apu.out);
|
||||||
|
let mut q = self.audio.lock().unwrap();
|
||||||
|
q.extend(samples);
|
||||||
|
let max = self.audio_rate as usize / 5 * 2; // keep at most ~200 ms queued
|
||||||
|
if q.len() > max {
|
||||||
|
let excess = q.len() - max;
|
||||||
|
q.drain(..excess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler for App {
|
||||||
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
if self.window.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let attrs = Window::default_attributes()
|
||||||
|
.with_title("Daedalian Opus (recompiled)")
|
||||||
|
.with_inner_size(LogicalSize::new(SCREEN_W as f64 * 4.0, SCREEN_H as f64 * 4.0));
|
||||||
|
let window = Arc::new(event_loop.create_window(attrs).expect("create window"));
|
||||||
|
let size = window.inner_size();
|
||||||
|
let surface = SurfaceTexture::new(size.width, size.height, window.clone());
|
||||||
|
let pixels = Pixels::new(SCREEN_W as u32, SCREEN_H as u32, surface).expect("pixels");
|
||||||
|
self.window = Some(window);
|
||||||
|
self.pixels = Some(pixels);
|
||||||
|
self.next_frame = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => event_loop.exit(),
|
||||||
|
WindowEvent::KeyboardInput { event, .. } => {
|
||||||
|
if let PhysicalKey::Code(code) = event.physical_key {
|
||||||
|
if code == KeyCode::Escape {
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
self.key(code, event.state == ElementState::Pressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::Resized(size) => {
|
||||||
|
if let Some(p) = &mut self.pixels {
|
||||||
|
let _ = p.resize_surface(size.width.max(1), size.height.max(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
if let Some(p) = &mut self.pixels {
|
||||||
|
for (dst, &shade) in p.frame_mut().chunks_exact_mut(4).zip(&self.machine.framebuffer) {
|
||||||
|
dst.copy_from_slice(&SHADES[shade as usize]);
|
||||||
|
}
|
||||||
|
if let Err(e) = p.render() {
|
||||||
|
eprintln!("render error: {e}");
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
if self.window.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.run_due_frames();
|
||||||
|
if let Some(w) = &self.window {
|
||||||
|
w.request_redraw();
|
||||||
|
}
|
||||||
|
event_loop.set_control_flow(ControlFlow::WaitUntil(self.next_frame));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
if args.first().map(|s| s.as_str()) == Some("--headless") {
|
||||||
|
headless::run(ROM_DATA, &args[1..]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let audio: AudioQueue = Arc::new(Mutex::new(VecDeque::new()));
|
||||||
|
let (stream, rate) = match start_audio(audio.clone()) {
|
||||||
|
Some((s, r)) => (Some(s), r),
|
||||||
|
None => {
|
||||||
|
eprintln!("audio: no output device, running silent");
|
||||||
|
(None, 48_000)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut app = App {
|
||||||
|
window: None,
|
||||||
|
pixels: None,
|
||||||
|
machine: Machine::new(ROM_DATA, rate),
|
||||||
|
next_frame: Instant::now(),
|
||||||
|
audio,
|
||||||
|
audio_rate: rate,
|
||||||
|
_stream: stream,
|
||||||
|
};
|
||||||
|
let event_loop = EventLoop::new().expect("event loop");
|
||||||
|
event_loop.run_app(&mut app).expect("run");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user