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