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"
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]
bitfield = "0.19.3"
# iced = { version = "0.14.0", features = ["debug", "canvas", "tokio", "lazy", "image", "advanced"] }
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"] }
thiserror = "2.0.18"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.20", features = ["ansi", "chrono", "env-filter", "json", "serde"] }
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 iced::{
Element, Font,
widget::{column, text},
};
use tracing::debug;
macro_rules! lut {
@@ -89,8 +85,10 @@ bitfield::bitfield! {
struct PulseChannel {
enabled: bool,
duty_vol: DutyVol,
sweep: Sweep,
sweep_reload: bool,
sweep_counter: u8,
counter: LengthCounter,
@@ -102,15 +100,17 @@ struct PulseChannel {
envelope_start: bool,
envelope_counter: u8,
envelope_divider: u8,
extra: u16,
}
impl PulseChannel {
pub fn new() -> Self {
pub fn new(extra: u16) -> Self {
Self {
enabled: false,
duty_vol: DutyVol(0),
sweep: Sweep(0),
sweep_reload: false,
sweep_counter: 0,
counter: LengthCounter::new(),
period: 0,
period_timer: 0,
@@ -119,6 +119,7 @@ impl PulseChannel {
envelope_start: false,
envelope_counter: 0,
envelope_divider: 0,
extra,
}
}
@@ -131,7 +132,7 @@ impl PulseChannel {
0x01 => {
self.sweep.0 = val;
self.sweep_reload = true;
},
}
0x02 => self.period = self.period & 0x700 | (val as u16),
0x03 => {
let reg = LengthTimerHigh(val);
@@ -143,7 +144,7 @@ impl PulseChannel {
_ => unreachable!(),
}
}
fn volume(&self) -> u8 {
if self.duty_vol.const_vol() {
self.duty_vol.volume()
@@ -151,7 +152,7 @@ impl PulseChannel {
self.envelope_counter
}
}
pub fn clock(&mut self) {
if !self.enabled {
self.sample = 0;
@@ -177,7 +178,7 @@ impl PulseChannel {
// TODO
}
pub fn cur_sample(&self) -> u8 {
if self.enabled && !self.counter.silenced() {
if self.enabled && !self.counter.silenced() && !self.sweep_silenced() {
self.sample
} else {
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) {
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> {
text!(
pub fn view(&self) -> String {
format!(
"Square Channel
Evelope Volume: {0:>3} ${0:02X}
Evelope Volume: {0:>4} ${0:02X}
Constant Volume: {1}
Length Counter - Halted: {2}
Duty: {3:>3} ${3:02X}
Sweep - Shift: {4:>3} ${4:02X}
Duty: {3:>4} ${3:02X}
Sweep - Shift: {4:>4} ${4:02X}
Sweep - Negate: {5}
Sweep - Period: {6:>3} ${6:02X}
Sweep - Period: {6:>4} ${6:02X}
Sweep - Enabled: {7}
Period: {8:>3} ${8:04X}
Length Counter - Reload Value: {9:>3} ${9:04X}
Period: {8:>4} ${8:04X}
Length Counter - Reload Value: {9:>4} ${9:04X}
Enabled: {10}
Timer: {11:>3} ${11:04X}
Duty Position: {12:>3} ${12:02X}
Length Counter - Counter: {13:>3} ${13:02X}
Envelope - Counter: {14:>3} ${14:02X}
Envelope - Divider: {15:>3} ${15:02X}
Output: {16:>3} ${16:02X}
Timer: {11:>4} ${11:04X}
Duty Position: {12:>4} ${12:02X}
Length Counter - Counter: {13:>4} ${13:02X}
Envelope - Counter: {14:>4} ${14:02X}
Envelope - Divider: {15:>4} ${15:02X}
Output: {16:>4} ${16:02X}
",
self.duty_vol.volume(),
self.duty_vol.const_vol(),
@@ -242,14 +267,12 @@ Output: {16:>3} ${16:02X}
0,
self.enabled,
self.period_timer,
self.cur, // ?
self.cur, // ?
self.counter.current,
self.envelope_counter,
self.envelope_divider,
self.cur_sample(),
)
.font(Font::MONOSPACE)
.into()
}
}
@@ -344,8 +367,8 @@ impl TriangleChannel {
self.q_frame_clock();
}
pub fn view<T>(&self) -> Element<'_, T> {
text!(
pub fn view(&self) -> String {
format!(
"Triangle Channel
Linear Counter - Reload: {0:>3} ${0:02X}
Linear Counter - Halted: {1}
@@ -372,8 +395,6 @@ Output: {10:>3} ${10:02X}
self.reload,
self.cur_sample(),
)
.font(Font::MONOSPACE)
.into()
}
}
@@ -465,8 +486,8 @@ impl NoiseChannel {
self.q_frame_clock();
}
pub fn view<T>(&self) -> Element<'_, T> {
text!("").font(Font::MONOSPACE).into()
pub fn view(&self) -> String {
format!("")
}
}
@@ -505,8 +526,8 @@ impl DeltaChannel {
false
}
pub fn view<T>(&self) -> Element<'_, T> {
text!("").font(Font::MONOSPACE).into()
pub fn view(&self) -> String {
format!("")
}
}
@@ -544,8 +565,8 @@ impl std::fmt::Debug for APU {
impl APU {
pub fn init() -> Self {
Self {
pulse_1: PulseChannel::new(),
pulse_2: PulseChannel::new(),
pulse_1: PulseChannel::new(1),
pulse_2: PulseChannel::new(0),
triangle: TriangleChannel::new(),
noise: NoiseChannel::new(),
dmc: DeltaChannel::new(),
@@ -695,15 +716,26 @@ impl APU {
pub fn irq_waiting(&mut self) -> bool {
self.frame_counter.irq
}
}
pub fn view<'s, T: 's>(&'s self) -> Element<'s, T> {
column![
self.pulse_1.view(),
self.pulse_2.view(),
self.triangle.view(),
self.noise.view(),
self.dmc.view(),
]
.into()
#[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> {
column![
text(self.pulse_1.view()).font(Font::MONOSPACE),
text(self.pulse_2.view()).font(Font::MONOSPACE),
text(self.triangle.view()).font(Font::MONOSPACE),
text(self.noise.view()).font(Font::MONOSPACE),
text(self.dmc.view()).font(Font::MONOSPACE),
]
.into()
}
}
}

View File

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

View File

@@ -1,42 +1,25 @@
use std::{
fmt::{self, Display},
sync::Arc,
fmt::{self, Display}, ops::Deref
};
use iced::{
Element, Fill, Font, Padding, Task,
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},
},
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
};
// 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};
pub trait Memory {
fn peek(&self, val: usize) -> Option<u8>;
fn len(&self) -> usize;
fn edit_ver(&self) -> usize;
}
use nes_emu::{PPU, Mapped, PPUMMRegisters};
#[derive(Debug, Clone, Copy)]
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 {
fn peek(&self, val: usize) -> Option<u8>;
fn len(&self) -> usize;
}
impl Buffer for &[u8] {
impl Buffer for BufferSlice<'_> {
fn peek(&self, val: usize) -> Option<u8> {
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 {
val: raw,
on_edit: None,
@@ -209,7 +202,7 @@ pub struct HexEdit {
new_value: u8,
}
pub struct HexEditor<B, M, R: iced_core::text::Renderer> {
pub struct HexEditor<B, M, R: Renderer> {
val: B,
font_size: Option<Pixels>,
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>
where
R: iced_core::text::Renderer,
R: Renderer,
{
pub fn font(mut self, font: R::Font) -> Self {
self.font = Some(font);
@@ -228,7 +221,7 @@ where
impl<B: Buffer, M, R> HexEditor<B, M, R>
where
R: iced_core::text::Renderer,
R: Renderer,
{
fn value_bounds(&self, renderer: &R, layout: Layout<'_>) -> Rectangle {
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>
where
R: iced_core::text::Renderer,
R: Renderer,
{
fn tag(&self) -> Tag {
Tag::of::<HexEditorState>()
@@ -354,10 +347,10 @@ where
size,
line_height: size.into(),
font,
align_x: iced_core::text::Alignment::Left,
align_y: iced_core::alignment::Vertical::Top,
shaping: iced_core::text::Shaping::Basic,
wrapping: iced_core::text::Wrapping::None,
align_x: iced::advanced::text::Alignment::Left,
align_y: Vertical::Top,
shaping: iced::advanced::text::Shaping::Basic,
wrapping: iced::advanced::text::Wrapping::None,
hint_factor: None,
},
pos,
@@ -444,10 +437,10 @@ where
size,
line_height: size.into(),
font,
align_x: iced_core::text::Alignment::Left,
align_y: iced_core::alignment::Vertical::Top,
shaping: iced_core::text::Shaping::Basic,
wrapping: iced_core::text::Wrapping::None,
align_x: iced::advanced::text::Alignment::Left,
align_y: Vertical::Top,
shaping: iced::advanced::text::Shaping::Basic,
wrapping: iced::advanced::text::Wrapping::None,
hint_factor: None,
},
pos,

View File

@@ -19,14 +19,19 @@ use iced::{
};
use nes_emu::{
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 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"), "/", "crc_check.nes");
@@ -151,6 +156,7 @@ impl Emulator {
fn new() -> (Self, Task<Message>) {
let mut nes = nes_emu::NES::load_nes_file(ROM_FILE).expect("Failed to load nes file");
nes.reset();
nes.debug_log_mut().enable();
let (win, task) = iced::window::open(Settings {
min_size: None,
..Settings::default()
@@ -234,7 +240,10 @@ impl Emulator {
.and_then(|p| {
Task::future(async move {
// 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)));
@@ -375,7 +384,7 @@ impl Emulator {
&& !self.nes.halted()
{
count += 1;
if count > 100000 {
if count > 90_000 {
println!("Loop overran...");
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)]
pub struct DebugLog {
enabled: bool,
current: String,
history: Vec<String>,
max_history: Option<NonZeroUsize>,
@@ -11,6 +12,7 @@ pub struct DebugLog {
impl DebugLog {
pub fn new() -> Self {
Self {
enabled: false,
current: String::new(),
history: vec![],
max_history: None,
@@ -47,25 +49,41 @@ impl DebugLog {
pub fn pop(&mut self) -> Option<String> {
self.history.pop()
}
pub fn enable(&mut self) {
self.enabled = true;
}
}
impl std::fmt::Write for DebugLog {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
let tmp = self.current.write_str(s);
self.rotate();
tmp
if self.enabled {
let tmp = self.current.write_str(s);
self.rotate();
tmp
} else {
Ok(())
}
}
fn write_char(&mut self, c: char) -> std::fmt::Result {
let tmp = self.current.write_char(c);
self.rotate();
tmp
if self.enabled {
let tmp = self.current.write_char(c);
self.rotate();
tmp
} else {
Ok(())
}
}
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::fmt::Result {
let tmp = self.current.write_fmt(args);
self.rotate();
tmp
if self.enabled {
let tmp = self.current.write_fmt(args);
self.rotate();
tmp
} else {
Ok(())
}
}
}

View File

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

View File

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

View File

@@ -1,11 +1,6 @@
use std::fmt;
use bytes::{Bytes, BytesMut};
use iced::{
Point, Size,
advanced::graphics::geometry::Renderer,
widget::canvas::{Fill, Frame},
};
use crate::mem::{Mapped, PpuMem, Value};
@@ -16,12 +11,6 @@ pub struct Color {
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 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
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)
}
pub fn raw_image(&self) -> &[u8] {
&self.raw_rgba
}
pub fn assert_eq(&self, other: &Self) {
// if self.buffer != other.buffer {
for y in 0..H {
@@ -1126,238 +1119,6 @@ impl PPU {
pub fn oam_edit_ver(&self) -> usize {
self.oam.edit_ver
}
pub fn render_name_table<R: Renderer>(&self, mem: &Mapped, frame: &mut Frame<R>) {
for y in 0..60 {
for x in 0..64 {
let row = y % 30;
let col = x % 32;
let off = 0x2000 + 0x400 * (y / 30 * 2 + x / 32);
let name = mem.peek_ppu((off + col + row * 32) as u16).unwrap() as u16 * 16
+ if self.background.second_pattern {
0x1000
} else {
0
};
let attr = mem
.peek_ppu((col / 4 + row / 4 * 8 + 0x3C0 + off) as u16)
.unwrap();
// attr << (((col & 1) << 1) | ((row & 1) << 0)) * 2
// let h_off = ((self.pixel - 1) / 16) % 2;
// let v_off = (self.scanline / 16) % 2;
// let off = v_off * 4 + h_off * 2;
let palette = (attr >> (((col & 2) << 0) | ((row & 2) << 1))) & 0x3;
for y_off in 0..8 {
let low = mem.peek_ppu(name + y_off).unwrap();
let high = mem.peek_ppu(name + y_off + 8).unwrap();
for bit in 0..8 {
frame.fill_rectangle(
Point::new(
x as f32 * 8. + 8. - bit as f32,
y as f32 * 8. + y_off as f32,
),
Size::new(1., 1.),
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
(false, false) => self.palette.color(0),
(true, false) => self.palette.color(1 + 4 * palette),
(false, true) => self.palette.color(2 + 4 * palette),
(true, true) => self.palette.color(3 + 4 * palette),
// (false, false) => Color { r: 0, g: 0, b: 0 },
// (true, false) => Color {
// r: 64,
// g: 64,
// b: 64,
// },
// (false, true) => Color {
// r: 128,
// g: 128,
// b: 128,
// },
// (true, true) => Color {
// r: 255,
// g: 255,
// b: 255,
// },
},
);
}
}
// for
// let pat = mem.peek();
}
}
}
pub fn name_cursor_info(&self, mem: &Mapped, cursor: Point) -> Option<String> {
let x = (cursor.x / 8.) as usize;
let y = (cursor.y / 8.) as usize;
if x < 64 && y < 60 {
let row = y % 30;
let col = x % 32;
let off = 0x2000 + 0x400 * (y / 30 * 2 + x / 32);
let name = mem.peek_ppu((off + col + row * 32) as u16).unwrap() as usize;
let attr = mem
.peek_ppu((col / 4 + row / 4 * 8 + 0x3C0 + off) as u16)
.unwrap();
Some(format!(
"Row, Column: {}, {}
X, Y: {}, {}
Tilemap address: ${:04X}
Tile Index: ${:02X}
Tile Address (PPU): ${:04X}
Tile Address (CHR): ${:04X}
Palette Index {}
Palette Address ${:04X}
Attribute Address ${:04X}
Attribute data: ${:02X}
",
row,
col,
col * 8,
row * 8,
off + col + row * 32,
name,
name * 16
+ if self.background.second_pattern {
0x1000
} else {
0
},
name * 16
+ if self.background.second_pattern {
0x1000
} else {
0
},
(attr >> (((col & 1) << 1) | ((row & 1) << 2))) & 0x3,
((attr >> (((col & 1) << 1) | ((row & 1) << 2))) & 0x3) as usize * 4 + 0x3F00,
col / 4 + row / 4 * 8 + 0x3C0 + off,
attr,
))
} else {
None
}
}
pub fn render_pattern_tables<R: Renderer>(&self, mem: &Mapped, frame: &mut Frame<R>) {
for y in 0..16 {
for x in 0..16 {
let name = (y * 16 + x) * 16;
for y_off in 0..8 {
let low = mem.peek_ppu(name + y_off).unwrap();
let high = mem.peek_ppu(name + y_off + 8).unwrap();
for bit in 0..8 {
frame.fill_rectangle(
Point::new(
x as f32 * 8. + 8. - bit as f32,
y as f32 * 8. + y_off as f32,
),
Size::new(1., 1.),
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
(false, false) => Color { r: 0, g: 0, b: 0 },
(true, false) => Color {
r: 64,
g: 64,
b: 64,
},
(false, true) => Color {
r: 128,
g: 128,
b: 128,
},
(true, true) => Color {
r: 255,
g: 255,
b: 255,
},
},
);
}
}
}
}
for y in 0..16 {
for x in 0..16 {
let name = (y * 16 + x) * 16;
for y_off in 0..8 {
let low = mem.peek_ppu(name + y_off + 0x1000).unwrap();
let high = mem.peek_ppu(name + y_off + 8 + 0x1000).unwrap();
for bit in 0..8 {
frame.fill_rectangle(
Point::new(
x as f32 * 8. + 8. - bit as f32,
y as f32 * 8. + y_off as f32 + 130.,
),
Size::new(1., 1.),
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
(false, false) => Color { r: 0, g: 0, b: 0 },
(true, false) => Color {
r: 64,
g: 64,
b: 64,
},
(false, true) => Color {
r: 128,
g: 128,
b: 128,
},
(true, true) => Color {
r: 255,
g: 255,
b: 255,
},
},
);
}
}
// for
// let pat = mem.peek();
}
}
}
pub fn pattern_cursor_info(&self, cursor: Point) -> Option<String> {
let x = (cursor.x / 8.) as usize;
let y = (cursor.y / 8.) as usize;
if x < 16 && y < 32 {
Some(format!(
"Tile address (PPU): {:04X}\nTile address (CHR): {:04X}\nIndex: {:02X}",
(y * 16 + x) * 16,
(y * 16 + x) * 16,
((y % 16) * 16 + x),
))
} else {
None
}
}
pub fn render_palette<R: Renderer>(&self, frame: &mut Frame<R>) {
for y in 0..8 {
for x in 0..4 {
frame.fill_rectangle(
Point::new(x as f32 * 10., y as f32 * 10.),
Size::new(10., 10.),
self.palette.colors[(self.palette.ram[x + y * 4] & 0x3F) as usize],
);
}
}
}
pub fn palette_cursor_info(&self, cursor: Point) -> Option<String> {
let x = (cursor.x / 10.) as usize;
let y = (cursor.y / 10.) as usize;
if x < 4 && y < 8 {
Some(format!(
"Index: {:02X}\nValue: {:02X}\nColor code: {}",
x + y * 4,
self.palette.ram[x + y * 4] & 0x3F,
self.palette.colors[(self.palette.ram[x + y * 4] & 0x3F) as usize],
))
} else {
None
}
}
}
#[cfg(test)]
@@ -1414,3 +1175,253 @@ mod tests {
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>) {
for y in 0..60 {
for x in 0..64 {
let row = y % 30;
let col = x % 32;
let off = 0x2000 + 0x400 * (y / 30 * 2 + x / 32);
let name = mem.peek_ppu((off + col + row * 32) as u16).unwrap() as u16 * 16
+ if self.background.second_pattern {
0x1000
} else {
0
};
let attr = mem
.peek_ppu((col / 4 + row / 4 * 8 + 0x3C0 + off) as u16)
.unwrap();
// attr << (((col & 1) << 1) | ((row & 1) << 0)) * 2
// let h_off = ((self.pixel - 1) / 16) % 2;
// let v_off = (self.scanline / 16) % 2;
// let off = v_off * 4 + h_off * 2;
let palette = (attr >> (((col & 2) << 0) | ((row & 2) << 1))) & 0x3;
for y_off in 0..8 {
let low = mem.peek_ppu(name + y_off).unwrap();
let high = mem.peek_ppu(name + y_off + 8).unwrap();
for bit in 0..8 {
frame.fill_rectangle(
Point::new(
x as f32 * 8. + 8. - bit as f32,
y as f32 * 8. + y_off as f32,
),
Size::new(1., 1.),
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
(false, false) => self.palette.color(0),
(true, false) => self.palette.color(1 + 4 * palette),
(false, true) => self.palette.color(2 + 4 * palette),
(true, true) => self.palette.color(3 + 4 * palette),
// (false, false) => Color { r: 0, g: 0, b: 0 },
// (true, false) => Color {
// r: 64,
// g: 64,
// b: 64,
// },
// (false, true) => Color {
// r: 128,
// g: 128,
// b: 128,
// },
// (true, true) => Color {
// r: 255,
// g: 255,
// b: 255,
// },
},
);
}
}
// for
// let pat = mem.peek();
}
}
}
pub fn name_cursor_info(&self, mem: &Mapped, cursor: Point) -> Option<String> {
let x = (cursor.x / 8.) as usize;
let y = (cursor.y / 8.) as usize;
if x < 64 && y < 60 {
let row = y % 30;
let col = x % 32;
let off = 0x2000 + 0x400 * (y / 30 * 2 + x / 32);
let name = mem.peek_ppu((off + col + row * 32) as u16).unwrap() as usize;
let attr = mem
.peek_ppu((col / 4 + row / 4 * 8 + 0x3C0 + off) as u16)
.unwrap();
Some(format!(
"Row, Column: {}, {}
X, Y: {}, {}
Tilemap address: ${:04X}
Tile Index: ${:02X}
Tile Address (PPU): ${:04X}
Tile Address (CHR): ${:04X}
Palette Index {}
Palette Address ${:04X}
Attribute Address ${:04X}
Attribute data: ${:02X}
",
row,
col,
col * 8,
row * 8,
off + col + row * 32,
name,
name * 16
+ if self.background.second_pattern {
0x1000
} else {
0
},
name * 16
+ if self.background.second_pattern {
0x1000
} else {
0
},
(attr >> (((col & 1) << 1) | ((row & 1) << 2))) & 0x3,
((attr >> (((col & 1) << 1) | ((row & 1) << 2))) & 0x3) as usize * 4 + 0x3F00,
col / 4 + row / 4 * 8 + 0x3C0 + off,
attr,
))
} else {
None
}
}
pub fn render_pattern_tables<R: Renderer>(&self, mem: &Mapped, frame: &mut Frame<R>) {
for y in 0..16 {
for x in 0..16 {
let name = (y * 16 + x) * 16;
for y_off in 0..8 {
let low = mem.peek_ppu(name + y_off).unwrap();
let high = mem.peek_ppu(name + y_off + 8).unwrap();
for bit in 0..8 {
frame.fill_rectangle(
Point::new(
x as f32 * 8. + 8. - bit as f32,
y as f32 * 8. + y_off as f32,
),
Size::new(1., 1.),
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
(false, false) => Color { r: 0, g: 0, b: 0 },
(true, false) => Color {
r: 64,
g: 64,
b: 64,
},
(false, true) => Color {
r: 128,
g: 128,
b: 128,
},
(true, true) => Color {
r: 255,
g: 255,
b: 255,
},
},
);
}
}
}
}
for y in 0..16 {
for x in 0..16 {
let name = (y * 16 + x) * 16;
for y_off in 0..8 {
let low = mem.peek_ppu(name + y_off + 0x1000).unwrap();
let high = mem.peek_ppu(name + y_off + 8 + 0x1000).unwrap();
for bit in 0..8 {
frame.fill_rectangle(
Point::new(
x as f32 * 8. + 8. - bit as f32,
y as f32 * 8. + y_off as f32 + 130.,
),
Size::new(1., 1.),
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
(false, false) => Color { r: 0, g: 0, b: 0 },
(true, false) => Color {
r: 64,
g: 64,
b: 64,
},
(false, true) => Color {
r: 128,
g: 128,
b: 128,
},
(true, true) => Color {
r: 255,
g: 255,
b: 255,
},
},
);
}
}
// for
// let pat = mem.peek();
}
}
}
pub fn pattern_cursor_info(&self, cursor: Point) -> Option<String> {
let x = (cursor.x / 8.) as usize;
let y = (cursor.y / 8.) as usize;
if x < 16 && y < 32 {
Some(format!(
"Tile address (PPU): {:04X}\nTile address (CHR): {:04X}\nIndex: {:02X}",
(y * 16 + x) * 16,
(y * 16 + x) * 16,
((y % 16) * 16 + x),
))
} else {
None
}
}
pub fn render_palette<R: Renderer>(&self, frame: &mut Frame<R>) {
for y in 0..8 {
for x in 0..4 {
frame.fill_rectangle(
Point::new(x as f32 * 10., y as f32 * 10.),
Size::new(10., 10.),
self.palette.colors[(self.palette.ram[x + y * 4] & 0x3F) as usize],
);
}
}
}
pub fn palette_cursor_info(&self, cursor: Point) -> Option<String> {
let x = (cursor.x / 10.) as usize;
let y = (cursor.y / 10.) as usize;
if x < 4 && y < 8 {
Some(format!(
"Index: {:02X}\nValue: {:02X}\nColor code: {}",
x + y * 4,
self.palette.ram[x + y * 4] & 0x3F,
self.palette.colors[(self.palette.ram[x + y * 4] & 0x3F) as usize],
))
} else {
None
}
}
}
}

View File

@@ -2,8 +2,8 @@
.include "audio_inp.s"
zp_res TIMER_LOW
; zp_res TIMER_LOW
; zp_res TIMER_LOW
zp_res SWEEP
zp_res DLCV
; zp_res TIMER_LOW
reset:
@@ -14,10 +14,12 @@ reset:
lda #$01
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 DLCV
lda #$7F ;
sta PULSE_CH1_SWEEP
sta SWEEP
lda #$6F
sta PULSE_CH1_TLOW
sta TIMER_LOW
@@ -43,21 +45,32 @@ reset:
write_char_x ':'
load_ppu_addr $2061
; write_char_x 't'
; write_char_x 'i'
; write_char_x 'm'
; write_char_x 'e'
; write_char_x 'r'
; write_char_x ' '
; write_char_x 'h'
; write_char_x 'o'
; write_char_x 'w'
; write_char_x ':'
write_char_x 's'
write_char_x 'w'
write_char_x 'e'
write_char_x 'e'
write_char_x 'p'
write_char_x ':'
load_ppu_addr $2081
write_char_x 'd'
write_char_x 'l'
write_char_x 'c'
write_char_x 'v'
write_char_x ':'
load_ppu_addr $204C
lda TIMER_LOW
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
loop:
jmp loop
@@ -67,28 +80,83 @@ loop:
audio_nmi:
EOR JOY_PREV
STA JOY_PREV
lda JOYTEMP
and #JOY_UP_MASK
and JOY_PREV
beq next_1
beq down
load_ppu_addr $204C
lda #$01
adc TIMER_LOW
sta TIMER_LOW
sta PULSE_CH1_TLOW
jsr write_hex_a
next_1:
down:
lda JOYTEMP
and #JOY_DOWN_MASK
and JOY_PREV
beq next_2
beq left
load_ppu_addr $204C
lda #$FF
adc TIMER_LOW
sta TIMER_LOW
sta PULSE_CH1_TLOW
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
STA JOY_PREV
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>