Compare commits

...

3 Commits

Author SHA1 Message Date
e882b3b325 Fix audio for wasm
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 10s
2026-03-27 00:49:06 -05:00
f6c3d073e7 Remove generated wasm files 2026-03-27 00:28:45 -05:00
b433148843 Split WASM and native versions, and move iced support code to native 2026-03-27 00:27:34 -05:00
19 changed files with 1632 additions and 1012 deletions

3
.helix/languages.toml Normal file
View File

@@ -0,0 +1,3 @@
[language-server.rust-analyzer]
config = { cargo = { features = "all" } }

1279
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,18 +3,52 @@ name = "nes-emu"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[features]
default = []
iced = [
"dep:iced", "iced/debug", "iced/canvas", "iced/lazy", "iced/image", "iced/advanced", "tokio", "fs"
]
fs = []
tokio = ["iced/tokio", "dep:tokio", "tokio/time", "tokio/fs"]
iced_wasm = ["dep:iced"]
audio = ["dep:cpal", "dep:ringbuf"]
web = ["dep:web-sys", "dep:web-sys"]
[dependencies] [dependencies]
bitfield = "0.19.3" bitfield = "0.19.3"
# iced = { version = "0.14.0", features = ["debug", "canvas", "tokio", "lazy", "image", "advanced"] } thiserror = "2.0.18"
iced = { path = "../iced", features = ["debug", "canvas", "tokio", "lazy", "image", "advanced"] }
iced_core = { path = "../iced/core", features = ["advanced"] }
rfd = "0.17.2"
# iced_graphics = { version = "0.14.0", features = ["geometry", "image"] }
# iced_widget = { version = "0.13.4", features = ["canvas", "image"] }
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] }
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = { version = "0.3.20", features = ["ansi", "chrono", "env-filter", "json", "serde"] } tracing-subscriber = { version = "0.3.20", features = ["ansi", "chrono", "env-filter", "json", "serde"] }
bytes = "*" bytes = "*"
cpal = "0.17.1"
ringbuf = "0.4.8" # yew = { version = "0.23", features = ["csr"], optional = true }
web-sys = { version = "*", features = [
"HtmlCanvasElement", "CanvasRenderingContext2d",
"Window", "Document", "KeyboardEvent",
"DocumentTimeline", "AudioContextState",
"HtmlButtonElement",
"HtmlInputElement", "FileList", "File", "Blob",
"ImageBitmap", "ImageData",
"AudioContext", "AudioContextOptions",
"AudioBuffer", "AudioBufferOptions", "AudioDestinationNode", "AudioBufferSourceNode",
"GainNode", "AudioParam", "DelayNode"
], optional = true }
cpal = { version = "0.17.1", optional = true}
ringbuf = { version = "0.4.8", optional = true}
iced = { path = "../iced", features = [], optional = true }
# iced_core = { path = "../iced/core", features = ["advanced"], optional = true }
rfd = { version = "0.17.2", optional = true }
tokio = { version = "1.48.0", features = [], optional = true }
log = "*"
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1"
console_log = "1.0"
[[bin]]
name = "native"
required-features = ["iced", "rfd", "audio"]
[[bin]]
name = "wasm"
required-features = ["web"]

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Yew App</title>
<style>
canvas {
image-rendering: pixelated;
}
</style>
</head>
<body></body>
</html>

View File

@@ -1,9 +1,5 @@
use std::iter::repeat_n; use std::iter::repeat_n;
use iced::{
Element, Font,
widget::{column, text},
};
use tracing::debug; use tracing::debug;
macro_rules! lut { macro_rules! lut {
@@ -89,8 +85,10 @@ bitfield::bitfield! {
struct PulseChannel { struct PulseChannel {
enabled: bool, enabled: bool,
duty_vol: DutyVol, duty_vol: DutyVol,
sweep: Sweep, sweep: Sweep,
sweep_reload: bool, sweep_reload: bool,
sweep_counter: u8,
counter: LengthCounter, counter: LengthCounter,
@@ -102,15 +100,17 @@ struct PulseChannel {
envelope_start: bool, envelope_start: bool,
envelope_counter: u8, envelope_counter: u8,
envelope_divider: u8, envelope_divider: u8,
extra: u16,
} }
impl PulseChannel { impl PulseChannel {
pub fn new() -> Self { pub fn new(extra: u16) -> Self {
Self { Self {
enabled: false, enabled: false,
duty_vol: DutyVol(0), duty_vol: DutyVol(0),
sweep: Sweep(0), sweep: Sweep(0),
sweep_reload: false, sweep_reload: false,
sweep_counter: 0,
counter: LengthCounter::new(), counter: LengthCounter::new(),
period: 0, period: 0,
period_timer: 0, period_timer: 0,
@@ -119,6 +119,7 @@ impl PulseChannel {
envelope_start: false, envelope_start: false,
envelope_counter: 0, envelope_counter: 0,
envelope_divider: 0, envelope_divider: 0,
extra,
} }
} }
@@ -131,7 +132,7 @@ impl PulseChannel {
0x01 => { 0x01 => {
self.sweep.0 = val; self.sweep.0 = val;
self.sweep_reload = true; self.sweep_reload = true;
}, }
0x02 => self.period = self.period & 0x700 | (val as u16), 0x02 => self.period = self.period & 0x700 | (val as u16),
0x03 => { 0x03 => {
let reg = LengthTimerHigh(val); let reg = LengthTimerHigh(val);
@@ -177,7 +178,7 @@ impl PulseChannel {
// TODO // TODO
} }
pub fn cur_sample(&self) -> u8 { pub fn cur_sample(&self) -> u8 {
if self.enabled && !self.counter.silenced() { if self.enabled && !self.counter.silenced() && !self.sweep_silenced() {
self.sample self.sample
} else { } else {
0 0
@@ -205,30 +206,54 @@ impl PulseChannel {
} }
} }
} }
fn target_period(&self) -> u16 {
let amt = self.period >> self.sweep.shift();
if self.sweep.negate() {
self.period.saturating_sub(amt + self.extra)
} else {
self.period + amt
}
}
fn sweep_silenced(&self) -> bool {
self.period < 8 || self.target_period() > 0x7FF
}
pub fn h_frame_clock(&mut self) { pub fn h_frame_clock(&mut self) {
self.q_frame_clock(); self.q_frame_clock();
if self.sweep.enable() {
if self.sweep_reload {
self.sweep_counter = self.sweep.period();
self.sweep_reload = false;
} else if self.sweep_counter == 0 {
self.sweep_counter = self.sweep.period();
if self.period < 8 || self.target_period() < 0x7FF {
self.period = self.target_period();
}
} else {
self.sweep_counter -= 1;
}
}
} }
pub fn view<T>(&self) -> Element<'_, T> { pub fn view(&self) -> String {
text!( format!(
"Square Channel "Square Channel
Evelope Volume: {0:>3} ${0:02X} Evelope Volume: {0:>4} ${0:02X}
Constant Volume: {1} Constant Volume: {1}
Length Counter - Halted: {2} Length Counter - Halted: {2}
Duty: {3:>3} ${3:02X} Duty: {3:>4} ${3:02X}
Sweep - Shift: {4:>3} ${4:02X} Sweep - Shift: {4:>4} ${4:02X}
Sweep - Negate: {5} Sweep - Negate: {5}
Sweep - Period: {6:>3} ${6:02X} Sweep - Period: {6:>4} ${6:02X}
Sweep - Enabled: {7} Sweep - Enabled: {7}
Period: {8:>3} ${8:04X} Period: {8:>4} ${8:04X}
Length Counter - Reload Value: {9:>3} ${9:04X} Length Counter - Reload Value: {9:>4} ${9:04X}
Enabled: {10} Enabled: {10}
Timer: {11:>3} ${11:04X} Timer: {11:>4} ${11:04X}
Duty Position: {12:>3} ${12:02X} Duty Position: {12:>4} ${12:02X}
Length Counter - Counter: {13:>3} ${13:02X} Length Counter - Counter: {13:>4} ${13:02X}
Envelope - Counter: {14:>3} ${14:02X} Envelope - Counter: {14:>4} ${14:02X}
Envelope - Divider: {15:>3} ${15:02X} Envelope - Divider: {15:>4} ${15:02X}
Output: {16:>3} ${16:02X} Output: {16:>4} ${16:02X}
", ",
self.duty_vol.volume(), self.duty_vol.volume(),
self.duty_vol.const_vol(), self.duty_vol.const_vol(),
@@ -248,8 +273,6 @@ Output: {16:>3} ${16:02X}
self.envelope_divider, self.envelope_divider,
self.cur_sample(), self.cur_sample(),
) )
.font(Font::MONOSPACE)
.into()
} }
} }
@@ -344,8 +367,8 @@ impl TriangleChannel {
self.q_frame_clock(); self.q_frame_clock();
} }
pub fn view<T>(&self) -> Element<'_, T> { pub fn view(&self) -> String {
text!( format!(
"Triangle Channel "Triangle Channel
Linear Counter - Reload: {0:>3} ${0:02X} Linear Counter - Reload: {0:>3} ${0:02X}
Linear Counter - Halted: {1} Linear Counter - Halted: {1}
@@ -372,8 +395,6 @@ Output: {10:>3} ${10:02X}
self.reload, self.reload,
self.cur_sample(), self.cur_sample(),
) )
.font(Font::MONOSPACE)
.into()
} }
} }
@@ -465,8 +486,8 @@ impl NoiseChannel {
self.q_frame_clock(); self.q_frame_clock();
} }
pub fn view<T>(&self) -> Element<'_, T> { pub fn view(&self) -> String {
text!("").font(Font::MONOSPACE).into() format!("")
} }
} }
@@ -505,8 +526,8 @@ impl DeltaChannel {
false false
} }
pub fn view<T>(&self) -> Element<'_, T> { pub fn view(&self) -> String {
text!("").font(Font::MONOSPACE).into() format!("")
} }
} }
@@ -544,8 +565,8 @@ impl std::fmt::Debug for APU {
impl APU { impl APU {
pub fn init() -> Self { pub fn init() -> Self {
Self { Self {
pulse_1: PulseChannel::new(), pulse_1: PulseChannel::new(1),
pulse_2: PulseChannel::new(), pulse_2: PulseChannel::new(0),
triangle: TriangleChannel::new(), triangle: TriangleChannel::new(),
noise: NoiseChannel::new(), noise: NoiseChannel::new(),
dmc: DeltaChannel::new(), dmc: DeltaChannel::new(),
@@ -695,15 +716,26 @@ impl APU {
pub fn irq_waiting(&mut self) -> bool { pub fn irq_waiting(&mut self) -> bool {
self.frame_counter.irq self.frame_counter.irq
} }
}
#[cfg(feature = "iced")]
mod apu_iced {
use super::*;
use iced::{
Element, Font,
widget::{column, text},
};
impl APU {
pub fn view<'s, T: 's>(&'s self) -> Element<'s, T> { pub fn view<'s, T: 's>(&'s self) -> Element<'s, T> {
column![ column![
self.pulse_1.view(), text(self.pulse_1.view()).font(Font::MONOSPACE),
self.pulse_2.view(), text(self.pulse_2.view()).font(Font::MONOSPACE),
self.triangle.view(), text(self.triangle.view()).font(Font::MONOSPACE),
self.noise.view(), text(self.noise.view()).font(Font::MONOSPACE),
self.dmc.view(), text(self.dmc.view()).font(Font::MONOSPACE),
] ]
.into() .into()
} }
} }
}

View File

@@ -23,8 +23,7 @@ use iced::{
scrollable, text, scrollable, text,
}, },
}; };
use nes_emu::{Break, CycleResult, Mapped, NES, PPU};
use crate::{Break, CycleResult, NES, PPU, mem::Mapped};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DebuggerState { pub struct DebuggerState {
@@ -86,6 +85,8 @@ where
text(format!("{val:032b}")) text(format!("{val:032b}"))
} }
type Type = NES;
impl DebuggerState { impl DebuggerState {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -106,23 +107,23 @@ impl DebuggerState {
row![column![ row![column![
text("Status"), text("Status"),
row![ row![
labelled("A:", text(format!("{:02X}", nes.cpu.a))), labelled("A:", text(format!("{:02X}", nes.cpu().a))),
labelled("X:", text(format!("{:02X}", nes.cpu.x))), labelled("X:", text(format!("{:02X}", nes.cpu().x))),
labelled("Y:", text(format!("{:02X}", nes.cpu.y))), labelled("Y:", text(format!("{:02X}", nes.cpu().y))),
labelled("PC:", text(format!("{:04X}", nes.cpu.pc))), labelled("PC:", text(format!("{:04X}", nes.cpu().pc))),
labelled("Cycle:", text(format!("{}", nes.cpu_cycle()))), labelled("Cycle:", text(format!("{}", nes.cpu_cycle()))),
labelled("SP:", text(format!("{:02X}", nes.cpu.sp))), labelled("SP:", text(format!("{:02X}", nes.cpu().sp))),
] ]
.spacing(5.), .spacing(5.),
row![ row![
labelled("P:", text(format!("{:02X}", nes.cpu.status.0))), labelled("P:", text(format!("{:02X}", nes.cpu().status.0))),
labelled_box("Carry", nes.cpu.status.carry()), labelled_box("Carry", nes.cpu().status.carry()),
labelled_box("Zero", nes.cpu.status.zero()), labelled_box("Zero", nes.cpu().status.zero()),
labelled_box("Interrupt", nes.cpu.status.interrupt_disable()), labelled_box("Interrupt", nes.cpu().status.interrupt_disable()),
labelled_box("--", false), labelled_box("--", false),
labelled_box("--", false), labelled_box("--", false),
labelled_box("Overflow", nes.cpu.status.overflow()), labelled_box("Overflow", nes.cpu().status.overflow()),
labelled_box("Negative", nes.cpu.status.negative()), labelled_box("Negative", nes.cpu().status.negative()),
] ]
.spacing(5.), .spacing(5.),
row![ row![
@@ -246,7 +247,7 @@ impl DebuggerState {
.into() .into()
} }
fn run_n_clock_cycles(nes: &mut NES, n: usize) { fn run_n_clock_cycles(nes: &mut Type, n: usize) {
for _ in 0..n { for _ in 0..n {
if nes.run_one_clock_cycle(&Break::default()).dbg_int || nes.halted() { if nes.run_one_clock_cycle(&Break::default()).dbg_int || nes.halted() {
break; break;
@@ -321,7 +322,7 @@ impl DebuggerState {
ppu_scanline: true, ppu_scanline: true,
..Break::default() ..Break::default()
}, },
|_, n| n.ppu.scanline == self.to_scan_line, |_, n| n.ppu().scanline == self.to_scan_line,
), ),
DebuggerMessage::RunFrames => Self::run_n_clock_cycles(nes, self.frames * 341 * 262), DebuggerMessage::RunFrames => Self::run_n_clock_cycles(nes, self.frames * 341 * 262),
DebuggerMessage::RunBreakpoint => Self::run_until( DebuggerMessage::RunBreakpoint => Self::run_until(
@@ -330,7 +331,7 @@ impl DebuggerState {
break_points: vec![self.breakpoint as u16], break_points: vec![self.breakpoint as u16],
..Break::default() ..Break::default()
}, },
|_, nes| nes.cpu.pc as usize == self.breakpoint, |_, nes| nes.cpu().pc as usize == self.breakpoint,
), ),
// DebuggerMessage::Run => { // DebuggerMessage::Run => {
// Self::run_until(nes, &Break { ..Break::default() }, |_, nes| nes.halted()) // Self::run_until(nes, &Break { ..Break::default() }, |_, nes| nes.halted())

View File

@@ -1,42 +1,25 @@
use std::{ use std::{
fmt::{self, Display}, fmt::{self, Display}, ops::Deref
sync::Arc,
}; };
use iced::{ use iced::{
Element, Fill, Font, Padding, Task, advanced::{layout::{Limits, Node}, overlay, renderer::{Quad, Style}, text::Renderer, widget::{tree::{State, Tag}, Operation, Tree}, Clipboard, Layout, Shell, Text, Widget}, alignment::Vertical, keyboard::{key::Named, Key}, mouse::{Button, Cursor, Interaction, ScrollDelta}, widget::{column, lazy, text}, Color, Element, Event, Fill, Font, Length, Padding, Pixels, Point, Rectangle, Size, Task, Vector
advanced::graphics::text::{
FontSystem,
cosmic_text::{
Attrs, Family, Metrics,
fontdb::{ID, Query},
},
font_system,
},
keyboard::{Key, key::Named},
mouse::{Button, ScrollDelta},
widget::{column, lazy, row, text},
};
use iced_core::{
Clipboard, Color, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Text, Vector,
Widget,
layout::{Limits, Node},
mouse::{Cursor, Interaction},
overlay,
renderer::{Quad, Style},
widget::{
Operation, Tree,
tree::{State, Tag},
},
}; };
// use iced_core::{
// Clipboard, Color, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Text, Vector,
// Widget,
// layout::{Limits, Node},
// mouse::{Cursor, Interaction},
// overlay,
// renderer::{Quad, Style},
// widget::{
// Operation, Tree,
// tree::{State, Tag},
// },
// };
use nes_emu::Memory;
use crate::{PPU, mem::Mapped, ppu::PPUMMRegisters}; use nes_emu::{PPU, Mapped, PPUMMRegisters};
pub trait Memory {
fn peek(&self, val: usize) -> Option<u8>;
fn len(&self) -> usize;
fn edit_ver(&self) -> usize;
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
struct Cpu<'a>(&'a Mapped); struct Cpu<'a>(&'a Mapped);
@@ -169,12 +152,22 @@ impl HexView {
} }
} }
pub struct BufferSlice<'a>(pub &'a [u8]);
impl Deref for BufferSlice<'_> {
type Target = [u8];
fn deref(&self) -> &Self::Target {
self.0
}
}
pub trait Buffer { pub trait Buffer {
fn peek(&self, val: usize) -> Option<u8>; fn peek(&self, val: usize) -> Option<u8>;
fn len(&self) -> usize; fn len(&self) -> usize;
} }
impl Buffer for &[u8] { impl Buffer for BufferSlice<'_> {
fn peek(&self, val: usize) -> Option<u8> { fn peek(&self, val: usize) -> Option<u8> {
self.get(val).copied() self.get(val).copied()
} }
@@ -193,7 +186,7 @@ impl<M: Memory> Buffer for M {
} }
} }
pub fn hex_editor<B: Buffer, M, R: iced_core::text::Renderer>(raw: B) -> HexEditor<B, M, R> { pub fn hex_editor<B: Buffer, M, R: Renderer>(raw: B) -> HexEditor<B, M, R> {
HexEditor { HexEditor {
val: raw, val: raw,
on_edit: None, on_edit: None,
@@ -209,7 +202,7 @@ pub struct HexEdit {
new_value: u8, new_value: u8,
} }
pub struct HexEditor<B, M, R: iced_core::text::Renderer> { pub struct HexEditor<B, M, R: Renderer> {
val: B, val: B,
font_size: Option<Pixels>, font_size: Option<Pixels>,
font: Option<R::Font>, font: Option<R::Font>,
@@ -218,7 +211,7 @@ pub struct HexEditor<B, M, R: iced_core::text::Renderer> {
impl<B, M, R> HexEditor<B, M, R> impl<B, M, R> HexEditor<B, M, R>
where where
R: iced_core::text::Renderer, R: Renderer,
{ {
pub fn font(mut self, font: R::Font) -> Self { pub fn font(mut self, font: R::Font) -> Self {
self.font = Some(font); self.font = Some(font);
@@ -228,7 +221,7 @@ where
impl<B: Buffer, M, R> HexEditor<B, M, R> impl<B: Buffer, M, R> HexEditor<B, M, R>
where where
R: iced_core::text::Renderer, R: Renderer,
{ {
fn value_bounds(&self, renderer: &R, layout: Layout<'_>) -> Rectangle { fn value_bounds(&self, renderer: &R, layout: Layout<'_>) -> Rectangle {
let size = self.font_size.unwrap_or(renderer.default_size()); let size = self.font_size.unwrap_or(renderer.default_size());
@@ -296,7 +289,7 @@ const LINE_HEIGHT: f32 = 1.3;
impl<B: Buffer, M, T, R> Widget<M, T, R> for HexEditor<B, M, R> impl<B: Buffer, M, T, R> Widget<M, T, R> for HexEditor<B, M, R>
where where
R: iced_core::text::Renderer, R: Renderer,
{ {
fn tag(&self) -> Tag { fn tag(&self) -> Tag {
Tag::of::<HexEditorState>() Tag::of::<HexEditorState>()
@@ -354,10 +347,10 @@ where
size, size,
line_height: size.into(), line_height: size.into(),
font, font,
align_x: iced_core::text::Alignment::Left, align_x: iced::advanced::text::Alignment::Left,
align_y: iced_core::alignment::Vertical::Top, align_y: Vertical::Top,
shaping: iced_core::text::Shaping::Basic, shaping: iced::advanced::text::Shaping::Basic,
wrapping: iced_core::text::Wrapping::None, wrapping: iced::advanced::text::Wrapping::None,
hint_factor: None, hint_factor: None,
}, },
pos, pos,
@@ -444,10 +437,10 @@ where
size, size,
line_height: size.into(), line_height: size.into(),
font, font,
align_x: iced_core::text::Alignment::Left, align_x: iced::advanced::text::Alignment::Left,
align_y: iced_core::alignment::Vertical::Top, align_y: Vertical::Top,
shaping: iced_core::text::Shaping::Basic, shaping: iced::advanced::text::Shaping::Basic,
wrapping: iced_core::text::Wrapping::None, wrapping: iced::advanced::text::Wrapping::None,
hint_factor: None, hint_factor: None,
}, },
pos, pos,

View File

@@ -19,14 +19,19 @@ use iced::{
}; };
use nes_emu::{ use nes_emu::{
Break, NES, Break, NES,
audio::Audio,
debugger::{DbgImage, DebuggerMessage, DebuggerState, dbg_image},
header_menu::header_menu,
hex_view::{HexEvent, HexView},
resize_watcher::resize_watcher,
}; };
use tokio::{io::AsyncWriteExt, runtime::Runtime}; use tokio::{io::AsyncWriteExt, runtime::Runtime};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
mod debugger;
use debugger::{DbgImage, DebuggerMessage, DebuggerState, dbg_image};
mod resize_watcher;
use resize_watcher::resize_watcher;
mod header_menu;
use header_menu::header_menu;
mod hex_view;
use hex_view::{HexEvent, HexView};
mod audio;
use audio::Audio;
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "even_odd.nes"); // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "even_odd.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "crc_check.nes"); // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "crc_check.nes");
@@ -151,6 +156,7 @@ impl Emulator {
fn new() -> (Self, Task<Message>) { fn new() -> (Self, Task<Message>) {
let mut nes = nes_emu::NES::load_nes_file(ROM_FILE).expect("Failed to load nes file"); let mut nes = nes_emu::NES::load_nes_file(ROM_FILE).expect("Failed to load nes file");
nes.reset(); nes.reset();
nes.debug_log_mut().enable();
let (win, task) = iced::window::open(Settings { let (win, task) = iced::window::open(Settings {
min_size: None, min_size: None,
..Settings::default() ..Settings::default()
@@ -234,7 +240,10 @@ impl Emulator {
.and_then(|p| { .and_then(|p| {
Task::future(async move { Task::future(async move {
// println!("Opening: {}", p.path().display()); // println!("Opening: {}", p.path().display());
NES::async_load_nes_file(p.path()).await.ok() NES::async_load_nes_file(p.path()).await.ok().map(|mut nes| {
nes.debug_log_mut().enable();
nes
})
}) })
}) })
.and_then(|n| Task::done(Message::OpenRom(n))); .and_then(|n| Task::done(Message::OpenRom(n)));
@@ -375,7 +384,7 @@ impl Emulator {
&& !self.nes.halted() && !self.nes.halted()
{ {
count += 1; count += 1;
if count > 100000 { if count > 90_000 {
println!("Loop overran..."); println!("Loop overran...");
break; break;
} }

305
src/bin/wasm/main.rs Normal file
View File

@@ -0,0 +1,305 @@
use std::sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
};
use log::info;
use nes_emu::{Break, NES};
#[cfg(not(target_arch = "wasm32"))]
use tracing_subscriber::EnvFilter;
use web_sys::{
AudioBuffer, AudioBufferOptions, AudioContext, AudioContextOptions, AudioContextState,
CanvasRenderingContext2d, HtmlButtonElement, HtmlCanvasElement, HtmlInputElement, ImageBitmap,
ImageData, KeyboardEvent,
js_sys::{Function, Uint8Array},
wasm_bindgen::{Clamped, JsValue, UnwrapThrowExt, closure::Closure},
window,
};
const DEFAULT_ROM: &[u8] = include_bytes!(concat!(
env!("ROM_DIR"),
"/input_test.nes" // TODO: select default rom
));
pub fn main() {
#[cfg(target_arch = "wasm32")]
{
console_log::init().expect("Initialize logger");
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
}
#[cfg(not(target_arch = "wasm32"))]
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let win = window().expect_throw("Window is undefined");
let doc = win.document().expect_throw("Document is undefined");
let canvas = doc
.get_element_by_id("main_view")
.expect_throw("Failed to get canvas element");
let canvas = HtmlCanvasElement::from(JsValue::from(canvas));
let ctx =
CanvasRenderingContext2d::from(JsValue::from(canvas.get_context("2d").unwrap().unwrap()));
let cl = Arc::new(Closure::new(move |image| {
ctx.reset();
ctx.draw_image_with_image_bitmap(&ImageBitmap::from(image), 0., 0.)
.expect_throw("Failed to draw image");
}));
let audio_ctx_opts = AudioContextOptions::new();
audio_ctx_opts.set_sample_rate(60. * 3723.);
// audio_ctx_opts.set_
let audio = AudioContext::new_with_context_options(&audio_ctx_opts)
.expect_throw("Failed to create audio context");
let _ = audio.suspend().expect_throw("Failed to suspend AudioCtx");
const BUFFER_SIZE: usize = 1 << 10;
const SAMPLE_RATE: f64 = 60. * 3723.;
let audio_buffer_opts = AudioBufferOptions::new(BUFFER_SIZE as u32, SAMPLE_RATE as f32);
audio_buffer_opts.set_number_of_channels(1);
// let audio_buffer =
// AudioBuffer::new(&audio_buffer_opts).expect_throw("Failed to create audio buffer");
// let mut raw_buf = [0.; BUFFER_SIZE];
// for i in 0..raw_buf.len() {
// raw_buf[i] = (i as f32 / 200.).sin()
// }
// audio_buffer
// .copy_to_channel(&raw_buf, 0)
// .expect_throw("Failed to copy audio data");
let gain_node = audio
.create_gain()
.expect_throw("Failed to create gain node");
gain_node.set_channel_count(1);
gain_node.gain().set_value(0.01);
gain_node
.connect_with_audio_node_and_output(&audio.destination(), 0)
.expect_throw("Failed to connect to audio destination");
// gain_node
// .connect_with_audio_node_and_output(&audio.destination(), 1)
// .expect_throw("Failed to connect to audio destination 2");
let delay_node = audio
.create_delay()
.expect_throw("Failed to create delay node");
delay_node.set_channel_count(1);
delay_node.delay_time().set_value(0.);
delay_node
.connect_with_audio_node(&gain_node)
.expect_throw("Failed to connect to audio destination");
// source_node.start_with_when(when)
let nes = Arc::new(Mutex::new({
let mut n = NES::load_nes_file_mem(DEFAULT_ROM).unwrap();
n.reset();
n
}));
// let timeout = Arc::new(Mutex::new(None as Option<i32>));
let poisoned = Arc::new(AtomicBool::new(false));
// let timeout_cpy = timeout.clone();
{
let audio = audio.clone();
let el = doc.get_element_by_id("pause").unwrap();
let element = HtmlButtonElement::from(JsValue::from(el.clone()));
let f: Closure<dyn Fn(JsValue)> = Closure::new(move |_e| {
if audio.state() == AudioContextState::Suspended {
let _ = audio.resume();
element.set_inner_text("Pause");
} else {
let _ = audio.suspend();
element.set_inner_text("Play");
}
});
el.add_event_listener_with_callback("click", &Function::from(f.into_js_value()))
.expect_throw("Failed to setup pause callback");
}
{
let nes = nes.clone();
let poisoned = poisoned.clone();
let element = HtmlInputElement::from(JsValue::from(doc.get_element_by_id("file").unwrap()));
element.set_accept(".nes");
let load: Closure<dyn FnMut(JsValue)> = Closure::new(move |v| {
let v = Uint8Array::from(v);
// info!("{v:?}");
let new_emu = NES::load_nes_file_mem(&v.to_vec()).expect("Failed to load nes file");
if poisoned.swap(true, Ordering::AcqRel) == false {
let mut nes = nes.lock().unwrap();
*nes = new_emu;
nes.power_cycle();
if poisoned.swap(false, Ordering::AcqRel) == true {}
}
});
let f: Closure<dyn Fn(JsValue)> = Closure::new(move |_e| {
if let Some(file) = element.files().and_then(|l| l.get(0)) {
let _ = file.bytes().then(&load);
}
});
doc.get_element_by_id("load")
.unwrap()
.add_event_listener_with_callback("click", &Function::from(f.into_js_value()))
.expect_throw("Failed to setup pause callback");
}
{
let nes = nes.clone();
let poisoned = poisoned.clone();
let f: Closure<dyn Fn(JsValue)> = Closure::new(move |e| {
if poisoned.swap(true, Ordering::AcqRel) == false {
let e = KeyboardEvent::from(e);
let key = e.key();
// info!("{}", key);
if key == "ArrowLeft" {
nes.lock().unwrap().controller_1().set_left(true);
} else if key == "ArrowRight" {
nes.lock().unwrap().controller_1().set_right(true);
} else if key == "ArrowUp" {
nes.lock().unwrap().controller_1().set_up(true);
} else if key == "ArrowDown" {
nes.lock().unwrap().controller_1().set_down(true);
} else if key == "a" || key == "A" {
nes.lock().unwrap().controller_1().set_a(true);
} else if key == "s" || key == "S" {
nes.lock().unwrap().controller_1().set_b(true);
} else if key == "q" || key == "Q" {
nes.lock().unwrap().controller_1().set_start(true);
} else if key == "w" || key == "W" {
nes.lock().unwrap().controller_1().set_select(true);
}
if poisoned.swap(false, Ordering::AcqRel) == true {}
}
});
win.set_onkeydown(Some(&Function::from(f.into_js_value())));
}
{
let nes = nes.clone();
let poisoned = poisoned.clone();
let f: Closure<dyn Fn(JsValue)> = Closure::new(move |e| {
if poisoned.swap(true, Ordering::AcqRel) == false {
let e = KeyboardEvent::from(e);
let key = e.key();
// info!("{}", key);
if key == "ArrowLeft" {
nes.lock().unwrap().controller_1().set_left(false);
} else if key == "ArrowRight" {
nes.lock().unwrap().controller_1().set_right(false);
} else if key == "ArrowUp" {
nes.lock().unwrap().controller_1().set_up(false);
} else if key == "ArrowDown" {
nes.lock().unwrap().controller_1().set_down(false);
} else if key == "a" || key == "A" {
nes.lock().unwrap().controller_1().set_a(false);
} else if key == "s" || key == "S" {
nes.lock().unwrap().controller_1().set_b(false);
} else if key == "q" || key == "Q" {
nes.lock().unwrap().controller_1().set_start(false);
} else if key == "w" || key == "W" {
nes.lock().unwrap().controller_1().set_select(false);
}
if poisoned.swap(false, Ordering::AcqRel) == true {}
}
});
win.set_onkeyup(Some(&Function::from(f.into_js_value())));
}
// let timeline = win.document().unwrap().timeline();
let mut last_frame = audio.current_time();
let mut raw_audio_buffer = [0f32; BUFFER_SIZE];
let mut cur_pos = 0;
let mut cur_total = BUFFER_SIZE; // Start time in samples, set to buffer size...
let period = Arc::new(Mutex::new(None as Option<Function>));
let period_cl = period.clone();
let periodic: Closure<dyn FnMut(JsValue)> = Closure::new(move |_v| {
let win = window().expect_throw("Window is undefined");
const FRAME_TIME: f64 = 1. / 60.;
let mut cancel = false;
if last_frame + FRAME_TIME > audio.current_time() {
last_frame += FRAME_TIME;
if audio.state() == AudioContextState::Running {
if poisoned.swap(true, Ordering::AcqRel) {
cancel = true;
// if let Some(_id) = timeout_cpy.lock().ok().and_then(|v| *v) {
// win.clear_interval_with_handle(id);
// win.cancel_animation_frame(id);
info!("Cleared interval due to poison");
// } else {
// info!("Not yet set id");
// }
} else {
let mut n = nes.lock().unwrap();
// info!("Running frame");
let mut count = 0;
while !n.halted() && !n.run_one_clock_cycle(&Break::default()).ppu_frame {
count += 1;
if count > 90_000 {
info!("Loop overran");
break;
}
}
// info!("New samples: {}", n.apu().get_frame_samples().len());
for s in n.apu().get_frame_samples() {
// raw_audio_buffer[cur_pos] = ((*s as f32) - 127.5) / 128.;
raw_audio_buffer[cur_pos] = (*s as f32) / 256.;
cur_pos += 1;
if cur_pos == BUFFER_SIZE {
cur_pos = 0;
cur_total += BUFFER_SIZE;
if win.get("play_audio").is_some_and(|v| v.is_truthy()) {
let audio_buffer = AudioBuffer::new(&audio_buffer_opts)
.expect_throw("Failed to create audio buffer");
audio_buffer
.copy_to_channel(&raw_audio_buffer, 0)
.expect_throw("Failed to copy audio data");
// info!("S: {:?}", audio_buffer.get_channel_data(0));
let source_node = audio
.create_buffer_source()
.expect_throw("Failed to create buffer source");
// source_node.set_loop(false);
source_node.set_buffer(Some(&audio_buffer));
source_node
.connect_with_audio_node(&gain_node)
.expect_throw("Failed to connect to gain node");
source_node
.start_with_when((cur_total as f64) / SAMPLE_RATE)
.expect_throw("Failed to start source_node");
// info!(
// "At {cur_total} samples, {:02.04}s ahead !loop",
// (cur_total as f64) / SAMPLE_RATE - audio.current_time()
// );
}
}
}
n.apu_mut().reset_frame_samples(); // Discard audio samples
// info!("Completed Frame in {} cycles", count);
let data = Clamped(n.ppu().render_buffer.raw_image());
// info!("Creating bitmap");
let image = ImageData::new_with_u8_clamped_array(data, 256)
.expect_throw("Image data could not be created");
let data = win
.create_image_bitmap_with_image_data(&image)
.expect_throw("Bitmap could not be created");
let _ = data.then(&cl);
drop(n);
if !poisoned.swap(false, Ordering::AcqRel) {
panic!("Poisoned logic invalid");
}
}
}
}
if !cancel {
win.request_animation_frame(period_cl.lock().unwrap().as_ref().unwrap())
.expect_throw("Failed to setup animation frame");
}
});
let js = Function::from(periodic.into_js_value());
let win = window().expect_throw("Window is undefined");
// let timeout_id = win
// .set_interval_with_callback_and_timeout_and_arguments_0(&Function::from(js), 16)
// .expect_throw("Failed to setup timeout");
// let timeout_id =
win.request_animation_frame(&js)
.expect_throw("Failed to setup animation frame");
*period.lock().unwrap() = Some(js);
// *timeout.lock().unwrap() = Some(timeout_id);
}

View File

@@ -2,6 +2,7 @@ use std::num::NonZeroUsize;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DebugLog { pub struct DebugLog {
enabled: bool,
current: String, current: String,
history: Vec<String>, history: Vec<String>,
max_history: Option<NonZeroUsize>, max_history: Option<NonZeroUsize>,
@@ -11,6 +12,7 @@ pub struct DebugLog {
impl DebugLog { impl DebugLog {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
enabled: false,
current: String::new(), current: String::new(),
history: vec![], history: vec![],
max_history: None, max_history: None,
@@ -47,25 +49,41 @@ impl DebugLog {
pub fn pop(&mut self) -> Option<String> { pub fn pop(&mut self) -> Option<String> {
self.history.pop() self.history.pop()
} }
pub fn enable(&mut self) {
self.enabled = true;
}
} }
impl std::fmt::Write for DebugLog { impl std::fmt::Write for DebugLog {
fn write_str(&mut self, s: &str) -> std::fmt::Result { fn write_str(&mut self, s: &str) -> std::fmt::Result {
if self.enabled {
let tmp = self.current.write_str(s); let tmp = self.current.write_str(s);
self.rotate(); self.rotate();
tmp tmp
} else {
Ok(())
}
} }
fn write_char(&mut self, c: char) -> std::fmt::Result { fn write_char(&mut self, c: char) -> std::fmt::Result {
if self.enabled {
let tmp = self.current.write_char(c); let tmp = self.current.write_char(c);
self.rotate(); self.rotate();
tmp tmp
} else {
Ok(())
}
} }
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::fmt::Result { fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::fmt::Result {
if self.enabled {
let tmp = self.current.write_fmt(args); let tmp = self.current.write_fmt(args);
self.rotate(); self.rotate();
tmp tmp
} else {
Ok(())
}
} }
} }

View File

@@ -2,18 +2,19 @@ mod apu;
mod controllers; mod controllers;
mod cpu; mod cpu;
pub mod debug; pub mod debug;
pub mod debugger; // pub mod debugger;
pub mod header_menu; // pub mod header_menu;
pub mod hex_view; // pub mod hex_view;
mod mem; mod mem;
mod ppu; mod ppu;
pub mod resize_watcher;
#[cfg(test)] #[cfg(test)]
mod test_roms; mod test_roms;
pub mod audio; pub use mem::{Memory, Mapped};
pub use ppu::{Color, PPU, RenderBuffer}; pub use ppu::{Color, PPU, RenderBuffer, PPUMMRegisters};
use tokio::io::AsyncReadExt as _; pub use cpu::{Cpu, CycleResult, CPUMMRegisters};
// #[cfg(not(target_arch = "wasm32"))]
// use tokio::io::AsyncReadExt as _;
use std::{fs::File, io::Read, path::Path}; use std::{fs::File, io::Read, path::Path};
@@ -22,9 +23,9 @@ use thiserror::Error;
use crate::{ use crate::{
apu::APU, apu::APU,
controllers::{ControllerState, Controllers}, controllers::{ControllerState, Controllers},
cpu::{CPUMMRegisters, ClockState, Cpu, CycleResult, DmaState}, cpu::{ClockState, DmaState},
debug::DebugLog, debug::DebugLog,
mem::{CpuMem, Mapped, PpuMem}, mem::{CpuMem, PpuMem},
}; };
#[derive(Error, Debug)] #[derive(Error, Debug)]
@@ -115,13 +116,18 @@ impl std::fmt::Debug for NES {
} }
impl NES { impl NES {
#[cfg(feature = "fs")]
pub fn load_nes_file(file: impl AsRef<Path>) -> Result<Self> { pub fn load_nes_file(file: impl AsRef<Path>) -> Result<Self> {
let mut raw = Vec::new(); let mut raw = Vec::new();
File::open(file)?.read_to_end(&mut raw)?; File::open(file)?.read_to_end(&mut raw)?;
Self::load_nes_file_mem(&raw) Self::load_nes_file_mem(&raw)
} }
// #[cfg(not(target_arch = "wasm32"))]
#[cfg(all(feature = "fs", feature = "tokio"))]
pub async fn async_load_nes_file(file: impl AsRef<Path>) -> Result<Self> { pub async fn async_load_nes_file(file: impl AsRef<Path>) -> Result<Self> {
use tokio::io::AsyncReadExt as _;
let mut raw = Vec::new(); let mut raw = Vec::new();
tokio::fs::File::open(file) tokio::fs::File::open(file)
.await? .await?
@@ -342,6 +348,10 @@ impl NES {
&self.ppu.render_buffer &self.ppu.render_buffer
} }
pub fn cpu(&self) -> &Cpu {
&self.cpu
}
pub fn ppu(&self) -> &PPU { pub fn ppu(&self) -> &PPU {
&self.ppu &self.ppu
} }

View File

@@ -1,10 +1,16 @@
use std::{fmt, sync::Arc}; use std::{fmt, sync::Arc};
use crate::{ use crate::{
CPUMMRegisters, PPU, apu::APU, controllers::Controllers, cpu::DmaState, hex_view::Memory, CPUMMRegisters, PPU, apu::APU, controllers::Controllers, cpu::DmaState,
ppu::PPUMMRegisters, ppu::PPUMMRegisters,
}; };
pub trait Memory {
fn peek(&self, val: usize) -> Option<u8>;
fn len(&self) -> usize;
fn edit_ver(&self) -> usize;
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Value<R> { pub enum Value<R> {
Value(u8), Value(u8),

View File

@@ -1,11 +1,6 @@
use std::fmt; use std::fmt;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use iced::{
Point, Size,
advanced::graphics::geometry::Renderer,
widget::canvas::{Fill, Frame},
};
use crate::mem::{Mapped, PpuMem, Value}; use crate::mem::{Mapped, PpuMem, Value};
@@ -16,12 +11,6 @@ pub struct Color {
pub b: u8, pub b: u8,
} }
impl Into<Fill> for Color {
fn into(self) -> Fill {
iced::Color::from_rgb8(self.r, self.g, self.b).into()
}
}
impl fmt::Display for Color { impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b) write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
@@ -66,6 +55,10 @@ impl<const W: usize, const H: usize> RenderBuffer<W, H> {
Bytes::copy_from_slice(&self.raw_rgba) Bytes::copy_from_slice(&self.raw_rgba)
} }
pub fn raw_image(&self) -> &[u8] {
&self.raw_rgba
}
pub fn assert_eq(&self, other: &Self) { pub fn assert_eq(&self, other: &Self) {
// if self.buffer != other.buffer { // if self.buffer != other.buffer {
for y in 0..H { for y in 0..H {
@@ -1126,7 +1119,79 @@ impl PPU {
pub fn oam_edit_ver(&self) -> usize { pub fn oam_edit_ver(&self) -> usize {
self.oam.edit_ver self.oam.edit_ver
} }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ppu_registers() {
// let mut ppu = PPU::with_chr_rom(&[0u8; 8192], Mapper::from_header(0));
let mut ppu = PPU::init();
// let mut mem = MemoryMap::new(vec![Segment::ram("")]);
let mut mem = Mapped::test_ram();
let mut mem = PpuMem::new(&mut mem);
assert_eq!(ppu.background.v, 0);
assert_eq!(ppu.background.t, 0);
assert_eq!(ppu.background.x, 0);
assert_eq!(ppu.background.w, false);
ppu.write_reg(&mut mem, 0, 0);
assert_eq!(ppu.background.v, 0);
ppu.write_reg(&mut mem, 0, 0b11);
assert_eq!(
ppu.background.t, 0b0001100_00000000,
"Actual: {:016b}",
ppu.background.t
);
assert_eq!(ppu.background.w, false);
ppu.write_reg(&mut mem, 5, 0x7D);
assert_eq!(
ppu.background.t, 0b0001100_00001111,
"Actual: {:016b}",
ppu.background.t
);
assert_eq!(ppu.background.x, 0b101);
assert_eq!(ppu.background.w, true);
ppu.write_reg(&mut mem, 5, 0x5E);
assert_eq!(
ppu.background.t, 0b1101101_01101111,
"Actual: {:016b}",
ppu.background.t
);
assert_eq!(ppu.background.x, 0b101);
assert_eq!(ppu.background.w, false);
ppu.write_reg(&mut mem, 5, 0x7D);
assert_eq!(
ppu.background.t, 0b1101101_01101111,
"Actual: {:016b}",
ppu.background.t
);
assert_eq!(ppu.background.x, 0b101);
assert_eq!(ppu.background.w, true);
ppu.read_reg(&mut mem, 2);
assert_eq!(ppu.background.w, false);
}
}
#[cfg(feature = "iced")]
mod ppu_iced {
use super::*;
use iced::{
Point, Size,
advanced::graphics::geometry::Renderer,
widget::canvas::{Fill, Frame},
};
impl Into<Fill> for Color {
fn into(self) -> Fill {
iced::Color::from_rgb8(self.r, self.g, self.b).into()
}
}
impl PPU {
pub fn render_name_table<R: Renderer>(&self, mem: &Mapped, frame: &mut Frame<R>) { pub fn render_name_table<R: Renderer>(&self, mem: &Mapped, frame: &mut Frame<R>) {
for y in 0..60 { for y in 0..60 {
for x in 0..64 { for x in 0..64 {
@@ -1359,58 +1424,4 @@ Attribute data: ${:02X}
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ppu_registers() {
// let mut ppu = PPU::with_chr_rom(&[0u8; 8192], Mapper::from_header(0));
let mut ppu = PPU::init();
// let mut mem = MemoryMap::new(vec![Segment::ram("")]);
let mut mem = Mapped::test_ram();
let mut mem = PpuMem::new(&mut mem);
assert_eq!(ppu.background.v, 0);
assert_eq!(ppu.background.t, 0);
assert_eq!(ppu.background.x, 0);
assert_eq!(ppu.background.w, false);
ppu.write_reg(&mut mem, 0, 0);
assert_eq!(ppu.background.v, 0);
ppu.write_reg(&mut mem, 0, 0b11);
assert_eq!(
ppu.background.t, 0b0001100_00000000,
"Actual: {:016b}",
ppu.background.t
);
assert_eq!(ppu.background.w, false);
ppu.write_reg(&mut mem, 5, 0x7D);
assert_eq!(
ppu.background.t, 0b0001100_00001111,
"Actual: {:016b}",
ppu.background.t
);
assert_eq!(ppu.background.x, 0b101);
assert_eq!(ppu.background.w, true);
ppu.write_reg(&mut mem, 5, 0x5E);
assert_eq!(
ppu.background.t, 0b1101101_01101111,
"Actual: {:016b}",
ppu.background.t
);
assert_eq!(ppu.background.x, 0b101);
assert_eq!(ppu.background.w, false);
ppu.write_reg(&mut mem, 5, 0x7D);
assert_eq!(
ppu.background.t, 0b1101101_01101111,
"Actual: {:016b}",
ppu.background.t
);
assert_eq!(ppu.background.x, 0b101);
assert_eq!(ppu.background.w, true);
ppu.read_reg(&mut mem, 2);
assert_eq!(ppu.background.w, false);
}
} }

View File

@@ -2,8 +2,8 @@
.include "audio_inp.s" .include "audio_inp.s"
zp_res TIMER_LOW zp_res TIMER_LOW
; zp_res TIMER_LOW zp_res SWEEP
; zp_res TIMER_LOW zp_res DLCV
; zp_res TIMER_LOW ; zp_res TIMER_LOW
reset: reset:
@@ -14,10 +14,12 @@ reset:
lda #$01 lda #$01
sta SNDCHN sta SNDCHN
lda #$AF ; Duty 2, LC halted, evelope enabled, volume = F lda #$BF ; Duty 2, LC halted, evelope enabled, volume = F
sta PULSE_CH1_DLCV sta PULSE_CH1_DLCV
sta DLCV
lda #$7F ; lda #$7F ;
sta PULSE_CH1_SWEEP sta PULSE_CH1_SWEEP
sta SWEEP
lda #$6F lda #$6F
sta PULSE_CH1_TLOW sta PULSE_CH1_TLOW
sta TIMER_LOW sta TIMER_LOW
@@ -43,21 +45,32 @@ reset:
write_char_x ':' write_char_x ':'
load_ppu_addr $2061 load_ppu_addr $2061
; write_char_x 't' write_char_x 's'
; write_char_x 'i' write_char_x 'w'
; write_char_x 'm' write_char_x 'e'
; write_char_x 'e' write_char_x 'e'
; write_char_x 'r' write_char_x 'p'
; write_char_x ' ' write_char_x ':'
; write_char_x 'h'
; write_char_x 'o' load_ppu_addr $2081
; write_char_x 'w' write_char_x 'd'
; write_char_x ':' write_char_x 'l'
write_char_x 'c'
write_char_x 'v'
write_char_x ':'
load_ppu_addr $204C load_ppu_addr $204C
lda TIMER_LOW lda TIMER_LOW
jsr write_hex_a jsr write_hex_a
load_ppu_addr $206C
lda SWEEP
jsr write_hex_a
load_ppu_addr $208C
lda DLCV
jsr write_hex_a
jsr enable_rendering jsr enable_rendering
loop: loop:
jmp loop jmp loop
@@ -67,28 +80,83 @@ loop:
audio_nmi: audio_nmi:
EOR JOY_PREV EOR JOY_PREV
STA JOY_PREV STA JOY_PREV
lda JOYTEMP lda JOYTEMP
and #JOY_UP_MASK and #JOY_UP_MASK
and JOY_PREV and JOY_PREV
beq next_1 beq down
load_ppu_addr $204C load_ppu_addr $204C
lda #$01 lda #$01
adc TIMER_LOW adc TIMER_LOW
sta TIMER_LOW sta TIMER_LOW
sta PULSE_CH1_TLOW sta PULSE_CH1_TLOW
jsr write_hex_a jsr write_hex_a
next_1:
down:
lda JOYTEMP lda JOYTEMP
and #JOY_DOWN_MASK and #JOY_DOWN_MASK
and JOY_PREV and JOY_PREV
beq next_2 beq left
load_ppu_addr $204C load_ppu_addr $204C
lda #$FF lda #$FF
adc TIMER_LOW adc TIMER_LOW
sta TIMER_LOW sta TIMER_LOW
sta PULSE_CH1_TLOW sta PULSE_CH1_TLOW
jsr write_hex_a jsr write_hex_a
next_2:
left:
lda JOYTEMP
and #JOY_LEFT_MASK
and JOY_PREV
beq right
load_ppu_addr $206C
lda #$FF
adc SWEEP
sta SWEEP
sta PULSE_CH1_SWEEP
jsr write_hex_a
right:
lda JOYTEMP
and #JOY_RIGHT_MASK
and JOY_PREV
beq joy_a
load_ppu_addr $206C
lda #$01
adc SWEEP
sta SWEEP
sta PULSE_CH1_SWEEP
jsr write_hex_a
joy_a:
lda JOYTEMP
and #JOY_A_MASK
and JOY_PREV
beq joy_b
load_ppu_addr $208C
lda DLCV
eor #$10
sta DLCV
sta PULSE_CH1_DLCV
jsr write_hex_a
; sta PULSE_CH1_DLCV
lda #$F0
sta PULSE_CH1_LCTH
joy_b:
lda JOYTEMP
and #JOY_B_MASK
and JOY_PREV
beq done
load_ppu_addr $204C
lda #$00
; adc TIMER_LOW
; sta TIMER_LOW
sta PULSE_CH1_TLOW
; jsr write_hex_a
done:
lda JOYTEMP lda JOYTEMP
STA JOY_PREV STA JOY_PREV
rts rts

7
wasm.fish Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/fish
# RUSTFLAGS=--cfg=web_sys_unstable_apis
cargo build --target wasm32-unknown-unknown --bin wasm --features web --release || exit
wasm-bindgen target/wasm32-unknown-unknown/release/wasm.wasm --out-dir wasm --web || exit
scp wasm/* hpdl380g10.loadingm.xyz:/data/site/nes/
simple-http-server wasm || exit

47
wasm/wasm.html Normal file
View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Wasm test</title>
<style>
canvas {
width: 512px;
height: 480px;
image-rendering: pixelated;
}
.flex {
display: flex;
gap: 2em;
}
.col {
flex-direction: column;
}
</style>
</head>
<body>
<h1>NES Emulator</h1>
<div class="flex">
<canvas id="main_view" width="256px" height="240px"></canvas>
<div>
<h4>Controls:</h4>
<p>D-Pad: Arrow keys</p>
<p>A: A key</p>
<p>B: S key</p>
<p>Start: Q key</p>
<p>Select: W key</p>
<button id="pause">Play</button>
</div>
<div class="flex col">
<h4>Load new ROM:</h4>
<input id="file" type="file" />
<button id="load">Load ROM</button>
</div>
</div>
<script type="module">
import init from "./wasm.js";
init();
</script>
</body>
</html>