Initial commit

This commit is contained in:
2026-10-31 13:00:20 -05:00
commit ecc66561d1
8 changed files with 3897 additions and 0 deletions
+18
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -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
+42
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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");
}