Compare commits
3 Commits
3010469c8a
...
e882b3b325
| Author | SHA1 | Date | |
|---|---|---|---|
|
e882b3b325
|
|||
|
f6c3d073e7
|
|||
|
b433148843
|
3
.helix/languages.toml
Normal file
3
.helix/languages.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[language-server.rust-analyzer]
|
||||||
|
config = { cargo = { features = "all" } }
|
||||||
|
|
||||||
1279
Cargo.lock
generated
1279
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
54
Cargo.toml
54
Cargo.toml
@@ -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
13
index.html
Normal 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>
|
||||||
118
src/apu.rs
118
src/apu.rs
@@ -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(),
|
||||||
@@ -242,14 +267,12 @@ Output: {16:>3} ${16:02X}
|
|||||||
0,
|
0,
|
||||||
self.enabled,
|
self.enabled,
|
||||||
self.period_timer,
|
self.period_timer,
|
||||||
self.cur, // ?
|
self.cur, // ?
|
||||||
self.counter.current,
|
self.counter.current,
|
||||||
self.envelope_counter,
|
self.envelope_counter,
|
||||||
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
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn view<'s, T: 's>(&'s self) -> Element<'s, T> {
|
#[cfg(feature = "iced")]
|
||||||
column![
|
mod apu_iced {
|
||||||
self.pulse_1.view(),
|
use super::*;
|
||||||
self.pulse_2.view(),
|
use iced::{
|
||||||
self.triangle.view(),
|
Element, Font,
|
||||||
self.noise.view(),
|
widget::{column, text},
|
||||||
self.dmc.view(),
|
};
|
||||||
]
|
|
||||||
.into()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -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,
|
||||||
@@ -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
305
src/bin/wasm/main.rs
Normal 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);
|
||||||
|
}
|
||||||
36
src/debug.rs
36
src/debug.rs
@@ -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 {
|
||||||
let tmp = self.current.write_str(s);
|
if self.enabled {
|
||||||
self.rotate();
|
let tmp = self.current.write_str(s);
|
||||||
tmp
|
self.rotate();
|
||||||
|
tmp
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_char(&mut self, c: char) -> std::fmt::Result {
|
fn write_char(&mut self, c: char) -> std::fmt::Result {
|
||||||
let tmp = self.current.write_char(c);
|
if self.enabled {
|
||||||
self.rotate();
|
let tmp = self.current.write_char(c);
|
||||||
tmp
|
self.rotate();
|
||||||
|
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 {
|
||||||
let tmp = self.current.write_fmt(args);
|
if self.enabled {
|
||||||
self.rotate();
|
let tmp = self.current.write_fmt(args);
|
||||||
tmp
|
self.rotate();
|
||||||
|
tmp
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
src/lib.rs
28
src/lib.rs
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
497
src/ppu.rs
497
src/ppu.rs
@@ -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,238 +1119,6 @@ impl PPU {
|
|||||||
pub fn oam_edit_ver(&self) -> usize {
|
pub fn oam_edit_ver(&self) -> usize {
|
||||||
self.oam.edit_ver
|
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)]
|
#[cfg(test)]
|
||||||
@@ -1414,3 +1175,253 @@ mod tests {
|
|||||||
assert_eq!(ppu.background.w, false);
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
7
wasm.fish
Executable 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
47
wasm/wasm.html
Normal 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>
|
||||||
Reference in New Issue
Block a user