├── external ├── rusticnes-core │ ├── .github │ │ └── FUNDING.yml │ ├── ntscpalette.pal │ ├── .gitignore │ ├── assets │ │ ├── 5x3font.png │ │ ├── troll8x8.chr │ │ └── chicago8x8.chr │ ├── Cargo.toml │ ├── audio_pipe.sh │ ├── src │ │ ├── lib.rs │ │ ├── mmc │ │ │ ├── mod.rs │ │ │ ├── none.rs │ │ │ ├── mapper.rs │ │ │ ├── mirroring.rs │ │ │ ├── cnrom.rs │ │ │ ├── nrom.rs │ │ │ ├── uxrom.rs │ │ │ ├── bnrom.rs │ │ │ ├── gxrom.rs │ │ │ ├── axrom.rs │ │ │ ├── ines31.rs │ │ │ ├── pxrom.rs │ │ │ └── action53.rs │ │ ├── apu │ │ │ ├── ring_buffer.rs │ │ │ ├── length_counter.rs │ │ │ ├── volume_envelope.rs │ │ │ ├── audio_channel.rs │ │ │ ├── noise.rs │ │ │ ├── triangle.rs │ │ │ ├── filters.rs │ │ │ ├── dmc.rs │ │ │ └── pulse.rs │ │ ├── memoryblock.rs │ │ ├── cartridge.rs │ │ ├── opcode_info.rs │ │ ├── nes.rs │ │ └── tracked_events.rs │ ├── LICENSE.txt │ └── README.md └── rusticnes-ui-common │ ├── .github │ └── FUNDING.yml │ ├── .gitignore │ ├── README.md │ ├── src │ ├── assets │ │ └── 8x8_font.png │ ├── panel.rs │ ├── lib.rs │ ├── test_window.rs │ ├── events.rs │ ├── cpu_window.rs │ ├── game_window.rs │ └── memory_window.rs │ ├── Cargo.toml │ └── planning.txt ├── .gitignore ├── assets ├── ffmpeg-icon.png ├── rusticnes-icon.png ├── nsf-presenter-icon.ico ├── nsf-presenter-icon.png └── nsf-presenter-icon-xl.png ├── src ├── gui │ ├── slint │ │ ├── arrow-reset.svg │ │ ├── circle-error.svg │ │ ├── info.svg │ │ ├── arrow-import.svg │ │ ├── arrow-export.svg │ │ ├── chevron-down.svg │ │ ├── module-metadata.slint │ │ ├── toolbar-button.slint │ │ ├── color-picker.slint │ │ └── channel-config.slint │ └── render_thread.rs ├── main.rs ├── emulator │ ├── config.rs │ ├── mod.rs │ ├── m3u_searcher.rs │ └── nsf.rs ├── video_builder │ ├── vb_unwrap.rs │ ├── backgrounds │ │ ├── debug_bg.rs │ │ ├── image_bg.rs │ │ ├── mod.rs │ │ └── video_bg.rs │ ├── video_options.rs │ └── ffmpeg_hacks.rs └── renderer │ ├── options.rs │ └── mod.rs ├── LICENSE ├── Cargo.toml └── README.md /external/rusticnes-core/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: "zeta0134" 2 | -------------------------------------------------------------------------------- /external/rusticnes-ui-common/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: "zeta0134" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.nsf 3 | *.mp4 4 | *.mkv 5 | *.pcm 6 | test-config.toml 7 | -------------------------------------------------------------------------------- /assets/ffmpeg-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nununoisy/nsf-presenter-rs/HEAD/assets/ffmpeg-icon.png -------------------------------------------------------------------------------- /assets/rusticnes-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nununoisy/nsf-presenter-rs/HEAD/assets/rusticnes-icon.png -------------------------------------------------------------------------------- /assets/nsf-presenter-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nununoisy/nsf-presenter-rs/HEAD/assets/nsf-presenter-icon.ico -------------------------------------------------------------------------------- /assets/nsf-presenter-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nununoisy/nsf-presenter-rs/HEAD/assets/nsf-presenter-icon.png -------------------------------------------------------------------------------- /assets/nsf-presenter-icon-xl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nununoisy/nsf-presenter-rs/HEAD/assets/nsf-presenter-icon-xl.png -------------------------------------------------------------------------------- /external/rusticnes-core/ntscpalette.pal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nununoisy/nsf-presenter-rs/HEAD/external/rusticnes-core/ntscpalette.pal -------------------------------------------------------------------------------- /external/rusticnes-core/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target/ 3 | tests/ 4 | **/*.rs.bk 5 | *.nes 6 | *.zip 7 | *.ogg 8 | *.flac 9 | *.raw 10 | -------------------------------------------------------------------------------- /external/rusticnes-core/assets/5x3font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nununoisy/nsf-presenter-rs/HEAD/external/rusticnes-core/assets/5x3font.png -------------------------------------------------------------------------------- /external/rusticnes-core/assets/troll8x8.chr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nununoisy/nsf-presenter-rs/HEAD/external/rusticnes-core/assets/troll8x8.chr -------------------------------------------------------------------------------- /external/rusticnes-ui-common/.gitignore: -------------------------------------------------------------------------------- 1 | # This is a library, do not commit the lock file 2 | Cargo.lock 3 | 4 | # Ignore build artifacts 5 | target -------------------------------------------------------------------------------- /external/rusticnes-core/assets/chicago8x8.chr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nununoisy/nsf-presenter-rs/HEAD/external/rusticnes-core/assets/chicago8x8.chr -------------------------------------------------------------------------------- /external/rusticnes-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rusticnes-core" 3 | version = "0.2.0" 4 | authors = ["Nicholas Flynt "] 5 | -------------------------------------------------------------------------------- /external/rusticnes-ui-common/README.md: -------------------------------------------------------------------------------- 1 | # rusticnes-ui-common 2 | Platform independent interface components for running the RusticNES emulator. Very WIP. 3 | -------------------------------------------------------------------------------- /external/rusticnes-ui-common/src/assets/8x8_font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nununoisy/nsf-presenter-rs/HEAD/external/rusticnes-ui-common/src/assets/8x8_font.png -------------------------------------------------------------------------------- /external/rusticnes-core/audio_pipe.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm audiodump.raw 3 | touch audiodump.raw 4 | tail -f audiodump.raw | play --type raw --encoding signed-integer --bits 16 --endian big --channels 1 --rate 44100 - 5 | -------------------------------------------------------------------------------- /src/gui/slint/arrow-reset.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/gui/slint/circle-error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /external/rusticnes-ui-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rusticnes-ui-common" 3 | version = "0.0.1" 4 | authors = ["Nicholas Flynt "] 5 | 6 | [dependencies] 7 | csscolorparser = "0.6.1" 8 | image = "0.19" 9 | toml = "0.5" 10 | regex = "1.6" 11 | rusticnes-core = { git = "https://github.com/zeta0134/rusticnes-core", rev = "cbd7146ad66dd8e95a35d46828ad7878f75de638" } -------------------------------------------------------------------------------- /external/rusticnes-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod addressing; 2 | pub mod apu; 3 | pub mod asm; 4 | pub mod cartridge; 5 | pub mod cycle_cpu; 6 | pub mod tracked_events; 7 | pub mod ines; 8 | pub mod memory; 9 | pub mod memoryblock; 10 | pub mod mmc; 11 | pub mod nes; 12 | pub mod nsf; 13 | pub mod opcodes; 14 | pub mod opcode_info; 15 | pub mod palettes; 16 | pub mod ppu; 17 | pub mod unofficial_opcodes; -------------------------------------------------------------------------------- /external/rusticnes-ui-common/src/panel.rs: -------------------------------------------------------------------------------- 1 | use application::RuntimeState; 2 | use drawing::SimpleBuffer; 3 | use events::Event; 4 | 5 | pub trait Panel { 6 | fn title(&self) -> &str; 7 | fn handle_event(&mut self, runtime: &RuntimeState, event: Event) -> Vec; 8 | fn active_canvas(&self) -> &SimpleBuffer; 9 | fn scale_factor(&self) -> u32 {return 1;} 10 | fn shown(&self) -> bool; 11 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod video_builder; 2 | mod emulator; 3 | mod renderer; 4 | mod cli; 5 | mod gui; 6 | 7 | use std::env; 8 | use build_time::build_time_utc; 9 | 10 | fn main() { 11 | println!("NSFPresenter started! (built {})", build_time_utc!("%Y-%m-%dT%H:%M:%S")); 12 | video_builder::init().unwrap(); 13 | 14 | match env::args().len() { 15 | 1 => gui::run(), 16 | _ => cli::run() 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/mmc/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod mapper; 2 | pub mod mirroring; 3 | 4 | pub mod action53; 5 | pub mod axrom; 6 | pub mod bnrom; 7 | pub mod cnrom; 8 | pub mod fme7; 9 | pub mod gxrom; 10 | pub mod ines31; 11 | pub mod mmc1; 12 | pub mod mmc3; 13 | pub mod mmc5; 14 | pub mod n163; 15 | pub mod none; 16 | pub mod nrom; 17 | pub mod nsf; 18 | pub mod pxrom; 19 | pub mod uxrom; 20 | pub mod vrc6; 21 | pub mod vrc7; 22 | pub mod fds; 23 | -------------------------------------------------------------------------------- /src/gui/slint/info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/gui/slint/arrow-import.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/gui/slint/arrow-export.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /external/rusticnes-ui-common/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate csscolorparser; 2 | extern crate image; 3 | extern crate regex; 4 | extern crate rusticnes_core; 5 | extern crate toml; 6 | 7 | pub mod application; 8 | pub mod events; 9 | pub mod panel; 10 | pub mod drawing; 11 | 12 | pub use events::Event; 13 | 14 | pub mod apu_window; 15 | pub mod cpu_window; 16 | pub mod game_window; 17 | pub mod event_window; 18 | pub mod memory_window; 19 | pub mod test_window; 20 | pub mod piano_roll_window; 21 | pub mod ppu_window; 22 | pub mod settings; -------------------------------------------------------------------------------- /src/emulator/config.rs: -------------------------------------------------------------------------------- 1 | pub const DEFAULT_CONFIG: &str = r###" 2 | [piano_roll] 3 | draw_piano_strings = false 4 | key_length = 24 5 | key_thickness = 5 6 | octave_count = 9 7 | scale_factor = 1 8 | speed_multiplier = 1 9 | starting_octave = 0 10 | waveform_height = 48 11 | oscilloscope_glow_thickness = 2.0 12 | oscilloscope_line_thickness = 0.75 13 | "###; 14 | 15 | pub const REQUIRED_CONFIG: &str = r###" 16 | [piano_roll] 17 | background_color = "rgba(0, 0, 0, 0)" 18 | canvas_width = 960 19 | canvas_height = 540 20 | 21 | [piano_roll.settings.APU."Final Mix"] 22 | hidden = true 23 | "###; -------------------------------------------------------------------------------- /src/gui/slint/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/video_builder/vb_unwrap.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, anyhow}; 2 | use ffmpeg_next::{Error, format}; 3 | 4 | pub trait VideoBuilderUnwrap { 5 | fn vb_unwrap(self) -> Result; 6 | } 7 | 8 | impl VideoBuilderUnwrap for Result { 9 | fn vb_unwrap(self) -> Result { 10 | match self { 11 | Ok(v) => Ok(v), 12 | Err(e) => Err(anyhow!("FFMPEG error: {}", e)) 13 | } 14 | } 15 | } 16 | 17 | impl VideoBuilderUnwrap for Result { 18 | fn vb_unwrap(self) -> Result { 19 | match self { 20 | Ok(v) => Ok(v), 21 | Err(e) => Err(anyhow!("FFMPEG pixel parsing error: {}", e)) 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/video_builder/backgrounds/debug_bg.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use ffmpeg_next::{format, frame}; 3 | use super::VideoBackground; 4 | 5 | pub struct DebugBackground(u32, u32); 6 | 7 | impl DebugBackground { 8 | pub fn open>(path: P, width: u32, height: u32) -> Option { 9 | if path.as_ref().to_str().unwrap_or("") != "__debug__" { 10 | return None; 11 | } 12 | 13 | Some(Self(width, height)) 14 | } 15 | } 16 | 17 | impl VideoBackground for DebugBackground { 18 | fn next_frame(&mut self) -> frame::Video { 19 | let mut frame = frame::Video::new(format::Pixel::RGBA, self.0, self.1); 20 | frame.plane_mut::<(u8, u8, u8, u8)>(0) 21 | .iter_mut() 22 | .for_each(|px| *px = (255, 0, 0, 128)); 23 | frame 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/video_builder/video_options.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use ffmpeg_next::Rational; 3 | 4 | #[derive(Clone)] 5 | pub struct VideoOptions { 6 | pub output_path: String, 7 | pub metadata: HashMap, 8 | pub background_path: Option, 9 | 10 | pub video_time_base: Rational, 11 | pub video_codec: String, 12 | pub video_codec_params: HashMap, 13 | pub pixel_format_in: String, 14 | pub pixel_format_out: String, 15 | pub resolution_in: (u32, u32), 16 | pub resolution_out: (u32, u32), 17 | 18 | pub audio_time_base: Rational, 19 | pub audio_codec: String, 20 | pub audio_codec_params: HashMap, 21 | pub audio_channels: i32, 22 | pub sample_format_in: String, 23 | pub sample_format_out: String, 24 | pub sample_rate: i32 25 | } -------------------------------------------------------------------------------- /external/rusticnes-core/src/mmc/none.rs: -------------------------------------------------------------------------------- 1 | // A dummy mapper with no loaded data. Useful for initializing an NesState 2 | // with no actual cartridge loaded. 3 | 4 | use mmc::mapper::*; 5 | 6 | pub struct NoneMapper { 7 | } 8 | 9 | impl NoneMapper { 10 | pub fn new() -> NoneMapper { 11 | return NoneMapper { 12 | } 13 | } 14 | } 15 | 16 | impl Mapper for NoneMapper { 17 | fn mirroring(&self) -> Mirroring { 18 | return Mirroring::Horizontal; 19 | } 20 | 21 | fn debug_read_cpu(&self, _: u16) -> Option { 22 | return None; 23 | } 24 | 25 | fn debug_read_ppu(&self, _: u16) -> Option { 26 | return None; 27 | } 28 | 29 | fn write_cpu(&mut self, _: u16, _: u8) { 30 | //Do nothing 31 | } 32 | 33 | fn write_ppu(&mut self, _: u16, _: u8) { 34 | //Do nothing 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/video_builder/backgrounds/image_bg.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use ffmpeg_next::{format, frame}; 3 | use image; 4 | use crate::video_builder::backgrounds::VideoBackground; 5 | 6 | pub struct ImageBackground(frame::Video); 7 | 8 | impl ImageBackground { 9 | pub fn open>(path: P, w: u32, h: u32) -> Option { 10 | let dyn_img = match image::open(path) { 11 | Ok(i) => i, 12 | Err(_) => return None 13 | }; 14 | let img = image::imageops::resize(&dyn_img.to_rgba(), w, h, image::imageops::Gaussian); 15 | 16 | let mut frame = frame::Video::new(format::Pixel::RGBA, w, h); 17 | frame.data_mut(0).copy_from_slice(&img.into_raw()); 18 | 19 | Some(Self(frame)) 20 | } 21 | } 22 | 23 | impl VideoBackground for ImageBackground { 24 | fn next_frame(&mut self) -> frame::Video { 25 | self.0.clone() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/video_builder/backgrounds/mod.rs: -------------------------------------------------------------------------------- 1 | mod debug_bg; 2 | mod video_bg; 3 | mod image_bg; 4 | 5 | use std::path::Path; 6 | use ffmpeg_next::frame; 7 | 8 | pub trait VideoBackground { 9 | fn next_frame(&mut self) -> frame::Video; 10 | } 11 | 12 | pub fn get_video_background>(path: P, width: u32, height: u32) -> Option> { 13 | if let Some(debug_vbg) = debug_bg::DebugBackground::open(&path, width, height) { 14 | return Some(Box::new(debug_vbg)); 15 | } 16 | 17 | // Use FFmpeg for GIFs 18 | if !path.as_ref().to_str().unwrap_or("").ends_with(".gif") { 19 | if let Some(image_vbg) = image_bg::ImageBackground::open(&path, width, height) { 20 | return Some(Box::new(image_vbg)); 21 | } 22 | } 23 | 24 | if let Some(video_vbg) = video_bg::MTVideoBackground::open(path.as_ref().to_str().unwrap_or(""), width, height) { 25 | return Some(Box::new(video_vbg)); 26 | } 27 | 28 | None 29 | } 30 | -------------------------------------------------------------------------------- /src/emulator/mod.rs: -------------------------------------------------------------------------------- 1 | mod nsf; 2 | mod nsfeparser; 3 | mod emulator; 4 | pub mod m3u_searcher; 5 | mod config; 6 | 7 | use std::fmt::{Display, Formatter}; 8 | 9 | pub use emulator::Emulator; 10 | pub use nsf::{Nsf, NsfDriverType}; 11 | pub const NES_NTSC_FRAMERATE: f64 = 1789772.7272727 / 29780.5; 12 | // pub const NES_PAL_FRAMERATE: f64 = 1662607.0 / 33247.5; 13 | 14 | #[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy)] 15 | pub struct SongPosition { 16 | pub end: bool, 17 | pub frame: u8, 18 | pub row: u8 19 | } 20 | 21 | impl SongPosition { 22 | pub fn new(frame: u8, row: u8) -> Self { 23 | Self { 24 | end: false, 25 | frame, 26 | row 27 | } 28 | } 29 | 30 | pub fn at_end() -> Self { 31 | Self { 32 | end: true, 33 | frame: 0, 34 | row: 0 35 | } 36 | } 37 | } 38 | 39 | impl Display for SongPosition { 40 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 41 | write!(f, "{:02X}:{:02X}", self.frame, self.row) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/apu/ring_buffer.rs: -------------------------------------------------------------------------------- 1 | // Implements a rolling buffer for audio samples, with a fixed length and infinite operation. 2 | // Indices wrap around from the end of this buffer back to the beginning, so no memory allocation 3 | // is needed once it's been constructed. 4 | 5 | // Not intended to be generic, or particularly safe beyond rust's usual guarantees. 6 | 7 | pub struct RingBuffer { 8 | buffer: Vec, 9 | index: usize 10 | } 11 | 12 | impl RingBuffer { 13 | pub fn new(length: usize) -> RingBuffer { 14 | return RingBuffer { 15 | buffer: vec!(0i16; length), 16 | index: 0 17 | }; 18 | } 19 | 20 | pub fn push(&mut self, sample: i16) { 21 | self.buffer[self.index] = sample; 22 | self.index = (self.index + 1) % self.buffer.len(); 23 | } 24 | 25 | pub fn buffer(&self) -> &Vec { 26 | return &self.buffer; 27 | } 28 | 29 | pub fn index(&self) -> usize { 30 | return self.index; 31 | } 32 | 33 | pub fn reset(&mut self) { 34 | self.index = 0; 35 | } 36 | } -------------------------------------------------------------------------------- /external/rusticnes-core/src/apu/length_counter.rs: -------------------------------------------------------------------------------- 1 | pub struct LengthCounterState { 2 | pub length: u8, 3 | pub halt_flag: bool, 4 | pub channel_enabled: bool, 5 | } 6 | 7 | impl LengthCounterState{ 8 | pub fn new() -> LengthCounterState { 9 | return LengthCounterState { 10 | length: 0, 11 | halt_flag: false, 12 | channel_enabled: false, 13 | } 14 | } 15 | 16 | pub fn clock(&mut self) { 17 | if self.channel_enabled { 18 | if self.length > 0 && !(self.halt_flag) { 19 | self.length -= 1; 20 | } 21 | } else { 22 | self.length = 0; 23 | } 24 | } 25 | 26 | pub fn set_length(&mut self, index: u8) { 27 | if self.channel_enabled { 28 | let table = [ 29 | 10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 30 | 12, 16, 24, 18, 48, 20, 96, 22, 192, 24, 72, 26, 16, 28, 32, 30]; 31 | self.length = table[index as usize]; 32 | } else { 33 | self.length = 0 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /external/rusticnes-core/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 zeta0134 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 nununoisy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nsf-presenter-rs" 3 | version = "0.6.1" 4 | edition = "2021" 5 | build = "build.rs" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [profile.dev] 10 | overflow-checks = false 11 | 12 | [profile.release] 13 | lto = "fat" 14 | codegen-units = 1 15 | panic = "abort" 16 | 17 | [dependencies] 18 | rusticnes-core = { path = "external/rusticnes-core" } 19 | rusticnes-ui-common = { path = "external/rusticnes-ui-common" } 20 | ffmpeg-next = "6.1.0" 21 | ffmpeg-sys-next = "6.1.0" 22 | clap = "4.2.1" 23 | indicatif = "0.17.7" 24 | image = "0.19.0" 25 | build-time = "0.1.3" 26 | slint = "1.3.2" 27 | native-dialog = "0.6.3" 28 | encoding_rs = "0.8.32" 29 | glob = "0.3.1" 30 | csscolorparser = "0.6.2" 31 | toml = "0.8.8" 32 | serde = { version = "1.0", features = ["derive"] } 33 | anyhow = "1.0.75" 34 | 35 | [build-dependencies] 36 | slint-build = "1.3.2" 37 | 38 | [target.'cfg(windows)'.build-dependencies] 39 | winres = "0.1" 40 | 41 | [patch."https://github.com/zeta0134/rusticnes-core"] 42 | rusticnes-core = { path = "external/rusticnes-core" } 43 | 44 | [patch."https://github.com/zeta0134/rusticnes-ui-common"] 45 | rusticnes-ui-common = { path = "external/rusticnes-ui-common" } 46 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/apu/volume_envelope.rs: -------------------------------------------------------------------------------- 1 | pub struct VolumeEnvelopeState { 2 | // Volume Envelope 3 | pub volume_register: u8, 4 | pub decay: u8, 5 | pub divider: u8, 6 | pub enabled: bool, 7 | pub looping: bool, 8 | pub start_flag: bool, 9 | } 10 | 11 | impl VolumeEnvelopeState { 12 | pub fn new() -> VolumeEnvelopeState { 13 | return VolumeEnvelopeState { 14 | volume_register: 0, 15 | decay: 0, 16 | divider: 0, 17 | enabled: false, 18 | looping: false, 19 | start_flag: false, 20 | } 21 | } 22 | 23 | pub fn current_volume(&self) -> u8 { 24 | if self.enabled { 25 | return self.decay; 26 | } else { 27 | return self.volume_register; 28 | } 29 | } 30 | 31 | pub fn clock(&mut self) { 32 | if self.start_flag { 33 | self.decay = 15; 34 | self.start_flag = false; 35 | self.divider = self.volume_register; 36 | } else { 37 | // Clock the divider 38 | if self.divider == 0 { 39 | self.divider = self.volume_register; 40 | if self.decay > 0 { 41 | self.decay -= 1; 42 | } else { 43 | if self.looping { 44 | self.decay = 15; 45 | } 46 | } 47 | } else { 48 | self.divider = self.divider - 1; 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /external/rusticnes-ui-common/src/test_window.rs: -------------------------------------------------------------------------------- 1 | use application::RuntimeState; 2 | use drawing::Color; 3 | use drawing::SimpleBuffer; 4 | use events::Event; 5 | use panel::Panel; 6 | 7 | pub struct TestWindow { 8 | pub canvas: SimpleBuffer, 9 | pub counter: u8, 10 | pub shown: bool, 11 | } 12 | 13 | impl TestWindow { 14 | pub fn new() -> TestWindow { 15 | return TestWindow { 16 | canvas: SimpleBuffer::new(256, 256), 17 | counter: 0, 18 | shown: false, 19 | }; 20 | } 21 | 22 | fn update(&mut self) { 23 | self.counter = self.counter.wrapping_add(1); 24 | } 25 | 26 | fn draw(&mut self) { 27 | for x in 0 ..= 255 { 28 | for y in 0 ..= 255 { 29 | let r = x; 30 | let g = y; 31 | let b = self.counter.wrapping_add(x ^ y); 32 | self.canvas.put_pixel(x as u32, y as u32, Color::rgb(r, g, b)); 33 | } 34 | } 35 | } 36 | } 37 | 38 | impl Panel for TestWindow { 39 | fn title(&self) -> &str { 40 | return "Hello World!"; 41 | } 42 | 43 | fn shown(&self) -> bool { 44 | return self.shown; 45 | } 46 | 47 | fn handle_event(&mut self, _: &RuntimeState, event: Event) -> Vec { 48 | match event { 49 | Event::Update => {self.update()}, 50 | Event::RequestFrame => {self.draw()}, 51 | Event::ShowTestWindow => {self.shown = true}, 52 | Event::CloseWindow => {self.shown = false}, 53 | _ => {} 54 | } 55 | return Vec::::new(); 56 | } 57 | 58 | fn active_canvas(&self) -> &SimpleBuffer { 59 | return &self.canvas; 60 | } 61 | } -------------------------------------------------------------------------------- /external/rusticnes-core/src/mmc/mapper.rs: -------------------------------------------------------------------------------- 1 | use apu::AudioChannelState; 2 | 3 | #[derive(Copy, Clone, PartialEq)] 4 | pub enum Mirroring { 5 | Horizontal, 6 | Vertical, 7 | OneScreenLower, 8 | OneScreenUpper, 9 | FourScreen, 10 | } 11 | 12 | pub fn mirroring_mode_name(mode: Mirroring) -> &'static str { 13 | match mode { 14 | Mirroring::Horizontal => "Horizontal", 15 | Mirroring::Vertical => "Vertical", 16 | Mirroring::OneScreenLower => "OneScreenLower", 17 | Mirroring::OneScreenUpper => "OneScreenUpper", 18 | Mirroring::FourScreen => "FourScreen" 19 | } 20 | } 21 | 22 | pub trait Mapper: Send { 23 | fn read_cpu(&mut self, address: u16) -> Option {return self.debug_read_cpu(address);} 24 | fn write_cpu(&mut self, address: u16, data: u8); 25 | fn access_ppu(&mut self, _address: u16) {} 26 | fn read_ppu(&mut self, address: u16) -> Option {return self.debug_read_ppu(address);} 27 | fn write_ppu(&mut self, address: u16, data: u8); 28 | fn debug_read_cpu(&self, address: u16) -> Option; 29 | fn debug_read_ppu(&self, address: u16) -> Option; 30 | fn print_debug_status(&self) {} 31 | fn mirroring(&self) -> Mirroring; 32 | fn has_sram(&self) -> bool {return false;} 33 | fn get_sram(&self) -> Vec {return vec![0u8; 0];} 34 | fn load_sram(&mut self, _: Vec) {} 35 | fn irq_flag(&self) -> bool {return false;} 36 | fn clock_cpu(&mut self) {} 37 | fn mix_expansion_audio(&self, nes_sample: f32) -> f32 {return nes_sample;} 38 | fn channels(&self) -> Vec<& dyn AudioChannelState> {return Vec::new();} 39 | fn channels_mut(&mut self) -> Vec<&mut dyn AudioChannelState> {return Vec::new();} 40 | fn record_expansion_audio_output(&mut self, _nes_sample: f32) {} 41 | fn nsf_set_track(&mut self, _track_index: u8) {} 42 | fn nsf_manual_mode(&mut self) {} 43 | fn audio_multiplexing(&mut self, _emulate: bool) {} 44 | fn vrc7_set_patches(&mut self, _patches: &[u8]) {} 45 | } 46 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/apu/audio_channel.rs: -------------------------------------------------------------------------------- 1 | // This is a generic trait, which all audio channels should implement. It is 2 | // primarily meant for use with debug features that display data about the 3 | // audio channels in realtime. 4 | 5 | use super::RingBuffer; 6 | 7 | #[derive(Clone)] 8 | pub enum PlaybackRate { 9 | FundamentalFrequency { frequency: f32 }, 10 | LfsrRate { index: usize, max: usize }, 11 | SampleRate { frequency: f32 }, 12 | } 13 | 14 | #[derive(Clone)] 15 | pub enum Volume { 16 | VolumeIndex { index: usize, max: usize }, 17 | } 18 | 19 | #[derive(Clone)] 20 | pub enum Timbre { 21 | DutyIndex { index: usize, max: usize }, 22 | LsfrMode { index: usize, max: usize }, 23 | PatchIndex { index: usize, max: usize }, 24 | } 25 | 26 | pub trait AudioChannelState { 27 | fn name(&self) -> String; 28 | fn chip(&self) -> String; 29 | fn sample_buffer(&self) -> &RingBuffer; 30 | // TODO: Remove this default implementation, once edge buffer 31 | // is properly supported in all channel types 32 | fn edge_buffer(&self) -> &RingBuffer; 33 | fn min_sample(&self) -> i16 {return i16::MIN;} 34 | fn max_sample(&self) -> i16 {return i16::MAX;} 35 | fn record_current_output(&mut self); 36 | fn muted(&self) -> bool; 37 | fn mute(&mut self); 38 | fn unmute(&mut self); 39 | 40 | fn playing(&self) -> bool { return false; } 41 | fn rate(&self) -> PlaybackRate { return PlaybackRate::SampleRate{frequency: 0.0}; } 42 | fn volume(&self) -> Option {return None} 43 | fn timbre(&self) -> Option {return None} 44 | fn amplitude(&self) -> f32 { 45 | /* pre-mixed volume, allows chips using non-linear mixing to tailor this value. 46 | results should be based on 2A03 pulse, where 1.0 corresponds to 0xF */ 47 | if !self.playing() {return 0.0} 48 | match self.volume() { 49 | Some(Volume::VolumeIndex{index, max}) => {return index as f32 / (max + 1) as f32}, 50 | None => {return 1.0} 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /external/rusticnes-ui-common/src/events.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | #[derive(Clone)] 4 | pub enum StandardControllerButton { 5 | A, 6 | B, 7 | Select, 8 | Start, 9 | DPadUp, 10 | DPadDown, 11 | DPadLeft, 12 | DPadRight, 13 | } 14 | 15 | #[derive(Clone)] 16 | pub enum Event { 17 | ApplyBooleanSetting(String, bool), 18 | ApplyFloatSetting(String, f64), 19 | ApplyIntegerSetting(String, i64), 20 | ApplyStringSetting(String, String), 21 | CloseWindow, 22 | CartridgeLoaded(String), 23 | CartridgeRejected(String, String), 24 | GameToggleOverscan, 25 | GameIncreaseScale, 26 | GameDecreaseScale, 27 | LoadCartridge(String, Rc>,Rc>), 28 | LoadSram(Rc>), 29 | LoadFailed(String), 30 | MouseMove(i32, i32), 31 | MouseClick(i32, i32), 32 | MouseRelease, 33 | MemoryViewerNextPage, 34 | MemoryViewerPreviousPage, 35 | MemoryViewerNextBus, 36 | MuteChannel(String, String), 37 | UnmuteChannel(String, String), 38 | NesNudgeAlignment, 39 | NesNewApuHalfFrame, 40 | NesNewApuQuarterFrame, 41 | NesNewFrame, 42 | NesNewScanline, 43 | NesPauseEmulation, 44 | NesRenderNTSC(usize), 45 | NesResumeEmulation, 46 | NesReset, 47 | NesRunCycle, 48 | NesRunFrame, 49 | NesRunOpcode, 50 | NesRunScanline, 51 | NesToggleEmulation, 52 | RequestFrame, 53 | RequestCartridgeDialog, 54 | RequestSramSave(String), 55 | SaveSram(String, Rc>), 56 | ShowApuWindow, 57 | ShowCpuWindow, 58 | ShowGameWindow, 59 | ShowEventWindow, 60 | ShowMemoryWindow, 61 | ShowPianoRollWindow, 62 | ShowPpuWindow, 63 | ShowTestWindow, 64 | StandardControllerPress(usize, StandardControllerButton), 65 | StandardControllerRelease(usize, StandardControllerButton), 66 | StoreBooleanSetting(String, bool), 67 | StoreFloatSetting(String, f64), 68 | StoreIntegerSetting(String, i64), 69 | StoreStringSetting(String, String), 70 | ToggleBooleanSetting(String), 71 | Update, 72 | } 73 | -------------------------------------------------------------------------------- /external/rusticnes-ui-common/planning.txt: -------------------------------------------------------------------------------- 1 | 2 | InputTranslator (receives, binds, and emits input events) 3 | FrameClockGenerator (manages update and draw timing for emulation state) 4 | 5 | ApplicationState (single source of truth for application) 6 | Contains: 7 | --- NesState (contains controller configuration?) 8 | --- CartridgeState (mapper later?) (is a file loaded, header details, etc etc) 9 | --- RunState (running, paused, breakpoint triggered, etc) 10 | 11 | This object is passed read-only to other components along with events, and can be 12 | passively polled to read the current application state. To modify this state, other 13 | objects send events with the requested changes. 14 | 15 | ApplicationState events - these all *change* state in some way 16 | 17 | NesState 18 | -------- 19 | 20 | NesStep 21 | - NesState is advanced by one cycle 22 | 23 | NesRunScanline 24 | - NesState is advanced by one scanline (341 PPU cycles on NTSC) 25 | 26 | NesRunFrame 27 | - NesState is advanced until the next NMI (whether enabled or not) 28 | 29 | NesPowerCycle 30 | - NesState is completely reinitialized with the loaded file (cached into CartridgeState) 31 | - RunState is unchanged 32 | 33 | NesReset 34 | - NesState.reset() is called 35 | 36 | Note: the various NesState advancing functions will all silently fail while there is no cartridge loaded, 37 | regardless of run state. 38 | 39 | RunState 40 | -------- 41 | 42 | RunStart, RunStop 43 | - RunState is set to running / stopped 44 | 45 | Note: the application generally opens in running mode; this will have no effect until a cartridge is loaded. 46 | 47 | 48 | CartridgeState 49 | -------------- 50 | 51 | LoadCartridge(&[u8]) 52 | - Performs a full cartridge load from the provided raw data 53 | --- Does *not* load the file from disk. That is handled by platform code. 54 | - On success, 55 | --- emits CartridgeLoaded event for UI to process 56 | --- NesState is PowerCycle'd 57 | - On failure: 58 | --- emits a CartridgeFailed event 59 | --- stores the last error message in CartridgeState 60 | --- stores the failing header data in CartridgeState 61 | 62 | ClearCartridge 63 | - CartridgeState is re-initialized, and all data cleared 64 | - NesState is effectively PowerCycle'd 65 | - Because CartridgeState.loaded() will now return false, emulation should stop regardless of RunState. 66 | 67 | Note: this is the ideal place to handle things like FDS disk swapping, but as that's not implemented, 68 | we are ignoring these problems for now. 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/mmc/mirroring.rs: -------------------------------------------------------------------------------- 1 | // Set of helper functions to assist mappers with a few of the most 2 | // common mirroring modes. Less common mirroring modes and more complex 3 | // logic may still be implemented within individual mappers as needed. 4 | const NT_OFFSET: (u16, u16, u16, u16) = (0x000, 0x400, 0x800, 0xC00); 5 | 6 | pub fn horizontal_mirroring(read_address: u16) -> u16 { 7 | let nt_base = read_address & 0xFFF; 8 | let nt_address = read_address & 0x3FF; 9 | match nt_base { 10 | // Nametable 0 (top-left) 11 | 0x000 ..= 0x3FF => nt_address + NT_OFFSET.0, 12 | 0x400 ..= 0x7FF => nt_address + NT_OFFSET.0, 13 | 0x800 ..= 0xBFF => nt_address + NT_OFFSET.1, 14 | 0xC00 ..= 0xFFF => nt_address + NT_OFFSET.1, 15 | _ => return 0, // wat 16 | } 17 | } 18 | 19 | pub fn vertical_mirroring(read_address: u16) -> u16 { 20 | let nt_base = read_address & 0xFFF; 21 | let nt_address = read_address & 0x3FF; 22 | match nt_base { 23 | // Nametable 0 (top-left) 24 | 0x000 ..= 0x3FF => nt_address + NT_OFFSET.0, 25 | 0x400 ..= 0x7FF => nt_address + NT_OFFSET.1, 26 | 0x800 ..= 0xBFF => nt_address + NT_OFFSET.0, 27 | 0xC00 ..= 0xFFF => nt_address + NT_OFFSET.1, 28 | _ => return 0, // wat 29 | } 30 | } 31 | 32 | pub fn one_screen_lower(read_address: u16) -> u16 { 33 | let nt_base = read_address & 0xFFF; 34 | let nt_address = read_address & 0x3FF; 35 | match nt_base { 36 | // Nametable 0 (top-left) 37 | 0x000 ..= 0x3FF => nt_address + NT_OFFSET.0, 38 | 0x400 ..= 0x7FF => nt_address + NT_OFFSET.0, 39 | 0x800 ..= 0xBFF => nt_address + NT_OFFSET.0, 40 | 0xC00 ..= 0xFFF => nt_address + NT_OFFSET.0, 41 | _ => return 0, // wat 42 | } 43 | } 44 | 45 | pub fn one_screen_upper(read_address: u16) -> u16 { 46 | let nt_base = read_address & 0xFFF; 47 | let nt_address = read_address & 0x3FF; 48 | match nt_base { 49 | // Nametable 0 (top-left) 50 | 0x000 ..= 0x3FF => nt_address + NT_OFFSET.1, 51 | 0x400 ..= 0x7FF => nt_address + NT_OFFSET.1, 52 | 0x800 ..= 0xBFF => nt_address + NT_OFFSET.1, 53 | 0xC00 ..= 0xFFF => nt_address + NT_OFFSET.1, 54 | _ => return 0, // wat 55 | } 56 | } 57 | 58 | pub fn four_banks(read_address: u16) -> u16 { 59 | let nt_base = read_address & 0xFFF; 60 | let nt_address = read_address & 0x3FF; 61 | match nt_base { 62 | // Nametable 0 (top-left) 63 | 0x000 ..= 0x3FF => nt_address + NT_OFFSET.0, 64 | 0x400 ..= 0x7FF => nt_address + NT_OFFSET.1, 65 | 0x800 ..= 0xBFF => nt_address + NT_OFFSET.2, 66 | 0xC00 ..= 0xFFF => nt_address + NT_OFFSET.3, 67 | _ => return 0, // wat 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/memoryblock.rs: -------------------------------------------------------------------------------- 1 | /// Represents one contiguous block of memory, typically residing on a single 2 | /// physical chip. Implementations have varying behavior, but provide one 3 | /// consistent guarantee: all memory access will return some value, possibly 4 | /// open bus. This helps the trait to correctly represent missing 5 | /// chips, wrapping behavior, mirroring, bank switching, etc. 6 | #[derive(Clone)] 7 | pub struct MemoryBlock { 8 | bytes: Vec, 9 | readonly: bool, 10 | volatile: bool 11 | } 12 | 13 | #[derive(PartialEq)] 14 | pub enum MemoryType { 15 | Rom, 16 | Ram, 17 | NvRam, 18 | } 19 | 20 | impl MemoryBlock { 21 | pub fn new(data: &[u8], memory_type: MemoryType) -> MemoryBlock { 22 | return MemoryBlock { 23 | bytes: data.to_vec(), 24 | readonly: memory_type == MemoryType::Rom, 25 | volatile: memory_type != MemoryType::NvRam, 26 | } 27 | } 28 | 29 | pub fn len(&self) -> usize { 30 | return self.bytes.len(); 31 | } 32 | 33 | pub fn is_volatile(&self) -> bool { 34 | return self.volatile; 35 | } 36 | 37 | pub fn is_readonly(&self) -> bool { 38 | return self.readonly; 39 | } 40 | 41 | pub fn bounded_read(&self, address: usize) -> Option { 42 | if address >= self.len() { 43 | return None; 44 | } 45 | return Some(self.bytes[address]); 46 | } 47 | 48 | pub fn bounded_write(&mut self, address: usize, data: u8) { 49 | if address >= self.len() || self.readonly { 50 | return; 51 | } 52 | self.bytes[address] = data; 53 | } 54 | 55 | pub fn wrapping_read(&self, address: usize) -> Option { 56 | if self.bytes.len() == 0 { 57 | return None; 58 | } 59 | return Some(self.bytes[address % self.len()]); 60 | } 61 | 62 | pub fn wrapping_write(&mut self, address: usize, data: u8) { 63 | if self.bytes.len() == 0 || self.readonly { 64 | return; 65 | } 66 | let len = self.len(); 67 | self.bytes[address % len] = data; 68 | } 69 | 70 | pub fn banked_read(&self, bank_size: usize, bank_index: usize, offset: usize) -> Option { 71 | let effective_address = (bank_size * bank_index) + (offset % bank_size); 72 | return self.wrapping_read(effective_address); 73 | } 74 | 75 | pub fn banked_write(&mut self, bank_size: usize, bank_index: usize, offset: usize, data: u8) { 76 | let effective_address = (bank_size * bank_index) + (offset % bank_size); 77 | self.wrapping_write(effective_address, data); 78 | } 79 | 80 | pub fn as_vec(&self) -> &Vec { 81 | return &self.bytes; 82 | } 83 | 84 | pub fn as_mut_vec(&mut self) -> &mut Vec { 85 | return &mut self.bytes; 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/mmc/cnrom.rs: -------------------------------------------------------------------------------- 1 | // CnROM, 16-32kb PRG ROM, up to 2048k CHR ROM 2 | // Reference capabilities: https://wiki.nesdev.com/w/index.php/INES_Mapper_003 3 | 4 | use ines::INesCartridge; 5 | use memoryblock::MemoryBlock; 6 | 7 | use mmc::mapper::*; 8 | use mmc::mirroring; 9 | 10 | pub struct CnRom { 11 | pub prg_rom: MemoryBlock, 12 | pub chr: MemoryBlock, 13 | pub mirroring: Mirroring, 14 | pub chr_bank: usize, 15 | pub vram: Vec, 16 | } 17 | 18 | impl CnRom { 19 | pub fn from_ines(ines: INesCartridge) -> Result { 20 | let prg_rom_block = ines.prg_rom_block(); 21 | let chr_block = ines.chr_block()?; 22 | 23 | return Ok(CnRom { 24 | prg_rom: prg_rom_block.clone(), 25 | chr: chr_block.clone(), 26 | mirroring: ines.header.mirroring(), 27 | chr_bank: 0x00, 28 | vram: vec![0u8; 0x1000], 29 | }); 30 | } 31 | } 32 | 33 | impl Mapper for CnRom { 34 | fn print_debug_status(&self) { 35 | println!("======= CnROM ======="); 36 | println!("CHR Bank: {}, Mirroring Mode: {}", self.chr_bank, mirroring_mode_name(self.mirroring)); 37 | println!("===================="); 38 | } 39 | 40 | fn mirroring(&self) -> Mirroring { 41 | return self.mirroring; 42 | } 43 | 44 | fn debug_read_cpu(&self, address: u16) -> Option { 45 | match address { 46 | 0x8000 ..= 0xFFFF => {self.prg_rom.wrapping_read((address - 0x8000) as usize)}, 47 | _ => None 48 | } 49 | } 50 | 51 | fn write_cpu(&mut self, address: u16, data: u8) { 52 | match address { 53 | 0x8000 ..= 0xFFFF => { 54 | self.chr_bank = data as usize; 55 | } 56 | _ => {} 57 | } 58 | } 59 | 60 | fn debug_read_ppu(&self, address: u16) -> Option { 61 | match address { 62 | 0x0000 ..= 0x1FFF => {self.chr.banked_read(0x2000, self.chr_bank, address as usize)}, 63 | 0x2000 ..= 0x3FFF => match self.mirroring { 64 | Mirroring::Horizontal => Some(self.vram[mirroring::horizontal_mirroring(address) as usize]), 65 | Mirroring::Vertical => Some(self.vram[mirroring::vertical_mirroring(address) as usize]), 66 | _ => None 67 | }, 68 | _ => None 69 | } 70 | } 71 | 72 | fn write_ppu(&mut self, address: u16, data: u8) { 73 | match address { 74 | 0x0000 ..= 0x1FFF => {self.chr.banked_write(0x2000, self.chr_bank, address as usize, data)}, 75 | 0x2000 ..= 0x3FFF => match self.mirroring { 76 | Mirroring::Horizontal => self.vram[mirroring::horizontal_mirroring(address) as usize] = data, 77 | Mirroring::Vertical => self.vram[mirroring::vertical_mirroring(address) as usize] = data, 78 | _ => {} 79 | }, 80 | _ => {} 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/cartridge.rs: -------------------------------------------------------------------------------- 1 | use mmc::mapper::*; 2 | use mmc::action53::Action53; 3 | use mmc::axrom::AxRom; 4 | use mmc::bnrom::BnRom; 5 | use mmc::cnrom::CnRom; 6 | use mmc::fme7::Fme7; 7 | use mmc::gxrom::GxRom; 8 | use mmc::ines31::INes31; 9 | use mmc::mmc1::Mmc1; 10 | use mmc::mmc3::Mmc3; 11 | use mmc::mmc5::Mmc5; 12 | use mmc::n163::Namco163; 13 | use mmc::nrom::Nrom; 14 | use mmc::nsf::NsfMapper; 15 | use mmc::pxrom::PxRom; 16 | use mmc::uxrom::UxRom; 17 | use mmc::vrc6::Vrc6; 18 | use mmc::vrc7::Vrc7; 19 | 20 | use ines::INesCartridge; 21 | use nsf::NsfFile; 22 | 23 | use std::io::Read; 24 | 25 | fn mapper_from_ines(ines: INesCartridge) -> Result, String> { 26 | let mapper_number = ines.header.mapper_number(); 27 | 28 | let mapper: Box = match mapper_number { 29 | 0 => Box::new(Nrom::from_ines(ines)?), 30 | 1 => Box::new(Mmc1::from_ines(ines)?), 31 | 2 => Box::new(UxRom::from_ines(ines)?), 32 | 3 => Box::new(CnRom::from_ines(ines)?), 33 | 4 => Box::new(Mmc3::from_ines(ines)?), 34 | 5 => Box::new(Mmc5::from_ines(ines)?), 35 | 7 => Box::new(AxRom::from_ines(ines)?), 36 | 9 => Box::new(PxRom::from_ines(ines)?), 37 | 19 => Box::new(Namco163::from_ines(ines)?), 38 | 24 => Box::new(Vrc6::from_ines(ines)?), 39 | 26 => Box::new(Vrc6::from_ines(ines)?), 40 | 28 => Box::new(Action53::from_ines(ines)?), 41 | 31 => Box::new(INes31::from_ines(ines)?), 42 | 34 => Box::new(BnRom::from_ines(ines)?), 43 | 66 => Box::new(GxRom::from_ines(ines)?), 44 | 69 => Box::new(Fme7::from_ines(ines)?), 45 | 85 => Box::new(Vrc7::from_ines(ines)?), 46 | _ => { 47 | return Err(format!("Unsupported iNES mapper: {}", ines.header.mapper_number())); 48 | } 49 | }; 50 | 51 | println!("Successfully loaded mapper: {}", mapper_number); 52 | 53 | return Ok(mapper); 54 | } 55 | 56 | pub fn mapper_from_reader(file_reader: &mut dyn Read) -> Result, String> { 57 | let mut entire_file = Vec::new(); 58 | match file_reader.read_to_end(&mut entire_file) { 59 | Ok(_) => {/* proceed normally */}, 60 | Err(e) => { 61 | return Err(format!("Failed to read any data at all, giving up.{}\n", e)); 62 | } 63 | } 64 | 65 | let mut errors = String::new(); 66 | match INesCartridge::from_reader(&mut entire_file.as_slice()) { 67 | Ok(ines) => {return mapper_from_ines(ines);}, 68 | Err(e) => {errors += format!("ines: {}\n", e).as_str()} 69 | } 70 | 71 | match NsfFile::from_reader(&mut entire_file.as_slice()) { 72 | Ok(nsf) => {return Ok(Box::new(NsfMapper::from_nsf(nsf)?));}, 73 | Err(e) => {errors += format!("nsf: {}\n", e).as_str()} 74 | } 75 | 76 | return Err(format!("Unable to open file as any known type, giving up.\n{}", errors)); 77 | } 78 | 79 | pub fn mapper_from_file(file_data: &[u8]) -> Result, String> { 80 | let mut file_reader = file_data; 81 | return mapper_from_reader(&mut file_reader); 82 | } -------------------------------------------------------------------------------- /external/rusticnes-core/src/mmc/nrom.rs: -------------------------------------------------------------------------------- 1 | // A very simple Mapper with no esoteric features or bank switching. 2 | // Reference capabilities: https://wiki.nesdev.com/w/index.php/NROM 3 | 4 | use ines::INesCartridge; 5 | use memoryblock::MemoryBlock; 6 | 7 | use mmc::mapper::*; 8 | use mmc::mirroring; 9 | 10 | pub struct Nrom { 11 | prg_rom: MemoryBlock, 12 | prg_ram: MemoryBlock, 13 | chr: MemoryBlock, 14 | 15 | mirroring: Mirroring, 16 | vram: Vec, 17 | } 18 | 19 | impl Nrom { 20 | pub fn from_ines(ines: INesCartridge) -> Result { 21 | let prg_rom_block = ines.prg_rom_block(); 22 | let prg_ram_block = ines.prg_ram_block()?; 23 | let chr_block = ines.chr_block()?; 24 | 25 | return Ok(Nrom { 26 | prg_rom: prg_rom_block.clone(), 27 | prg_ram: prg_ram_block.clone(), 28 | chr: chr_block.clone(), 29 | mirroring: ines.header.mirroring(), 30 | vram: vec![0u8; 0x1000], 31 | }); 32 | } 33 | } 34 | 35 | impl Mapper for Nrom { 36 | fn print_debug_status(&self) { 37 | println!("======= NROM ======="); 38 | println!("Mirroring Mode: {}", mirroring_mode_name(self.mirroring)); 39 | println!("===================="); 40 | } 41 | 42 | fn mirroring(&self) -> Mirroring { 43 | return self.mirroring; 44 | } 45 | 46 | fn debug_read_cpu(&self, address: u16) -> Option { 47 | match address { 48 | 0x6000 ..= 0x7FFF => {self.prg_ram.wrapping_read((address - 0x6000) as usize)}, 49 | 0x8000 ..= 0xFFFF => {self.prg_rom.wrapping_read((address - 0x8000) as usize)}, 50 | _ => None 51 | } 52 | } 53 | 54 | fn write_cpu(&mut self, address: u16, data: u8) { 55 | match address { 56 | 0x6000 ..= 0x7FFF => {self.prg_ram.wrapping_write((address - 0x6000) as usize, data);}, 57 | _ => {} 58 | } 59 | } 60 | 61 | fn debug_read_ppu(&self, address: u16) -> Option { 62 | match address { 63 | 0x0000 ..= 0x1FFF => return self.chr.wrapping_read(address as usize), 64 | 0x2000 ..= 0x3FFF => return match self.mirroring { 65 | Mirroring::Horizontal => Some(self.vram[mirroring::horizontal_mirroring(address) as usize]), 66 | Mirroring::Vertical => Some(self.vram[mirroring::vertical_mirroring(address) as usize]), 67 | _ => None 68 | }, 69 | _ => return None 70 | } 71 | } 72 | 73 | fn write_ppu(&mut self, address: u16, data: u8) { 74 | match address { 75 | 0x0000 ..= 0x1FFF => {self.chr.wrapping_write(address as usize, data);}, 76 | 0x2000 ..= 0x3FFF => match self.mirroring { 77 | Mirroring::Horizontal => self.vram[mirroring::horizontal_mirroring(address) as usize] = data, 78 | Mirroring::Vertical => self.vram[mirroring::vertical_mirroring(address) as usize] = data, 79 | _ => {} 80 | }, 81 | _ => {} 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/mmc/uxrom.rs: -------------------------------------------------------------------------------- 1 | // UxROM, simple bank switchable PRG ROM with the last page fixed 2 | // Reference capabilities: https://wiki.nesdev.com/w/index.php/UxROM 3 | 4 | use ines::INesCartridge; 5 | use memoryblock::MemoryBlock; 6 | 7 | use mmc::mapper::*; 8 | use mmc::mirroring; 9 | 10 | pub struct UxRom { 11 | pub prg_rom: MemoryBlock, 12 | pub chr: MemoryBlock, 13 | pub mirroring: Mirroring, 14 | pub prg_bank: usize, 15 | pub vram: Vec, 16 | } 17 | 18 | impl UxRom { 19 | pub fn from_ines(ines: INesCartridge) -> Result { 20 | let prg_rom_block = ines.prg_rom_block(); 21 | let chr_block = ines.chr_block()?; 22 | 23 | return Ok(UxRom { 24 | prg_rom: prg_rom_block.clone(), 25 | chr: chr_block.clone(), 26 | mirroring: ines.header.mirroring(), 27 | prg_bank: 0x00, 28 | vram: vec![0u8; 0x1000], 29 | }) 30 | } 31 | } 32 | 33 | impl Mapper for UxRom { 34 | fn print_debug_status(&self) { 35 | println!("======= UxROM ======="); 36 | println!("PRG Bank: {}, ", self.prg_bank); 37 | println!("Mirroring Mode: {}", mirroring_mode_name(self.mirroring)); 38 | println!("===================="); 39 | } 40 | 41 | fn mirroring(&self) -> Mirroring { 42 | return self.mirroring; 43 | } 44 | 45 | fn debug_read_cpu(&self, address: u16) -> Option { 46 | match address { 47 | 0x8000 ..= 0xBFFF => self.prg_rom.banked_read(0x4000, self.prg_bank, address as usize - 0x8000), 48 | 0xC000 ..= 0xFFFF => self.prg_rom.banked_read(0x4000, 0xFF, address as usize - 0xC000), 49 | _ => None 50 | } 51 | } 52 | 53 | fn write_cpu(&mut self, address: u16, data: u8) { 54 | match address { 55 | 0x8000 ..= 0xFFFF => { 56 | self.prg_bank = data as usize; 57 | } 58 | _ => {} 59 | } 60 | } 61 | 62 | fn debug_read_ppu(&self, address: u16) -> Option { 63 | match address { 64 | 0x0000 ..= 0x1FFF => self.chr.wrapping_read(address as usize), 65 | 0x2000 ..= 0x3FFF => match self.mirroring { 66 | Mirroring::Horizontal => Some(self.vram[mirroring::horizontal_mirroring(address) as usize]), 67 | Mirroring::Vertical => Some(self.vram[mirroring::vertical_mirroring(address) as usize]), 68 | _ => None 69 | }, 70 | _ => None 71 | } 72 | } 73 | 74 | fn write_ppu(&mut self, address: u16, data: u8) { 75 | match address { 76 | 0x0000 ..= 0x1FFF => self.chr.wrapping_write(address as usize, data), 77 | 0x2000 ..= 0x3FFF => match self.mirroring { 78 | Mirroring::Horizontal => self.vram[mirroring::horizontal_mirroring(address) as usize] = data, 79 | Mirroring::Vertical => self.vram[mirroring::vertical_mirroring(address) as usize] = data, 80 | _ => {} 81 | }, 82 | _ => {} 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/mmc/bnrom.rs: -------------------------------------------------------------------------------- 1 | // BNROM, bank switchable PRG ROM, 8kb CHR RAM, solder-pad fixed horizontal or vertical mirroring. 2 | // Essentially an AxROM variant, though I'm choosing to keep all numbered mapper implementations 3 | // dependency free for my own sanity. 4 | // Reference capabilities: https://wiki.nesdev.com/w/index.php/BNROM 5 | 6 | use ines::INesCartridge; 7 | use memoryblock::MemoryBlock; 8 | 9 | use mmc::mapper::*; 10 | use mmc::mirroring; 11 | 12 | pub struct BnRom { 13 | pub prg_rom: MemoryBlock, 14 | pub chr: MemoryBlock, 15 | pub mirroring: Mirroring, 16 | pub prg_bank: usize, 17 | pub vram: Vec, 18 | } 19 | 20 | impl BnRom { 21 | pub fn from_ines(ines: INesCartridge) -> Result { 22 | let prg_rom_block = ines.prg_rom_block(); 23 | let chr_block = ines.chr_block()?; 24 | 25 | return Ok(BnRom { 26 | prg_rom: prg_rom_block.clone(), 27 | chr: chr_block.clone(), 28 | mirroring: ines.header.mirroring(), 29 | prg_bank: 0x07, 30 | vram: vec![0u8; 0x1000], 31 | }); 32 | } 33 | } 34 | 35 | impl Mapper for BnRom { 36 | fn mirroring(&self) -> Mirroring { 37 | return self.mirroring; 38 | } 39 | 40 | fn print_debug_status(&self) { 41 | println!("======= BNROM ======="); 42 | println!("PRG Bank: {}, Mirroring Mode: {}", self.prg_bank, mirroring_mode_name(self.mirroring)); 43 | println!("===================="); 44 | } 45 | 46 | fn debug_read_cpu(&self, address: u16) -> Option { 47 | match address { 48 | 0x8000 ..= 0xFFFF => {self.prg_rom.banked_read(0x8000, self.prg_bank, (address - 0x8000) as usize)}, 49 | _ => None 50 | } 51 | } 52 | 53 | fn write_cpu(&mut self, address: u16, data: u8) { 54 | match address { 55 | 0x8000 ..= 0xFFFF => {self.prg_bank = data as usize;} 56 | _ => {} 57 | } 58 | } 59 | 60 | fn debug_read_ppu(&self, address: u16) -> Option { 61 | match address { 62 | 0x0000 ..= 0x1FFF => self.chr.wrapping_read(address as usize), 63 | 0x2000 ..= 0x3FFF => match self.mirroring { 64 | Mirroring::Horizontal => Some(self.vram[mirroring::horizontal_mirroring(address) as usize]), 65 | Mirroring::Vertical => Some(self.vram[mirroring::vertical_mirroring(address) as usize]), 66 | _ => None 67 | }, 68 | _ => None 69 | } 70 | } 71 | 72 | fn write_ppu(&mut self, address: u16, data: u8) { 73 | match address { 74 | 0x0000 ..= 0x1FFF => {self.chr.wrapping_write(address as usize, data);}, 75 | 0x2000 ..= 0x3FFF => match self.mirroring { 76 | Mirroring::Horizontal => self.vram[mirroring::horizontal_mirroring(address) as usize] = data, 77 | Mirroring::Vertical => self.vram[mirroring::vertical_mirroring(address) as usize] = data, 78 | _ => {} 79 | }, 80 | _ => {} 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/gui/slint/module-metadata.slint: -------------------------------------------------------------------------------- 1 | export struct ModuleMetadata { 2 | title: string, 3 | artist: string, 4 | copyright: string, 5 | 6 | driver: string, 7 | 8 | extended-metadata: bool, 9 | loop-detection: bool, 10 | extended-durations: [int], 11 | chips: [string], 12 | tracks: [string] 13 | } 14 | 15 | export component ModuleMetadataView { 16 | in property module-metadata: { 17 | title: "", 18 | artist: "", 19 | copyright: "", 20 | driver: "", 21 | extended-metadata: false, 22 | loop-detection: false, 23 | extended-durations: [], 24 | chips: [], 25 | tracks: [] 26 | }; 27 | 28 | VerticalLayout { 29 | alignment: center; 30 | spacing: 4px; 31 | 32 | Text { 33 | text: module-metadata.title; 34 | horizontal-alignment: center; 35 | } 36 | Text { 37 | text: module-metadata.artist; 38 | horizontal-alignment: center; 39 | } 40 | Text { 41 | text: module-metadata.copyright; 42 | horizontal-alignment: center; 43 | } 44 | HorizontalLayout { 45 | alignment: center; 46 | spacing: 16px; 47 | 48 | Text { 49 | text: "NSFe/NSF2 metadata"; 50 | color: module-metadata.extended-metadata 51 | ? green 52 | : red; 53 | } 54 | Text { 55 | text: "Loop detection"; 56 | color: module-metadata.loop-detection 57 | ? green 58 | : red; 59 | } 60 | Text { 61 | text: "NSFe/NSF2 duration"; 62 | color: module-metadata.extended-durations.length > 0 63 | ? green 64 | : red; 65 | } 66 | } 67 | HorizontalLayout { 68 | alignment: center; 69 | spacing: 12px; 70 | 71 | for chip in module-metadata.chips : Rectangle { 72 | background: chip == "2A03" ? #dddddd : 73 | chip == "FDS" ? #0066ff : 74 | chip == "N163" ? #ff0000 : 75 | chip == "MMC5" ? #2eb82e : 76 | chip == "VRC6" ? #ffcc00 : 77 | chip == "VRC7" ? #ff9800 : 78 | chip == "S5B" ? #ff33cc : 79 | transparent; 80 | 81 | width: 36px; 82 | height: 18px; 83 | border-radius: 2px; 84 | 85 | Text { 86 | horizontal-alignment: center; 87 | vertical-alignment: center; 88 | text: chip; 89 | 90 | color: chip == "2A03" ? black : 91 | chip == "VRC6" ? black : 92 | white; 93 | } 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/mmc/gxrom.rs: -------------------------------------------------------------------------------- 1 | // GxRom, simple bank switchable 32kb PRG ROM and 8k CHR ROM 2 | // Reference capabilities: https://wiki.nesdev.com/w/index.php/GxROM 3 | 4 | use ines::INesCartridge; 5 | use memoryblock::MemoryBlock; 6 | 7 | use mmc::mapper::*; 8 | use mmc::mirroring; 9 | 10 | pub struct GxRom { 11 | pub prg_rom: MemoryBlock, 12 | pub chr: MemoryBlock, 13 | pub mirroring: Mirroring, 14 | pub prg_bank: usize, 15 | pub chr_bank: usize, 16 | pub vram: Vec, 17 | } 18 | 19 | impl GxRom { 20 | pub fn from_ines(ines: INesCartridge) -> Result { 21 | let prg_rom_block = ines.prg_rom_block(); 22 | let chr_block = ines.chr_block()?; 23 | 24 | return Ok(GxRom { 25 | prg_rom: prg_rom_block.clone(), 26 | chr: chr_block.clone(), 27 | mirroring: ines.header.mirroring(), 28 | prg_bank: 0x00, 29 | chr_bank: 0x00, 30 | vram: vec![0u8; 0x1000], 31 | }); 32 | } 33 | } 34 | 35 | impl Mapper for GxRom { 36 | fn print_debug_status(&self) { 37 | println!("======= GxROM ======="); 38 | println!("PRG Bank: {}, CHR Bank: {}, Mirroring Mode: {}", self.prg_bank, self.chr_bank, mirroring_mode_name(self.mirroring)); 39 | println!("===================="); 40 | } 41 | 42 | fn mirroring(&self) -> Mirroring { 43 | return self.mirroring; 44 | } 45 | 46 | fn debug_read_cpu(&self, address: u16) -> Option { 47 | match address { 48 | 0x8000 ..= 0xFFFF => {self.prg_rom.banked_read(0x8000, self.prg_bank, (address - 0x8000) as usize)}, 49 | _ => None 50 | } 51 | } 52 | 53 | fn write_cpu(&mut self, address: u16, data: u8) { 54 | match address { 55 | 0x8000 ..= 0xFFFF => { 56 | self.prg_bank = ((data & 0b0011_0000) >> 4) as usize; 57 | self.chr_bank = (data & 0b0000_0011) as usize; 58 | } 59 | _ => {} 60 | } 61 | } 62 | 63 | fn debug_read_ppu(&self, address: u16) -> Option { 64 | match address { 65 | 0x0000 ..= 0x1FFF => self.chr.banked_read(0x2000, self.chr_bank, address as usize), 66 | 0x2000 ..= 0x3FFF => match self.mirroring { 67 | Mirroring::Horizontal => Some(self.vram[mirroring::horizontal_mirroring(address) as usize]), 68 | Mirroring::Vertical => Some(self.vram[mirroring::vertical_mirroring(address) as usize]), 69 | _ => None 70 | }, 71 | _ => None 72 | } 73 | } 74 | 75 | fn write_ppu(&mut self, address: u16, data: u8) { 76 | match address { 77 | 0x0000 ..= 0x1FFF => self.chr.banked_write(0x2000, self.chr_bank, address as usize, data), 78 | 0x2000 ..= 0x3FFF => match self.mirroring { 79 | Mirroring::Horizontal => self.vram[mirroring::horizontal_mirroring(address) as usize] = data, 80 | Mirroring::Vertical => self.vram[mirroring::vertical_mirroring(address) as usize] = data, 81 | _ => {} 82 | }, 83 | _ => {} 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/mmc/axrom.rs: -------------------------------------------------------------------------------- 1 | // AxROM, bank switchable PRG ROM, 8kb CHR RAM, basic single-screen mirroring. 2 | // Reference capabilities: https://wiki.nesdev.com/w/index.php/AxROM 3 | 4 | use ines::INesCartridge; 5 | use memoryblock::MemoryBlock; 6 | 7 | use mmc::mapper::*; 8 | use mmc::mirroring; 9 | 10 | pub struct AxRom { 11 | pub prg_rom: MemoryBlock, 12 | pub chr: MemoryBlock, 13 | pub mirroring: Mirroring, 14 | pub prg_bank: usize, 15 | pub vram: Vec, 16 | } 17 | 18 | impl AxRom { 19 | pub fn from_ines(ines: INesCartridge) -> Result { 20 | let prg_rom_block = ines.prg_rom_block(); 21 | let chr_block = ines.chr_block()?; 22 | 23 | return Ok(AxRom { 24 | prg_rom: prg_rom_block.clone(), 25 | chr: chr_block.clone(), 26 | mirroring: Mirroring::OneScreenUpper, 27 | prg_bank: 0x07, 28 | vram: vec![0u8; 0x1000], 29 | }); 30 | } 31 | } 32 | 33 | impl Mapper for AxRom { 34 | fn mirroring(&self) -> Mirroring { 35 | return self.mirroring; 36 | } 37 | 38 | fn print_debug_status(&self) { 39 | println!("======= AxROM ======="); 40 | println!("PRG Bank: {}, Mirroring Mode: {}", self.prg_bank, mirroring_mode_name(self.mirroring)); 41 | println!("===================="); 42 | } 43 | 44 | fn debug_read_cpu(&self, address: u16) -> Option { 45 | match address { 46 | 0x8000 ..= 0xFFFF => {self.prg_rom.banked_read(0x8000, self.prg_bank, (address - 0x8000) as usize)}, 47 | _ => None 48 | } 49 | } 50 | 51 | fn write_cpu(&mut self, address: u16, data: u8) { 52 | match address { 53 | 0x8000 ..= 0xFFFF => { 54 | self.prg_bank = (data & 0x07) as usize; 55 | if data & 0x10 == 0 { 56 | self.mirroring = Mirroring::OneScreenLower; 57 | } else { 58 | self.mirroring = Mirroring::OneScreenUpper; 59 | } 60 | } 61 | _ => {} 62 | } 63 | } 64 | 65 | fn debug_read_ppu(&self, address: u16) -> Option { 66 | match address { 67 | 0x0000 ..= 0x1FFF => self.chr.wrapping_read(address as usize), 68 | 0x2000 ..= 0x3FFF => match self.mirroring { 69 | Mirroring::OneScreenLower => Some(self.vram[mirroring::one_screen_lower(address) as usize]), 70 | Mirroring::OneScreenUpper => Some(self.vram[mirroring::one_screen_upper(address) as usize]), 71 | _ => None 72 | }, 73 | _ => None 74 | } 75 | } 76 | 77 | fn write_ppu(&mut self, address: u16, data: u8) { 78 | match address { 79 | 0x0000 ..= 0x1FFF => self.chr.wrapping_write(address as usize, data), 80 | 0x2000 ..= 0x3FFF => match self.mirroring { 81 | Mirroring::OneScreenLower => self.vram[mirroring::one_screen_lower(address) as usize] = data, 82 | Mirroring::OneScreenUpper => self.vram[mirroring::one_screen_upper(address) as usize] = data, 83 | _ => {} 84 | }, 85 | _ => {} 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/gui/slint/toolbar-button.slint: -------------------------------------------------------------------------------- 1 | export component ToolbarButton { 2 | in property icon; 3 | in property text: ""; 4 | in property tooltip: ""; 5 | in property enabled: true; 6 | in property destructive: false; 7 | 8 | callback clicked(); 9 | 10 | min-height: 32px; 11 | min-width: 32px; 12 | 13 | i-tooltip := Rectangle { 14 | y: root.height + 4px; 15 | width: i-tooltip-text.width + 20px; 16 | opacity: 0; 17 | 18 | background: #2c2c2cff; 19 | border-radius: 3px; 20 | drop-shadow-color: black; 21 | drop-shadow-blur: 4px; 22 | drop-shadow-offset-y: 2px; 23 | 24 | i-tooltip-text := Text { 25 | text: root.tooltip; 26 | horizontal-alignment: center; 27 | } 28 | } 29 | 30 | i-base := Rectangle { 31 | border-radius: 4px; 32 | background: transparent; 33 | animate background { 34 | duration: 100ms; 35 | easing: ease-in-out; 36 | } 37 | 38 | i-touch-area := TouchArea { 39 | clicked => { 40 | if (root.enabled) { 41 | root.clicked(); 42 | } 43 | } 44 | 45 | HorizontalLayout { 46 | alignment: center; 47 | 48 | Rectangle { 49 | width: 6px; 50 | } 51 | VerticalLayout { 52 | alignment: center; 53 | 54 | i-icon := Image { 55 | width: 20px; 56 | source: root.icon; 57 | colorize: white; 58 | animate colorize { 59 | duration: 100ms; 60 | easing: ease-in-out; 61 | } 62 | } 63 | } 64 | if root.text != "" : Rectangle { 65 | width: 6px; 66 | } 67 | i-text := Text { 68 | vertical-alignment: center; 69 | text: root.text; 70 | color: white; 71 | animate color { 72 | duration: 100ms; 73 | easing: ease-in-out; 74 | } 75 | } 76 | Rectangle { 77 | width: 6px; 78 | } 79 | } 80 | } 81 | } 82 | 83 | states [ 84 | disabled when !root.enabled: { 85 | i-base.background: transparent; 86 | i-icon.colorize: #FFFFFF87; 87 | i-text.color: #FFFFFF87; 88 | i-tooltip.opacity: 0; 89 | } 90 | clicked when i-touch-area.pressed: { 91 | i-base.background: #FFFFFF0F; 92 | i-icon.colorize: root.destructive ? #bc2f32 : #60cdff; 93 | i-text.color: root.destructive ? #bc2f32 : #60cdff; 94 | i-tooltip.opacity: root.tooltip != "" ? 1 : 0; 95 | } 96 | hovered when i-touch-area.has-hover: { 97 | i-base.background: #FFFFFF1F; 98 | i-icon.colorize: root.destructive ? #bc2f32 : #60cdff; 99 | i-text.color: root.destructive ? #bc2f32 : #60cdff; 100 | i-tooltip.opacity: root.tooltip != "" ? 1 : 0; 101 | 102 | in { 103 | animate i-tooltip.opacity { 104 | duration: 200ms; 105 | delay: 300ms; 106 | easing: ease-in-out; 107 | } 108 | } 109 | out { 110 | animate i-tooltip.opacity { 111 | duration: 150ms; 112 | easing: ease-in-out; 113 | } 114 | } 115 | } 116 | ] 117 | } -------------------------------------------------------------------------------- /external/rusticnes-core/README.md: -------------------------------------------------------------------------------- 1 | # RusticNES-Core 2 | 3 | This is an NES emulator written in the Rust programming language. I began this project because I wanted to teach myself Rust, and having already written [another emulator](https://github.com/zeta0134/LuaGB), I figured this was as good a way to introduce myself to the language as any. 4 | 5 | The emulator is split up into the Core library (this repository) and platform specific shells which depend on this library. rusticnes-core contains the entire emulator with as few external dependencies as possible (presently just Rust's standard FileIO functions) so that it remains portable. All platform specific code is the responsibility of the shell. 6 | 7 | If you're looking to compile and run a working copy of the emulator for PCs, you want [RusticNES-SDL](https://github.com/zeta0134/rusticnes-sdl), which is the reference implementation. I've tested this on Windows and Arch Linux, and it should run on Mac, and any other platform that [rust-sdl2](https://github.com/Rust-SDL2/rust-sdl2) supports. I may update this README with usage instructions for the core library after the project stabilizes a bit. At the moment the project is in constant flux and lacks what I'd call a stable API, so I'll instead refer you to [RusticNES-SDL](https://github.com/zeta0134/rusticnes-sdl) for the reference implementation. 8 | 9 | I'm striving for cycle accuracy with the emulator. While it works and runs many games, it presently falls short of this goal. I am presently most focused on getting the base emulator to run properly, and pass various [accuracy tests](http://tasvideos.org/EmulatorResources/NESAccuracyTests.html). Mapper support should be easy to add as I go. Here is the current state of the major systems: 10 | 11 | ## 6502 CPU 12 | 13 | - All instructions, including unofficial instructions, NOPs, and STPs 14 | - Mostly cycle accurate, which should include additional reads / writes on dummy cycles. 15 | - Missing proper read delay implementation, needed for DMC DMA read delay, and proper interaction between DMC and OAM DMA during simultaneous operation. 16 | 17 | ## APU 18 | 19 | - Feature complete as far as I can tell. Pulse, Triangle, Noise, and DMC are all working properly. 20 | - DMC wait delay is not implemented. 21 | - Audio is not mixed properly, relative channel volumes are therefore sometimes quite incorrect. It's close enough that things sound okay unless you know what to listen for. 22 | - No interpolation or filtering, which can make especially high frequencies sound a bit off. The APU is producing the correct output, but the subsequent clamping to 44.1 KHz introduces artifacts. 23 | - Triangle channel intentionally does not emulate extremely high frequencies, to avoid artifacts in the handful of games that use this to "silence" the channel 24 | 25 | ## PPU 26 | 27 | - Memory mapping, including cartridge mapper support, is all implemented and should be working. 28 | - Nametable mirroring modes appear to work correctly, and are controlled by the mapper. 29 | - Cycle timing should be very close to accurate. Tricky games like Battletoads appear to run correctly, though there may still be bugs here and there. 30 | - Sprite overflow is implemented correctly. The sprite overflow bug is not, so games relying on the behavior of the sprite overflow flag will encounter accuracy problems relative to real hardware. 31 | 32 | ## Input 33 | 34 | - A single Standard Controller plugged into port 1 is implemented. 35 | - Multiple controllers and additional peripheral support (Light Zapper, Track and Field Mat, Knitting Machine, etc) is planned, but not implemented. 36 | 37 | ## Mappers 38 | 39 | - Currently supported: AxROM, CnROM, GxROM, MMC1, MMC3, NROM, PxROM, UxROM 40 | - Currently unsupported: Everything else. 41 | - Behavior seems mostly correct, but accuracy is not guaranteed. 42 | - Some of blarggs mapper tests do not pass, especially those involving timing, which may be due to missing RDY line implementation 43 | - FDS and non-NTSC NES features (PAL, Vs System, etc) are entirely unsupported. 44 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/mmc/ines31.rs: -------------------------------------------------------------------------------- 1 | // iNES Mapper 031 represents a mapper created to facilitate cartridge compilations 2 | // of NSF music. It implements a common subset of the features used by NSFs. 3 | // Reference capabilities: https://wiki.nesdev.com/w/index.php/INES_Mapper_031 4 | 5 | use ines::INesCartridge; 6 | use memoryblock::MemoryBlock; 7 | 8 | use mmc::mapper::*; 9 | use mmc::mirroring; 10 | 11 | pub struct INes31 { 12 | pub prg_rom: MemoryBlock, 13 | pub chr: MemoryBlock, 14 | pub mirroring: Mirroring, 15 | pub vram: Vec, 16 | pub prg_banks: Vec, 17 | } 18 | 19 | impl INes31 { 20 | pub fn from_ines(ines: INesCartridge) -> Result { 21 | let prg_rom_block = ines.prg_rom_block(); 22 | let chr_block = ines.chr_block()?; 23 | 24 | return Ok(INes31 { 25 | prg_rom: prg_rom_block.clone(), 26 | chr: chr_block.clone(), 27 | mirroring: ines.header.mirroring(), 28 | vram: vec![0u8; 0x1000], 29 | prg_banks: vec![255usize; 8], 30 | }) 31 | } 32 | } 33 | 34 | impl Mapper for INes31 { 35 | fn print_debug_status(&self) { 36 | println!("======= iNes 31 ======="); 37 | println!("Mirroring Mode: {}", mirroring_mode_name(self.mirroring)); 38 | println!("===================="); 39 | } 40 | 41 | fn mirroring(&self) -> Mirroring { 42 | return self.mirroring; 43 | } 44 | 45 | fn debug_read_cpu(&self, address: u16) -> Option { 46 | match address { 47 | 0x8000 ..= 0x8FFF => self.prg_rom.banked_read(0x1000, self.prg_banks[0], (address as usize) - 0x8000), 48 | 0x9000 ..= 0x9FFF => self.prg_rom.banked_read(0x1000, self.prg_banks[1], (address as usize) - 0x9000), 49 | 0xA000 ..= 0xAFFF => self.prg_rom.banked_read(0x1000, self.prg_banks[2], (address as usize) - 0xA000), 50 | 0xB000 ..= 0xBFFF => self.prg_rom.banked_read(0x1000, self.prg_banks[3], (address as usize) - 0xB000), 51 | 0xC000 ..= 0xCFFF => self.prg_rom.banked_read(0x1000, self.prg_banks[4], (address as usize) - 0xC000), 52 | 0xD000 ..= 0xDFFF => self.prg_rom.banked_read(0x1000, self.prg_banks[5], (address as usize) - 0xD000), 53 | 0xE000 ..= 0xEFFF => self.prg_rom.banked_read(0x1000, self.prg_banks[6], (address as usize) - 0xE000), 54 | 0xF000 ..= 0xFFFF => self.prg_rom.banked_read(0x1000, self.prg_banks[7], (address as usize) - 0xF000), 55 | _ => None 56 | } 57 | } 58 | 59 | fn write_cpu(&mut self, address: u16, data: u8) { 60 | match address { 61 | 0x5FF8 => {self.prg_banks[0] = data as usize}, 62 | 0x5FF9 => {self.prg_banks[1] = data as usize}, 63 | 0x5FFA => {self.prg_banks[2] = data as usize}, 64 | 0x5FFB => {self.prg_banks[3] = data as usize}, 65 | 0x5FFC => {self.prg_banks[4] = data as usize}, 66 | 0x5FFD => {self.prg_banks[5] = data as usize}, 67 | 0x5FFE => {self.prg_banks[6] = data as usize}, 68 | 0x5FFF => {self.prg_banks[7] = data as usize}, 69 | _ => {} 70 | } 71 | } 72 | 73 | fn debug_read_ppu(&self, address: u16) -> Option { 74 | match address { 75 | 0x0000 ..= 0x1FFF => self.chr.wrapping_read(address as usize), 76 | 0x2000 ..= 0x3FFF => match self.mirroring { 77 | Mirroring::Horizontal => Some(self.vram[mirroring::horizontal_mirroring(address) as usize]), 78 | Mirroring::Vertical => Some(self.vram[mirroring::vertical_mirroring(address) as usize]), 79 | _ => None 80 | }, 81 | _ => None 82 | } 83 | } 84 | 85 | fn write_ppu(&mut self, address: u16, data: u8) { 86 | match address { 87 | 0x0000 ..= 0x1FFF => self.chr.wrapping_write(address as usize, data), 88 | 0x2000 ..= 0x3FFF => match self.mirroring { 89 | Mirroring::Horizontal => self.vram[mirroring::horizontal_mirroring(address) as usize] = data, 90 | Mirroring::Vertical => self.vram[mirroring::vertical_mirroring(address) as usize] = data, 91 | _ => {} 92 | }, 93 | _ => {} 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/video_builder/backgrounds/video_bg.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::sync::{Arc, Mutex}; 3 | use std::thread::{self, JoinHandle}; 4 | use std::time; 5 | use ffmpeg_next::{format, software::scaling, util::frame, media::Type, codec}; 6 | use super::VideoBackground; 7 | 8 | fn spawn_decoding_thread(frames: Arc>>, path: &str, w: u32, h: u32) -> JoinHandle<()> { 9 | let path = path.to_string(); 10 | thread::spawn(move || { 11 | println!("[MTVBG] Decoding thread started"); 12 | 13 | let mut in_ctx = format::input(&path).unwrap(); 14 | let in_stream = in_ctx 15 | .streams() 16 | .best(Type::Video) 17 | .ok_or(ffmpeg_next::Error::StreamNotFound) 18 | .unwrap(); 19 | 20 | let stream_idx = in_stream.index(); 21 | 22 | let v_codec_ctx = codec::Context::from_parameters(in_stream.parameters()) 23 | .unwrap(); 24 | let mut v_decoder = v_codec_ctx 25 | .decoder() 26 | .video() 27 | .unwrap(); 28 | 29 | let mut sws_ctx = scaling::Context::get( 30 | v_decoder.format(), v_decoder.width(), v_decoder.height(), 31 | format::Pixel::RGBA, w, h, 32 | scaling::Flags::FAST_BILINEAR 33 | ).unwrap(); 34 | 35 | println!("[MTVBG] Starting to decode..."); 36 | 37 | let mut decoded_frame = frame::Video::empty(); 38 | let mut rgba_frame = frame::Video::empty(); 39 | 40 | for (stream, packet) in in_ctx.packets() { 41 | if stream.index() == stream_idx { 42 | v_decoder.send_packet(&packet) 43 | .unwrap(); 44 | 45 | while v_decoder.receive_frame(&mut decoded_frame).is_ok() { 46 | sws_ctx.run(&decoded_frame, &mut rgba_frame) 47 | .unwrap(); 48 | 49 | { 50 | let mut guarded_frames = frames.lock().unwrap(); 51 | guarded_frames.push_back(rgba_frame.clone()); 52 | if guarded_frames.len() <= 30 { 53 | continue; 54 | } 55 | } 56 | 57 | // Pause decoding if we have too many queued frames and wait for decoder 58 | // to consume some before resuming so we don't gobble up RAM 59 | loop { 60 | { 61 | let guarded_frames = frames.lock().unwrap(); 62 | if guarded_frames.len() <= 10 { 63 | break; 64 | } 65 | } 66 | thread::sleep(time::Duration::from_millis(100)); 67 | } 68 | } 69 | } 70 | } 71 | 72 | println!("[MTVBG] Decoding thread stopping"); 73 | }) 74 | } 75 | 76 | pub struct MTVideoBackground { 77 | w: u32, 78 | h: u32, 79 | handle: JoinHandle<()>, 80 | frames: Arc>> 81 | } 82 | 83 | impl MTVideoBackground { 84 | pub fn open(path: &str, w: u32, h: u32) -> Option { 85 | if format::input(&path).is_err() { 86 | return None; 87 | } 88 | 89 | let frames: Arc>> = Arc::new(Mutex::new(VecDeque::new())); 90 | let handle = spawn_decoding_thread(frames.clone(), path, w, h); 91 | 92 | thread::sleep(time::Duration::from_millis(50)); 93 | 94 | Some(Self { 95 | w, 96 | h, 97 | handle, 98 | frames 99 | }) 100 | } 101 | } 102 | 103 | impl VideoBackground for MTVideoBackground { 104 | fn next_frame(&mut self) -> frame::Video { 105 | loop { 106 | let mut guarded_frames = self.frames.lock().unwrap(); 107 | if let Some(frame) = guarded_frames.pop_front() { 108 | break frame; 109 | } else { 110 | drop(guarded_frames); 111 | if self.handle.is_finished() { 112 | let blank_frame = frame::Video::new(format::Pixel::RGBA, self.w, self.h); 113 | break blank_frame 114 | } 115 | thread::sleep(time::Duration::from_millis(10)); 116 | } 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /src/gui/render_thread.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Error, anyhow}; 2 | use std::thread; 3 | use std::sync::mpsc; 4 | use std::time::{Duration, Instant}; 5 | use crate::emulator::SongPosition; 6 | use crate::renderer::Renderer; 7 | use crate::renderer::options::RendererOptions; 8 | 9 | #[derive(Clone)] 10 | pub enum RenderThreadRequest { 11 | StartRender(RendererOptions), 12 | CancelRender, 13 | Terminate 14 | } 15 | 16 | #[derive(Clone)] 17 | pub struct RenderProgressInfo { 18 | pub frame: u64, 19 | pub average_fps: u32, 20 | pub encoded_size: usize, 21 | pub expected_duration_frames: Option, 22 | pub expected_duration: Option, 23 | pub eta_duration: Option, 24 | pub elapsed_duration: Duration, 25 | pub encoded_duration: Duration, 26 | pub song_position: Option, 27 | pub loop_count: Option 28 | } 29 | 30 | pub enum RenderThreadMessage { 31 | Error(Error), 32 | RenderStarting, 33 | RenderProgress(RenderProgressInfo), 34 | RenderComplete, 35 | RenderCancelled 36 | } 37 | 38 | macro_rules! rt_unwrap { 39 | ($v: expr, $cb: tt) => { 40 | match $v { 41 | Ok(v) => v, 42 | Err(e) => { 43 | $cb(RenderThreadMessage::Error(e)); 44 | return; 45 | } 46 | } 47 | }; 48 | } 49 | 50 | pub fn render_thread(cb: F) -> (thread::JoinHandle<()>, mpsc::Sender) 51 | where 52 | F: Fn(RenderThreadMessage) + Send + 'static 53 | { 54 | let (tx, rx) = mpsc::channel(); 55 | let handle = thread::spawn(move || { 56 | println!("Renderer thread started"); 57 | 58 | 'main: loop { 59 | let options = match rx.recv().unwrap() { 60 | RenderThreadRequest::StartRender(o) => o, 61 | RenderThreadRequest::CancelRender => { 62 | cb(RenderThreadMessage::Error(anyhow!("No active render to cancel."))); 63 | continue; 64 | } 65 | RenderThreadRequest::Terminate => break 'main 66 | }; 67 | cb(RenderThreadMessage::RenderStarting); 68 | 69 | let mut renderer = rt_unwrap!(Renderer::new(options), cb); 70 | rt_unwrap!(renderer.start_encoding(), cb); 71 | 72 | let mut last_progress_timestamp = Instant::now(); 73 | // Janky way to force an update 74 | last_progress_timestamp.checked_sub(Duration::from_secs(2)); 75 | 76 | 'render: loop { 77 | match rx.try_recv() { 78 | Ok(RenderThreadRequest::StartRender(_)) => { 79 | cb(RenderThreadMessage::Error(anyhow!("Cannot start a render while one is already being processed."))); 80 | }, 81 | Ok(RenderThreadRequest::CancelRender) => { 82 | cb(RenderThreadMessage::RenderCancelled); 83 | break 'render 84 | }, 85 | Ok(RenderThreadRequest::Terminate) => break 'main, 86 | _ => () 87 | } 88 | if !(rt_unwrap!(renderer.step(), cb)) { 89 | break; 90 | } 91 | 92 | if last_progress_timestamp.elapsed().as_secs_f64() >= 0.5 { 93 | last_progress_timestamp = Instant::now(); 94 | 95 | let progress_info = RenderProgressInfo { 96 | frame: renderer.current_frame(), 97 | average_fps: renderer.average_fps(), 98 | encoded_size: renderer.encoded_size(), 99 | expected_duration_frames: renderer.expected_duration_frames(), 100 | expected_duration: renderer.expected_duration(), 101 | eta_duration: renderer.eta_duration(), 102 | elapsed_duration: renderer.elapsed(), 103 | encoded_duration: renderer.encoded_duration(), 104 | song_position: renderer.song_position(), 105 | loop_count: renderer.loop_count(), 106 | }; 107 | 108 | cb(RenderThreadMessage::RenderProgress(progress_info)); 109 | } 110 | } 111 | 112 | rt_unwrap!(renderer.finish_encoding(), cb); 113 | cb(RenderThreadMessage::RenderComplete); 114 | } 115 | }); 116 | (handle, tx) 117 | } 118 | -------------------------------------------------------------------------------- /src/emulator/m3u_searcher.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, Context}; 2 | use std::collections::HashMap; 3 | use std::fs; 4 | use std::path::Path; 5 | use std::str::FromStr; 6 | use std::time::Duration; 7 | use glob::{glob_with, MatchOptions}; 8 | use encoding_rs::{CoderResult, WINDOWS_1252, SHIFT_JIS}; 9 | 10 | fn read_m3u_file>(m3u_path: P) -> Result { 11 | let data = fs::read(m3u_path)?; 12 | let mut result = String::with_capacity(data.len() * 4); 13 | 14 | let mut cp1252_decoder = WINDOWS_1252.new_decoder(); 15 | let (coder_result, _bytes_read, did_replacements) = cp1252_decoder.decode_to_string(&data, &mut result, true); 16 | if coder_result != CoderResult::OutputFull && !did_replacements { 17 | return Ok(result); 18 | } 19 | 20 | result.clear(); 21 | let mut shift_jis_decoder = SHIFT_JIS.new_decoder(); 22 | let (coder_result, _bytes_read, did_replacements) = shift_jis_decoder.decode_to_string(&data, &mut result, true); 23 | if coder_result != CoderResult::OutputFull && !did_replacements { 24 | return Ok(result); 25 | } 26 | 27 | String::from_utf8(data).context("M3U string is not valid CP-1252, Shift-JIS, or UTF-8") 28 | } 29 | 30 | pub fn search>(nsf_path: P) -> Result)>> { 31 | let mut result: HashMap)> = HashMap::new(); 32 | 33 | let nsf_filename = nsf_path.as_ref().file_name().unwrap().to_str().unwrap().to_string(); 34 | 35 | let mut nsf_dir = nsf_path 36 | .as_ref() 37 | .parent() 38 | .context("Invalid path")? 39 | .canonicalize()?; 40 | nsf_dir.push("*.m3u"); 41 | 42 | let mut nsf_dir = nsf_dir.to_str().unwrap().to_string(); 43 | if nsf_dir.starts_with("\\\\?\\") { 44 | let _ = nsf_dir.drain(0..4); 45 | } 46 | 47 | let options = MatchOptions { 48 | case_sensitive: false, 49 | require_literal_separator: false, 50 | require_literal_leading_dot: false, 51 | }; 52 | for glob_entry in glob_with(&nsf_dir, options)? { 53 | let m3u_path = glob_entry?; 54 | println!("Discovered M3U file: {}", m3u_path.file_name().unwrap().to_str().unwrap()); 55 | 56 | for line in read_m3u_file(m3u_path)?.lines() { 57 | if line.is_empty() || line.starts_with('#') { 58 | continue; 59 | } 60 | 61 | let mut components: Vec = Vec::new(); 62 | for raw_component in line.split(',') { 63 | if !components.is_empty() && components.last().unwrap().replace("\\\\", "").ends_with('\\') { 64 | let _ = components.last_mut().unwrap().pop(); 65 | components.last_mut().unwrap().push(','); 66 | components.last_mut().unwrap().push_str(&raw_component.replace("\\\\", "\\")); 67 | } else { 68 | components.push(raw_component.replace("\\\\", "\\")); 69 | } 70 | } 71 | let mut component_iter = components.iter().cloned(); 72 | 73 | let filename = component_iter.next().unwrap_or("".to_string()); 74 | if filename.to_lowercase() != format!("{}::nsf", nsf_filename.to_lowercase()) { 75 | continue; 76 | } 77 | 78 | let index = u8::from_str(&component_iter.next().unwrap_or("".to_string())) 79 | .context("M3U track index is missing/invalid")? 80 | .saturating_sub(1); 81 | 82 | let mut track_title = component_iter.next().unwrap_or("".to_string()); 83 | if track_title.is_empty() { 84 | continue; 85 | } else if track_title.chars().count() > 60 { 86 | let new_len = track_title.char_indices().nth(57).map(|(i, _)| i).unwrap_or(track_title.len()); 87 | track_title.truncate(new_len); 88 | track_title.push_str("..."); 89 | } 90 | 91 | let duration_seconds = component_iter.next().unwrap_or("".to_string()) 92 | .split(':') 93 | .fold(0.0_f64, |acc, cur| { 94 | let duration_component = f64::from_str(cur).unwrap_or_default(); 95 | (acc * 60.0) + duration_component 96 | }); 97 | let duration = if duration_seconds > 0.0 { 98 | Some(Duration::from_secs_f64(duration_seconds)) 99 | } else { 100 | None 101 | }; 102 | 103 | result.insert(index, (track_title, duration)); 104 | } 105 | } 106 | 107 | Ok(result) 108 | } -------------------------------------------------------------------------------- /src/video_builder/ffmpeg_hacks.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, anyhow}; 2 | use std::ffi::{CStr, CString}; 3 | use ffmpeg_next::{codec, Codec, Error, format, StreamMut}; 4 | use ffmpeg_sys_next::{av_get_sample_fmt, avcodec_alloc_context3, avcodec_parameters_from_context, avcodec_parameters_to_context, av_version_info}; 5 | 6 | pub fn ffmpeg_version() -> &'static str { 7 | // ffmpeg-next does not provide a way to get the FFmpeg version number. It does provide the 8 | // libav version number, but that follows a wildly different scheme and isn't very helpful. 9 | // Safety: The return value of av_version_info() is guaranteed by the API to be a valid C 10 | // string with a static lifetime. 11 | unsafe { 12 | CStr::from_ptr(av_version_info()).to_str().unwrap() 13 | } 14 | } 15 | 16 | pub fn ffmpeg_create_context(codec: Codec, parameters: codec::Parameters) -> Result { 17 | // ffmpeg-next does not provide a way to pass a codec to avcodec_alloc_context3, which 18 | // is necessary for initializing certain contexts (e.g. mp4/libx264). 19 | // Safety: The return value of avcodec_alloc_context3() is checked to ensure that the allocation 20 | // succeeded. 21 | // Safety: The allocated context is wrapped in a safe abstraction, which handles freeing the 22 | // associated resources later. 23 | // Safety: The value of avcodec_parameters_to_context is checked to ensure errors are handled. 24 | unsafe { 25 | let context = avcodec_alloc_context3(codec.as_ptr()); 26 | if context.is_null() { 27 | return Err(anyhow!("FFMPEG error: avcodec_alloc_context3() failed")); 28 | } 29 | 30 | let mut context = codec::Context::wrap(context, None); 31 | match avcodec_parameters_to_context(context.as_mut_ptr(), parameters.as_ptr()) { 32 | 0 => Ok(context), 33 | e => Err(anyhow!(Error::from(e))) 34 | } 35 | } 36 | } 37 | 38 | pub fn ffmpeg_copy_context_params(stream: &mut StreamMut, context: &codec::Context) -> Result<()> { 39 | // This context copy is required to fully initialize some codecs (e.g. AAC). ffmpeg-next does not 40 | // provide a safe abstraction so it must be done here. 41 | // Safety: The value of avcodec_parameters_from_context is checked to ensure errors are handled. 42 | // Safety: All mutable pointer dereferences are done strictly on initialized memory since they 43 | // come from a mutable reference to a safe abstraction. 44 | unsafe { 45 | match avcodec_parameters_from_context((*stream.as_mut_ptr()).codecpar, context.as_ptr()) { 46 | 0 => Ok(()), 47 | e => Err(anyhow!(Error::from(e))) 48 | } 49 | } 50 | } 51 | 52 | pub fn ffmpeg_copy_codec_params(stream: &mut StreamMut, context: &codec::Context, codec: &Codec) -> Result<()> { 53 | // This augmented context copy is required to initialize some codecs. ffmpeg-next does not 54 | // provide a safe abstraction so it must be done here. 55 | // Safety: All mutable pointer dereferences are done strictly on initialized memory since they 56 | // come from a mutable reference to a safe abstraction. 57 | unsafe { 58 | ffmpeg_copy_context_params(stream, context)?; 59 | (*(*stream.as_mut_ptr()).codecpar).codec_id = codec.id().into(); 60 | (*(*stream.as_mut_ptr()).codecpar).codec_type = codec.medium().into(); 61 | } 62 | Ok(()) 63 | } 64 | 65 | pub fn ffmpeg_sample_format_from_string(value: &str) -> format::Sample { 66 | // This is provided by ffmpeg-next, but only for `&'static str`, presumably due to 67 | // some confusion over the `const char*` in the method signature? 68 | unsafe { 69 | let value = CString::new(value).unwrap(); 70 | 71 | format::Sample::from(av_get_sample_fmt(value.as_ptr())) 72 | } 73 | } 74 | 75 | pub fn ffmpeg_get_audio_context_frame_size(context: &codec::Context, variable_frame_size: usize) -> usize { 76 | let frame_size = unsafe { (*context.as_ptr()).frame_size as usize }; 77 | let ctx_codec = context.codec().unwrap(); 78 | debug_assert!(ctx_codec.is_audio()); 79 | debug_assert!(ctx_codec.is_encoder()); 80 | 81 | if ctx_codec.capabilities().contains(codec::Capabilities::VARIABLE_FRAME_SIZE) || frame_size == 0 { 82 | variable_frame_size 83 | } else { 84 | frame_size 85 | } 86 | } 87 | 88 | pub fn ffmpeg_context_bytes_written(context: &format::context::Output) -> usize { 89 | #[cfg(not(feature = "ffmpeg_6_0"))] 90 | let bytes_written = unsafe { (*(*context.as_ptr()).pb).written }; 91 | #[cfg(feature = "ffmpeg_6_0")] 92 | let bytes_written = unsafe { (*(*context.as_ptr()).pb).bytes_written }; 93 | std::cmp::max(bytes_written, 0) as usize 94 | } -------------------------------------------------------------------------------- /src/gui/slint/color-picker.slint: -------------------------------------------------------------------------------- 1 | import { Button, VerticalBox, LineEdit } from "std-widgets.slint"; 2 | 3 | export global ColorUtils { 4 | pure callback color-to-hex(color) -> string; 5 | pure callback hex-to-color(string) -> color; 6 | pure callback color-components(color) -> [int]; 7 | } 8 | 9 | component ColorSlider inherits Rectangle { 10 | in-out property maximum: 255; 11 | in-out property minimum: 0; 12 | in-out property value; 13 | 14 | in property left-color: #000; 15 | in property right-color: #fff; 16 | 17 | callback moved(); 18 | 19 | min-height: 24px; 20 | min-width: 100px; 21 | horizontal-stretch: 1; 22 | vertical-stretch: 0; 23 | 24 | border-radius: root.height/2; 25 | background: @linear-gradient(90deg, left-color 0%, right-color 100%); 26 | border-width: 1px; 27 | border-color: #999; 28 | 29 | handle := Rectangle { 30 | width: self.height; 31 | height: parent.height; 32 | border-width: 3px; 33 | border-radius: self.height / 2; 34 | background: touch.pressed ? #f8f: touch.has-hover ? #66f : #0000ff; 35 | border-color: self.background.darker(50%); 36 | x: (root.width - handle.width) * (root.value - root.minimum)/(root.maximum - root.minimum); 37 | } 38 | touch := TouchArea { 39 | property pressed-value; 40 | pointer-event(event) => { 41 | if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) { 42 | self.pressed-value = root.value; 43 | } 44 | } 45 | moved => { 46 | if (self.enabled && self.pressed) { 47 | root.value = max(root.minimum, min(root.maximum, 48 | self.pressed-value + (touch.mouse-x - touch.pressed-x) * (root.maximum - root.minimum) / (root.width - handle.width))); 49 | root.moved(); 50 | } 51 | } 52 | } 53 | } 54 | 55 | export component ColorPicker { 56 | in-out property r: 255; 57 | in-out property g: 255; 58 | in-out property b: 255; 59 | out property value: Colors.rgb(r, g, b); 60 | 61 | callback changed(int, int, int); 62 | 63 | function update-value() { 64 | value = Colors.rgb(r, g, b); 65 | changed(r, g, b); 66 | } 67 | 68 | function update-rgb() { 69 | r = ColorUtils.color-components(value)[0]; 70 | g = ColorUtils.color-components(value)[1]; 71 | b = ColorUtils.color-components(value)[2]; 72 | changed(r, g, b); 73 | } 74 | 75 | VerticalBox { 76 | alignment: start; 77 | HorizontalLayout { 78 | alignment: space-between; 79 | Rectangle { 80 | background: rgb(root.r, root.g, root.b); 81 | width: 50%; 82 | border-radius: 4px; 83 | border-width: 1px; 84 | border-color: #999; 85 | } 86 | LineEdit { 87 | text: ColorUtils.color-to-hex(value); 88 | accepted(hex) => { 89 | root.value = ColorUtils.hex-to-color(hex); 90 | update-rgb(); 91 | } 92 | } 93 | } 94 | HorizontalLayout { 95 | alignment: stretch; 96 | Text { 97 | vertical-alignment: center; 98 | text: root.r; 99 | width: 30px; 100 | } 101 | ColorSlider { 102 | value <=> root.r; 103 | left-color: rgb(0, root.g, root.b); 104 | right-color: rgb(255, root.g, root.b); 105 | moved => { root.update-value(); } 106 | } 107 | } 108 | HorizontalLayout { 109 | alignment: stretch; 110 | Text { 111 | vertical-alignment: center; 112 | text: root.g; 113 | width: 30px; 114 | } 115 | ColorSlider { 116 | value <=> root.g; 117 | left-color: rgb(root.r, 0, root.b); 118 | right-color: rgb(root.r, 255, root.b); 119 | moved => { root.update-value(); } 120 | } 121 | } 122 | HorizontalLayout { 123 | alignment: stretch; 124 | Text { 125 | vertical-alignment: center; 126 | text: root.b; 127 | width: 30px; 128 | } 129 | ColorSlider { 130 | value <=> root.b; 131 | left-color: rgb(root.r, root.g, 0); 132 | right-color: rgb(root.r, root.g, 255); 133 | moved => { root.update-value(); } 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /external/rusticnes-ui-common/src/cpu_window.rs: -------------------------------------------------------------------------------- 1 | use application::RuntimeState; 2 | use drawing; 3 | use drawing::Color; 4 | use drawing::Font; 5 | use drawing::SimpleBuffer; 6 | use events::Event; 7 | use panel::Panel; 8 | 9 | use rusticnes_core::nes::NesState; 10 | use rusticnes_core::opcode_info::disassemble_instruction; 11 | use rusticnes_core::memory; 12 | 13 | pub struct CpuWindow { 14 | pub canvas: SimpleBuffer, 15 | pub font: Font, 16 | pub shown: bool, 17 | } 18 | 19 | impl CpuWindow { 20 | pub fn new() -> CpuWindow { 21 | let font = Font::from_raw(include_bytes!("assets/8x8_font.png"), 8); 22 | 23 | return CpuWindow { 24 | canvas: SimpleBuffer::new(256, 300), 25 | font: font, 26 | shown: false, 27 | }; 28 | } 29 | 30 | pub fn draw_registers(&mut self, nes: &NesState, x: u32, y: u32) { 31 | drawing::text(&mut self.canvas, &self.font, x, y, 32 | "===== Registers =====", 33 | Color::rgb(192, 192, 192)); 34 | drawing::text(&mut self.canvas, &self.font, x, y + 8, 35 | &format!("A: 0x{:02X}", nes.registers.a), Color::rgb(255, 255, 128)); 36 | drawing::text(&mut self.canvas, &self.font, x, y + 16, 37 | &format!("X: 0x{:02X}", nes.registers.x), Color::rgb(160, 160, 160)); 38 | drawing::text(&mut self.canvas, &self.font, x, y + 24, 39 | &format!("Y: 0x{:02X}", nes.registers.y), Color::rgb(224, 224, 224)); 40 | 41 | drawing::text(&mut self.canvas, &self.font, x + 64, y + 8, 42 | &format!("PC: 0x{:04X}", nes.registers.pc), Color::rgb(255, 128, 128)); 43 | drawing::text(&mut self.canvas, &self.font, x + 64, y + 16, 44 | &format!("S: {:02X}", nes.registers.s), Color::rgb(128, 128, 255)); 45 | drawing::text(&mut self.canvas, &self.font, x + 64, y + 16, 46 | " 0x10 ", Color::rgb(128, 128, 255)); 47 | drawing::text(&mut self.canvas, &self.font, x + 64, y + 24, 48 | "F: nvdzic", Color::rgba(128, 192, 128, 64)); 49 | drawing::text(&mut self.canvas, &self.font, x + 64, y + 24, 50 | &format!("F: {}{}{}{}{}{}", 51 | if nes.registers.flags.negative {"n"} else {" "}, 52 | if nes.registers.flags.overflow {"v"} else {" "}, 53 | if nes.registers.flags.decimal {"d"} else {" "}, 54 | if nes.registers.flags.zero {"z"} else {" "}, 55 | if nes.registers.flags.interrupts_disabled {"i"} else {" "}, 56 | if nes.registers.flags.carry {"c"} else {" "}), 57 | Color::rgb(128, 192, 128)); 58 | } 59 | 60 | pub fn draw_disassembly(&mut self, nes: &NesState, x: u32, y: u32) { 61 | drawing::text(&mut self.canvas, &self.font, x, y, 62 | "===== Disassembly =====", Color::rgb(255, 255, 255)); 63 | 64 | let mut data_bytes_to_skip = 0; 65 | for i in 0 .. 30 { 66 | let pc = nes.registers.pc + (i as u16); 67 | let opcode = memory::debug_read_byte(nes, pc); 68 | let data1 = memory::debug_read_byte(nes, pc + 1); 69 | let data2 = memory::debug_read_byte(nes, pc + 2); 70 | let (instruction, data_bytes) = disassemble_instruction(opcode, data1, data2); 71 | let mut text_color = Color::rgb(255, 255, 255); 72 | 73 | if data_bytes_to_skip > 0 { 74 | text_color = Color::rgb(64, 64, 64); 75 | data_bytes_to_skip -= 1; 76 | } else { 77 | data_bytes_to_skip = data_bytes; 78 | } 79 | 80 | drawing::text(&mut self.canvas, &self.font, x, y + 16 + (i as u32 * 8), 81 | &format!("0x{:04X} - 0x{:02X}: {}", pc, opcode, instruction), 82 | text_color); 83 | } 84 | } 85 | 86 | fn draw(&mut self, nes: &NesState) { 87 | // Clear! 88 | let width = self.canvas.width; 89 | let height = self.canvas.height; 90 | drawing::rect(&mut self.canvas, 0, 0, width, height, Color::rgb(0,0,0)); 91 | self.draw_registers(nes, 0, 0); 92 | self.draw_disassembly(nes, 0, 40); 93 | } 94 | } 95 | 96 | impl Panel for CpuWindow { 97 | fn title(&self) -> &str { 98 | return "CPU Status"; 99 | } 100 | 101 | fn shown(&self) -> bool { 102 | return self.shown; 103 | } 104 | 105 | fn handle_event(&mut self, runtime: &RuntimeState, event: Event) -> Vec { 106 | match event { 107 | Event::RequestFrame => {self.draw(&runtime.nes)}, 108 | Event::ShowCpuWindow => {self.shown = true}, 109 | Event::CloseWindow => {self.shown = false}, 110 | _ => {} 111 | } 112 | return Vec::::new(); 113 | } 114 | 115 | fn active_canvas(&self) -> &SimpleBuffer { 116 | return &self.canvas; 117 | } 118 | 119 | fn scale_factor(&self) -> u32 { 120 | return 2; 121 | } 122 | } -------------------------------------------------------------------------------- /external/rusticnes-core/src/opcode_info.rs: -------------------------------------------------------------------------------- 1 | pub fn alu_block(addressing_mode_index: u8, opcode_index: u8) -> (&'static str, &'static str) { 2 | let addressing_mode = match addressing_mode_index { 3 | // Zero Page Mode 4 | 0b000 => "(d, x)", 5 | 0b001 => "d", 6 | 0b010 => "#i", 7 | 0b011 => "a", 8 | 0b100 => "(d), y", 9 | 0b101 => "d, x", 10 | 0b110 => "a, y", 11 | 0b111 => "a, x", 12 | 13 | // Not implemented yet 14 | _ => "???", 15 | }; 16 | 17 | let opcode_name = match opcode_index { 18 | 0b000 => "ORA", 19 | 0b001 => "AND", 20 | 0b010 => "EOR", 21 | 0b011 => "ADC", 22 | 0b100 => "STA", 23 | 0b101 => "LDA", 24 | 0b110 => "CMP", 25 | 0b111 => "SBC", 26 | _ => "???" 27 | }; 28 | 29 | return (opcode_name, addressing_mode); 30 | } 31 | 32 | pub fn rmw_block(opcode: u8, addressing_mode_index: u8, opcode_index: u8) -> (&'static str, &'static str) { 33 | // First, handle some block 10 opcodes that break the mold 34 | return match opcode { 35 | // Assorted NOPs 36 | 0x82 | 0xC2 | 0xE2 => ("NOP", "#i"), 37 | 0x1A | 0x3A | 0x5A | 0x7A | 0xDA | 0xEA | 0xFA => ("NOP", ""), 38 | // Certain opcodes may be vital to your success. THESE opcodes are not. 39 | 0x02 | 0x22 | 0x42 | 0x62 | 0x12 | 0x32 | 0x52 | 0x72 | 0x92 | 0xB2 | 0xD2 | 0xF2 => ("STP", ""), 40 | 0xA2 => ("LDX", "#i"), 41 | 0x8A => ("TXA", ""), 42 | 0xAA => ("TAX", ""), 43 | 0xCA => ("DEX", ""), 44 | 0x9A => ("TXS", ""), 45 | 0xBA => ("TSX", ""), 46 | 0x96 => ("STX", "d, y"), 47 | 0xB6 => ("LDX", "d, y"), 48 | 0xBE => ("LDX", "a, y"), 49 | _ => { 50 | let addressing_mode = match addressing_mode_index { 51 | // Zero Page Mode 52 | 0b001 => "d", 53 | 0b010 => "", // Note: masked for 8A, AA, CA, and EA above 54 | 0b011 => "a", 55 | 0b101 => "d, x", 56 | 0b111 => "a, x", 57 | 58 | // Not implemented yet 59 | _ => "???", 60 | }; 61 | 62 | let opcode_name = match opcode_index { 63 | 0b000 => "ASL", 64 | 0b001 => "ROL", 65 | 0b010 => "LSR", 66 | 0b011 => "ROR", 67 | 0b100 => "STX", 68 | 0b101 => "LDX", 69 | 0b110 => "DEC", 70 | 0b111 => "INC", 71 | _ => "???" 72 | }; 73 | 74 | return (opcode_name, addressing_mode); 75 | } 76 | }; 77 | } 78 | 79 | pub fn control_block(opcode: u8) -> (&'static str, &'static str) { 80 | // Everything is pretty irregular, so we'll just match the whole opcode 81 | return match opcode { 82 | 0x10 => ("BPL", ""), 83 | 0x30 => ("BMI", ""), 84 | 0x50 => ("BVC", ""), 85 | 0x70 => ("BVS", ""), 86 | 0x90 => ("BCC", ""), 87 | 0xB0 => ("BCS", ""), 88 | 0xD0 => ("BNE", ""), 89 | 0xF0 => ("BEQ", ""), 90 | 91 | 0x00 => ("BRK", ""), 92 | 0x80 => ("NOP", "#i"), 93 | 94 | // Opcodes with similar addressing modes 95 | 0xA0 => ("LDY", "#i"), 96 | 0xC0 => ("CPY", "#i"), 97 | 0xE0 => ("CPX", "#i"), 98 | 0x24 => ("BIT", "d"), 99 | 0x84 => ("STY", "d"), 100 | 0xA4 => ("LDY", "d"), 101 | 0xC4 => ("CPY", "d"), 102 | 0xE4 => ("CPX", "d"), 103 | 0x2C => ("BIT", "a"), 104 | 0x8C => ("STY", "a"), 105 | 0xAC => ("LDY", "a"), 106 | 0xCC => ("CPY", "a"), 107 | 0xEC => ("CPX", "a"), 108 | 0x94 => ("STY", "d, x"), 109 | 0xB4 => ("LDY", "d, x"), 110 | 0xBC => ("LDY", "a, x"), 111 | 112 | 0x4C => ("JMP", "a"), 113 | 0x6C => ("JMP", "(a)"), 114 | 115 | 0x08 => ("PHP", ""), 116 | 0x28 => ("PLP", ""), 117 | 0x48 => ("PHA", ""), 118 | 0x68 => ("PLA", ""), 119 | 120 | 0x20 => ("JSR", ""), 121 | 0x40 => ("RTI", ""), 122 | 0x60 => ("RTS", ""), 123 | 124 | 0x88 => ("DEY", ""), 125 | 0xA8 => ("TAY", ""), 126 | 0xC8 => ("INY", ""), 127 | 0xE8 => ("INX", ""), 128 | 129 | 0x18 => ("CLC", ""), 130 | 0x58 => ("CLI", ""), 131 | 0xB8 => ("CLV", ""), 132 | 0xD8 => ("CLD", ""), 133 | 0x38 => ("SEC", ""), 134 | 0x78 => ("SEI", ""), 135 | 0xF8 => ("SED", ""), 136 | 0x98 => ("TYA", ""), 137 | 138 | _ => ("???", "???") 139 | }; 140 | } 141 | 142 | pub fn addressing_bytes(addressing_mode: &str) -> u8 { 143 | return match addressing_mode { 144 | "#i" | "d" | "(d, x)" | "(d), y" | "d, x" => 1, 145 | "a" | "a, x" | "a, y" | "(a)" => 2, 146 | _ => 0 147 | } 148 | } 149 | 150 | pub fn disassemble_instruction(opcode: u8, _: u8, _: u8) -> (String, u8) { 151 | let logic_block = opcode & 0b0000_0011; 152 | let addressing_mode_index = (opcode & 0b0001_1100) >> 2; 153 | let opcode_index = (opcode & 0b1110_0000) >> 5; 154 | 155 | let (opcode_name, addressing_mode) = match logic_block { 156 | 0b00 => control_block(opcode), 157 | 0b01 => alu_block(addressing_mode_index, opcode_index), 158 | 0b10 => rmw_block(opcode, addressing_mode_index, opcode_index), 159 | _ => ("???", "") 160 | }; 161 | 162 | let instruction = format!("{} {}", opcode_name, addressing_mode); 163 | let data_bytes = addressing_bytes(addressing_mode); 164 | return (instruction, data_bytes); 165 | } -------------------------------------------------------------------------------- /src/emulator/nsf.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::str; 3 | use crate::emulator::nsfeparser::{nsfe_to_nsf2, NsfeMetadata}; 4 | use encoding_rs::{CoderResult, SHIFT_JIS}; 5 | 6 | pub fn find_subsequence(haystack: &[T], needle: &[T]) -> Option 7 | where for<'a> &'a [T]: PartialEq 8 | { 9 | haystack.windows(needle.len()).position(|window| window == needle) 10 | } 11 | 12 | #[derive(Copy, Clone, PartialEq)] 13 | pub enum NsfDriverType { 14 | Unknown, 15 | FTClassic, 16 | FT0CC, 17 | FTDn 18 | } 19 | 20 | #[derive(Clone)] 21 | pub struct Nsf { 22 | raw_bytes: Vec, 23 | memoized_driver_type: NsfDriverType 24 | } 25 | 26 | fn determine_driver_type(raw_bytes: &[u8]) -> NsfDriverType { 27 | if find_subsequence(&raw_bytes, b"FTDRV").is_some() { 28 | NsfDriverType::FTClassic 29 | } else if find_subsequence(&raw_bytes, b"0CCFT").is_some() { 30 | NsfDriverType::FT0CC 31 | } else if find_subsequence(&raw_bytes, b"DN-FT").is_some() 32 | || find_subsequence(&raw_bytes, b"Dn-FT").is_some() { 33 | NsfDriverType::FTDn 34 | } else { 35 | NsfDriverType::Unknown 36 | } 37 | } 38 | 39 | pub fn decode_shift_jis(s: &[u8]) -> Option { 40 | let mut decoder = SHIFT_JIS.new_decoder(); 41 | let mut result = String::new(); 42 | result.reserve(s.len() * 4); // Probably way more than ever needed but better safe than sorry 43 | 44 | let (coder_result, _bytes_read, did_replacements) = decoder.decode_to_string(s, &mut result, true); 45 | if coder_result == CoderResult::OutputFull || did_replacements { 46 | return None; 47 | } 48 | 49 | Some(result) 50 | } 51 | 52 | macro_rules! string_fn { 53 | ($name: tt, $offset: literal, $max_len: literal) => { 54 | pub fn $name(&self) -> Result { 55 | self.parse_string($offset, $max_len) 56 | } 57 | } 58 | } 59 | 60 | macro_rules! bitflag_fn { 61 | ($offset: literal, $name: tt, $mask: literal) => { 62 | pub fn $name(&self) -> bool { 63 | (self.raw_bytes[$offset] & $mask) != 0 64 | } 65 | } 66 | } 67 | 68 | macro_rules! expansion_chip_fn { 69 | ($name: tt, $mask: literal) => { 70 | bitflag_fn!(0x7B, $name, $mask); 71 | } 72 | } 73 | 74 | macro_rules! nsf2_feature_fn { 75 | ($name: tt, $mask: literal) => { 76 | bitflag_fn!(0x7C, $name, $mask); 77 | } 78 | } 79 | 80 | impl Nsf { 81 | pub fn from(data: &[u8]) -> Nsf { 82 | let raw_bytes = match &data[0..4] { 83 | b"NSFE" => nsfe_to_nsf2(data).unwrap(), 84 | _ => data.to_vec() 85 | }; 86 | let memoized_driver_type = determine_driver_type(&raw_bytes); 87 | 88 | Nsf { 89 | raw_bytes, 90 | memoized_driver_type, 91 | } 92 | } 93 | 94 | pub fn magic_valid(&self) -> bool { 95 | &self.raw_bytes[..5] == b"NESM\x1A" 96 | } 97 | 98 | pub fn version(&self) -> u8 { 99 | self.raw_bytes[5] 100 | } 101 | 102 | pub fn songs(&self) -> u8 { 103 | self.raw_bytes[6] 104 | } 105 | 106 | pub fn starting_song(&self) -> u8 { 107 | self.raw_bytes[7] 108 | } 109 | 110 | fn parse_string(&self, offset: usize, max_len: usize) -> Result { 111 | let end = (offset..offset+max_len) 112 | .position(|i| self.raw_bytes[i] == 0) 113 | .unwrap_or(max_len); 114 | 115 | if let Some(shift_jis) = decode_shift_jis(&self.raw_bytes[offset..offset+end]) { 116 | return Ok(shift_jis); 117 | } 118 | Ok(str::from_utf8(&self.raw_bytes[offset..offset+end])?.to_string()) 119 | } 120 | 121 | string_fn!(title, 0xE, 0x20); 122 | string_fn!(artist, 0x2E, 0x20); 123 | string_fn!(copyright, 0x4E, 0x20); 124 | 125 | expansion_chip_fn!(vrc6, 0b0000_0001); 126 | expansion_chip_fn!(vrc7, 0b0000_0010); 127 | expansion_chip_fn!(fds, 0b0000_0100); 128 | expansion_chip_fn!(mmc5, 0b0000_1000); 129 | expansion_chip_fn!(n163, 0b0001_0000); 130 | expansion_chip_fn!(s5b, 0b0010_0000); 131 | 132 | pub fn driver_type(&self) -> NsfDriverType { 133 | if self.magic_valid() { 134 | self.memoized_driver_type 135 | } else { 136 | NsfDriverType::Unknown 137 | } 138 | } 139 | 140 | nsf2_feature_fn!(nsf2_irq, 0b0001_0000); 141 | nsf2_feature_fn!(nsf2_nonreturning_init, 0b0010_0000); 142 | nsf2_feature_fn!(nsf2_no_play_subroutine, 0b0100_0000); 143 | nsf2_feature_fn!(nsf2_has_metadata, 0b1000_0000); 144 | 145 | fn nsf2_program_length(&self) -> u32 { 146 | (u32::from_le_bytes((&self.raw_bytes[0x7C..0x80]).try_into().unwrap()) & 0xFFFFFF00) >> 8 147 | } 148 | 149 | pub fn nsfe_metadata(&self) -> Option { 150 | let metadata_offset = match (self.version(), self.nsf2_has_metadata()) { 151 | (2, true) => self.nsf2_program_length() as usize + 0x80, 152 | _ => return None 153 | }; 154 | 155 | match NsfeMetadata::from(&self.raw_bytes[metadata_offset..]) { 156 | Ok(d) => Some(d), 157 | Err(e) => { 158 | println!("NSFe metadata parse error: {}", e); 159 | None 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/renderer/options.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::str::FromStr; 3 | use std::ffi::OsStr; 4 | use std::fmt::{Display, Formatter}; 5 | use rusticnes_ui_common::piano_roll_window::ChannelSettings; 6 | use crate::video_builder::video_options::VideoOptions; 7 | 8 | pub const FRAME_RATE: i32 = 60; 9 | 10 | macro_rules! extra_str_traits { 11 | ($t: ty) => { 12 | impl From<&OsStr> for $t { 13 | fn from(value: &OsStr) -> Self { 14 | <$t>::from_str(value.to_str().unwrap()).unwrap() 15 | } 16 | } 17 | 18 | impl From for $t { 19 | fn from(value: String) -> Self { 20 | <$t>::from_str(value.as_str()).unwrap() 21 | } 22 | } 23 | } 24 | } 25 | 26 | #[derive(Copy, Clone)] 27 | pub enum StopCondition { 28 | Frames(u64), 29 | Loops(usize), 30 | NsfeLength 31 | } 32 | 33 | impl Display for StopCondition { 34 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 35 | match self { 36 | StopCondition::Frames(frames) => { 37 | if (*frames % FRAME_RATE as u64) == 0 { 38 | write!(f, "time:{}", *frames / FRAME_RATE as u64) 39 | } else { 40 | write!(f, "frames:{}", *frames) 41 | } 42 | }, 43 | StopCondition::Loops(loops) => write!(f, "loops:{}", *loops), 44 | StopCondition::NsfeLength => write!(f, "time:nsfe") 45 | } 46 | } 47 | } 48 | 49 | impl FromStr for StopCondition { 50 | type Err = String; 51 | 52 | fn from_str(s: &str) -> Result { 53 | let parts: Vec<_> = s.split(':').collect(); 54 | if parts.len() != 2 { 55 | return Err("Stop condition format invalid, try one of 'time:3', 'time:nsfe', 'frames:180', or 'loops:2'.".to_string()); 56 | } 57 | 58 | match parts[0] { 59 | "time" => match parts[1] { 60 | "nsfe" => Ok(StopCondition::NsfeLength), 61 | _ => { 62 | let time = u64::from_str(parts[1]).map_err( | e | e.to_string())?; 63 | Ok(StopCondition::Frames(time * FRAME_RATE as u64)) 64 | } 65 | }, 66 | "frames" => { 67 | let frames = u64::from_str(parts[1]).map_err(|e| e.to_string())?; 68 | Ok(StopCondition::Frames(frames)) 69 | }, 70 | "loops" => { 71 | let loops = usize::from_str(parts[1]).map_err(|e| e.to_string())?; 72 | Ok(StopCondition::Loops(loops)) 73 | }, 74 | _ => Err(format!("Unknown condition type {}. Valid types are 'time', 'frames', and 'loops'", parts[0])) 75 | } 76 | } 77 | } 78 | 79 | extra_str_traits!(StopCondition); 80 | 81 | #[derive(Clone)] 82 | pub struct RendererOptions { 83 | pub input_path: String, 84 | pub video_options: VideoOptions, 85 | 86 | pub track_index: u8, 87 | pub stop_condition: StopCondition, 88 | pub fadeout_length: u64, 89 | 90 | pub famicom: bool, 91 | pub high_quality: bool, 92 | pub multiplexing: bool, 93 | 94 | pub channel_settings: HashMap<(String, String), ChannelSettings>, 95 | pub config_import_path: Option 96 | } 97 | 98 | impl Default for RendererOptions { 99 | fn default() -> Self { 100 | Self { 101 | input_path: "".to_string(), 102 | video_options: VideoOptions { 103 | output_path: "".to_string(), 104 | metadata: Default::default(), 105 | background_path: None, 106 | video_time_base: (29_781, 1_789_773).into(), 107 | video_codec: "libx264".to_string(), 108 | video_codec_params: Default::default(), 109 | pixel_format_in: "rgba".to_string(), 110 | pixel_format_out: "yuv420p".to_string(), 111 | resolution_in: (960, 540), 112 | resolution_out: (1920, 1080), 113 | audio_time_base: (1, 44_100).into(), 114 | audio_codec: "aac".to_string(), 115 | audio_codec_params: Default::default(), 116 | audio_channels: 1, 117 | sample_format_in: "s16".to_string(), 118 | sample_format_out: "fltp".to_string(), 119 | sample_rate: 44_100, 120 | }, 121 | track_index: 0, 122 | stop_condition: StopCondition::Frames(300 * FRAME_RATE as u64), 123 | fadeout_length: 180, 124 | famicom: false, 125 | high_quality: true, 126 | multiplexing: false, 127 | channel_settings: HashMap::new(), 128 | config_import_path: None 129 | } 130 | } 131 | } 132 | 133 | impl RendererOptions { 134 | pub fn set_resolution_smart(&mut self, w: u32, h: u32) { 135 | self.video_options.resolution_out = (w, h); 136 | 137 | self.video_options.resolution_in = if w >= h { 138 | (960, ((960.0 / w as f32) * (h as f32)) as u32) 139 | } else { 140 | (((960.0 / h as f32) * (w as f32)) as u32, 960) 141 | }; 142 | 143 | println!("{}x{}", self.video_options.resolution_in.0, self.video_options.resolution_in.1); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/apu/noise.rs: -------------------------------------------------------------------------------- 1 | use super::length_counter::LengthCounterState; 2 | use super::volume_envelope::VolumeEnvelopeState; 3 | use super::audio_channel::AudioChannelState; 4 | use super::audio_channel::PlaybackRate; 5 | use super::audio_channel::Volume; 6 | use super::audio_channel::Timbre; 7 | use super::ring_buffer::RingBuffer; 8 | use super::filters; 9 | use super::filters::DspFilter; 10 | 11 | pub struct NoiseChannelState { 12 | pub name: String, 13 | pub chip: String, 14 | pub debug_disable: bool, 15 | pub output_buffer: RingBuffer, 16 | pub edge_buffer: RingBuffer, 17 | pub last_edge: bool, 18 | pub debug_filter: filters::HighPassIIR, 19 | pub length: u8, 20 | pub length_halt_flag: bool, 21 | 22 | pub envelope: VolumeEnvelopeState, 23 | pub length_counter: LengthCounterState, 24 | 25 | pub mode: u8, 26 | pub period_initial: u16, 27 | pub period_current: u16, 28 | 29 | // Actually a 15-bit register 30 | pub shift_register: u16, 31 | } 32 | 33 | impl NoiseChannelState { 34 | pub fn new(channel_name: &str, chip_name: &str, ) -> NoiseChannelState { 35 | return NoiseChannelState { 36 | name: String::from(channel_name), 37 | chip: String::from(chip_name), 38 | debug_disable: false, 39 | output_buffer: RingBuffer::new(32768), 40 | edge_buffer: RingBuffer::new(32768), 41 | last_edge: false, 42 | debug_filter: filters::HighPassIIR::new(44100.0, 300.0), 43 | length: 0, 44 | length_halt_flag: false, 45 | 46 | envelope: VolumeEnvelopeState::new(), 47 | length_counter: LengthCounterState::new(), 48 | mode: 0, 49 | period_initial: 0, 50 | period_current: 0, 51 | 52 | // Actually a 15-bit register 53 | shift_register: 1, 54 | } 55 | } 56 | 57 | pub fn clock(&mut self) { 58 | if self.period_current == 0 { 59 | self.period_current = self.period_initial - 1; 60 | 61 | let mut feedback = self.shift_register & 0b1; 62 | if self.mode == 1 { 63 | feedback ^= (self.shift_register >> 6) & 0b1; 64 | } else { 65 | feedback ^= (self.shift_register >> 1) & 0b1; 66 | } 67 | self.shift_register = self.shift_register >> 1; 68 | self.shift_register |= feedback << 14; 69 | self.last_edge = true; 70 | } else { 71 | self.period_current -= 1; 72 | } 73 | } 74 | 75 | pub fn output(&self) -> i16 { 76 | if self.length_counter.length > 0 { 77 | let mut sample = (self.shift_register & 0b1) as i16; 78 | sample *= self.envelope.current_volume() as i16; 79 | return sample; 80 | } else { 81 | return 0; 82 | } 83 | } 84 | } 85 | 86 | impl AudioChannelState for NoiseChannelState { 87 | fn name(&self) -> String { 88 | return self.name.clone(); 89 | } 90 | 91 | fn chip(&self) -> String { 92 | return self.chip.clone(); 93 | } 94 | 95 | fn sample_buffer(&self) -> &RingBuffer { 96 | return &self.output_buffer; 97 | } 98 | 99 | fn edge_buffer(&self) -> &RingBuffer { 100 | return &self.edge_buffer; 101 | } 102 | 103 | fn record_current_output(&mut self) { 104 | self.debug_filter.consume(self.output() as f32); 105 | self.output_buffer.push((self.debug_filter.output() * -4.0) as i16); 106 | self.edge_buffer.push(self.last_edge as i16); 107 | self.last_edge = false; 108 | } 109 | 110 | fn min_sample(&self) -> i16 { 111 | return -60; 112 | } 113 | 114 | fn max_sample(&self) -> i16 { 115 | return 60; 116 | } 117 | 118 | fn muted(&self) -> bool { 119 | return self.debug_disable; 120 | } 121 | 122 | fn mute(&mut self) { 123 | self.debug_disable = true; 124 | } 125 | 126 | fn unmute(&mut self) { 127 | self.debug_disable = false; 128 | } 129 | 130 | fn playing(&self) -> bool { 131 | return 132 | (self.length_counter.length > 0) && 133 | (self.envelope.current_volume() > 0); 134 | } 135 | 136 | fn rate(&self) -> PlaybackRate { 137 | let lsfr_index = match self.period_initial { 138 | 4 => {0xF}, 139 | 8 => {0xE}, 140 | 16 => {0xD}, 141 | 32 => {0xC}, 142 | 64 => {0xB}, 143 | 96 => {0xA}, 144 | 128 => {0x9}, 145 | 160 => {0x8}, 146 | 202 => {0x7}, 147 | 254 => {0x6}, 148 | 380 => {0x5}, 149 | 508 => {0x4}, 150 | 762 => {0x3}, 151 | 1016 => {0x2}, 152 | 2034 => {0x1}, 153 | 4068 => {0x0}, 154 | _ => {0x0} // also unreachable 155 | }; 156 | return PlaybackRate::LfsrRate {index: lsfr_index, max: 0xF}; 157 | } 158 | 159 | fn volume(&self) -> Option { 160 | return Some(Volume::VolumeIndex{ index: self.envelope.current_volume() as usize, max: 15 }); 161 | } 162 | 163 | fn timbre(&self) -> Option { 164 | return Some(Timbre::LsfrMode{index: self.mode as usize, max: 1}); 165 | } 166 | } -------------------------------------------------------------------------------- /external/rusticnes-core/src/mmc/pxrom.rs: -------------------------------------------------------------------------------- 1 | // MMC2, a somewhat advanced bank switcher with extended CHR memory 2 | // https://wiki.nesdev.com/w/index.php/MMC2 3 | 4 | use ines::INesCartridge; 5 | use memoryblock::MemoryBlock; 6 | 7 | use mmc::mapper::*; 8 | use mmc::mirroring; 9 | 10 | pub struct PxRom { 11 | pub prg_rom: MemoryBlock, 12 | pub prg_ram: MemoryBlock, 13 | pub chr: MemoryBlock, 14 | pub mirroring: Mirroring, 15 | pub chr_0_latch: u8, 16 | pub chr_0_fd_bank: usize, 17 | pub chr_0_fe_bank: usize, 18 | pub chr_1_latch: u8, 19 | pub chr_1_fd_bank: usize, 20 | pub chr_1_fe_bank: usize, 21 | pub prg_bank: usize, 22 | pub vram: Vec, 23 | } 24 | 25 | impl PxRom { 26 | pub fn from_ines(ines: INesCartridge) -> Result { 27 | let prg_rom_block = ines.prg_rom_block(); 28 | let prg_ram_block = ines.prg_ram_block()?; 29 | let chr_block = ines.chr_block()?; 30 | 31 | return Ok(PxRom { 32 | prg_rom: prg_rom_block.clone(), 33 | prg_ram: prg_ram_block.clone(), 34 | chr: chr_block.clone(), 35 | mirroring: Mirroring::Vertical, 36 | chr_0_latch: 0, 37 | chr_0_fd_bank: 0, 38 | chr_0_fe_bank: 0, 39 | chr_1_latch: 0, 40 | chr_1_fd_bank: 0, 41 | chr_1_fe_bank: 0, 42 | prg_bank: 0, 43 | vram: vec![0u8; 0x1000], 44 | }) 45 | } 46 | } 47 | 48 | impl Mapper for PxRom { 49 | fn print_debug_status(&self) { 50 | println!("======= PxROM ======="); 51 | println!("PRG Bank: {}, ", self.prg_bank); 52 | println!("CHR0 0xFD Bank: {}. CHR0 0xFE Bank: {}", self.chr_0_fd_bank, self.chr_0_fe_bank); 53 | println!("CHR1 0xFD Bank: {}. CHR1 0xFE Bank: {}", self.chr_1_fd_bank, self.chr_1_fe_bank); 54 | println!("Mirroring Mode: {}", mirroring_mode_name(self.mirroring)); 55 | println!("===================="); 56 | } 57 | 58 | fn mirroring(&self) -> Mirroring { 59 | return self.mirroring; 60 | } 61 | 62 | fn debug_read_cpu(&self, address: u16) -> Option { 63 | match address { 64 | 0x6000 ..= 0x7FFF => self.prg_ram.wrapping_read((address - 0x6000) as usize), 65 | 0x8000 ..= 0x9FFF => self.prg_rom.banked_read(0x2000, self.prg_bank, address as usize - 0x8000), 66 | 0xA000 ..= 0xBFFF => self.prg_rom.banked_read(0x2000, 0xFD, address as usize - 0xA000), 67 | 0xC000 ..= 0xDFFF => self.prg_rom.banked_read(0x2000, 0xFE, address as usize - 0xC000), 68 | 0xE000 ..= 0xFFFF => self.prg_rom.banked_read(0x2000, 0xFF, address as usize - 0xE000), 69 | _ => None 70 | } 71 | } 72 | 73 | fn write_cpu(&mut self, address: u16, data: u8) { 74 | match address { 75 | 0x6000 ..= 0x7FFF => self.prg_ram.wrapping_write(address as usize, data), 76 | 0xA000 ..= 0xAFFF => { self.prg_bank = (data & 0b0000_1111) as usize; }, 77 | 0xB000 ..= 0xBFFF => { self.chr_0_fd_bank = (data & 0b0001_1111) as usize; }, 78 | 0xC000 ..= 0xCFFF => { self.chr_0_fe_bank = (data & 0b0001_1111) as usize; }, 79 | 0xD000 ..= 0xDFFF => { self.chr_1_fd_bank = (data & 0b0001_1111) as usize; }, 80 | 0xE000 ..= 0xEFFF => { self.chr_1_fe_bank = (data & 0b0001_1111) as usize; }, 81 | 0xF000 ..= 0xFFFF => { 82 | if data & 0b1 == 0 { 83 | self.mirroring = Mirroring::Vertical; 84 | } else { 85 | self.mirroring = Mirroring::Horizontal; 86 | } 87 | }, 88 | _ => {} 89 | } 90 | } 91 | 92 | fn read_ppu(&mut self, address: u16) -> Option { 93 | match address { 94 | 0x0FD8 => {self.chr_0_latch = 0;}, 95 | 0x0FE8 => {self.chr_0_latch = 1;}, 96 | 0x1FD8 ..= 0x1FDF => {self.chr_1_latch = 0;}, 97 | 0x1FE8 ..= 0x1FEF => {self.chr_1_latch = 1;}, 98 | _ => {} 99 | } 100 | return self.debug_read_ppu(address); 101 | } 102 | 103 | fn debug_read_ppu(&self, address: u16) -> Option { 104 | match address { 105 | 0x0000 ..= 0x0FFF => { 106 | let chr_bank = match self.chr_0_latch { 107 | 0 => self.chr_0_fd_bank, 108 | 1 => self.chr_0_fe_bank, 109 | _ => 0 110 | }; 111 | self.chr.banked_read(0x1000, chr_bank, address as usize - 0x0000) 112 | }, 113 | 0x1000 ..= 0x1FFF => { 114 | let chr_bank = match self.chr_1_latch { 115 | 0 => self.chr_1_fd_bank, 116 | 1 => self.chr_1_fe_bank, 117 | _ => 0 118 | }; 119 | self.chr.banked_read(0x1000, chr_bank, address as usize - 0x0000) 120 | }, 121 | 0x2000 ..= 0x3FFF => match self.mirroring { 122 | Mirroring::Horizontal => Some(self.vram[mirroring::horizontal_mirroring(address) as usize]), 123 | Mirroring::Vertical => Some(self.vram[mirroring::vertical_mirroring(address) as usize]), 124 | _ => None 125 | }, 126 | _ => None 127 | } 128 | } 129 | 130 | fn write_ppu(&mut self, address: u16, data: u8) { 131 | match address { 132 | 0x2000 ..= 0x3FFF => match self.mirroring { 133 | Mirroring::Horizontal => self.vram[mirroring::horizontal_mirroring(address) as usize] = data, 134 | Mirroring::Vertical => self.vram[mirroring::vertical_mirroring(address) as usize] = data, 135 | _ => {} 136 | }, 137 | _ => {} 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/nes.rs: -------------------------------------------------------------------------------- 1 | use apu::ApuState; 2 | use cartridge; 3 | use cycle_cpu; 4 | use cycle_cpu::CpuState; 5 | use cycle_cpu::Registers; 6 | use memory; 7 | use memory::CpuMemory; 8 | use ppu::PpuState; 9 | use mmc::mapper::Mapper; 10 | use tracked_events::EventTracker; 11 | 12 | pub struct NesState { 13 | pub apu: ApuState, 14 | pub cpu: CpuState, 15 | pub memory: CpuMemory, 16 | pub ppu: PpuState, 17 | pub registers: Registers, 18 | pub master_clock: u64, 19 | pub p1_input: u8, 20 | pub p1_data: u8, 21 | pub p2_input: u8, 22 | pub p2_data: u8, 23 | pub input_latch: bool, 24 | pub mapper: Box, 25 | pub last_frame: u32, 26 | pub event_tracker: EventTracker, 27 | } 28 | 29 | impl NesState { 30 | pub fn new(m: Box) -> NesState { 31 | return NesState { 32 | apu: ApuState::new(), 33 | cpu: CpuState::new(), 34 | memory: CpuMemory::new(), 35 | ppu: PpuState::new(), 36 | registers: Registers::new(), 37 | master_clock: 0, 38 | p1_input: 0, 39 | p1_data: 0, 40 | p2_input: 0, 41 | p2_data: 0, 42 | input_latch: false, 43 | mapper: m, 44 | last_frame: 0, 45 | event_tracker: EventTracker::new(), 46 | } 47 | } 48 | 49 | #[deprecated(since="0.2.0", note="please use `::new(mapper)` instead")] 50 | pub fn from_rom(cart_data: &[u8]) -> Result { 51 | let maybe_mapper = cartridge::mapper_from_file(cart_data); 52 | match maybe_mapper { 53 | Ok(mapper) => { 54 | let mut nes = NesState::new(mapper); 55 | nes.power_on(); 56 | return Ok(nes); 57 | }, 58 | Err(why) => { 59 | return Err(why); 60 | } 61 | } 62 | } 63 | 64 | pub fn power_on(&mut self) { 65 | // Initialize CPU register state for power-up sequence 66 | self.registers.a = 0; 67 | self.registers.y = 0; 68 | self.registers.x = 0; 69 | self.registers.s = 0xFD; 70 | 71 | self.registers.set_status_from_byte(0x34); 72 | 73 | // Initialize I/O and Audio registers to known startup values 74 | for i in 0x4000 .. (0x400F + 1) { 75 | memory::write_byte(self, i, 0); 76 | } 77 | memory::write_byte(self, 0x4015, 0); 78 | memory::write_byte(self, 0x4017, 0); 79 | 80 | let pc_low = memory::read_byte(self, 0xFFFC); 81 | let pc_high = memory::read_byte(self, 0xFFFD); 82 | self.registers.pc = pc_low as u16 + ((pc_high as u16) << 8); 83 | 84 | // Clock the APU 10 times (this subtly affects the first IRQ's timing and frame counter operation) 85 | for _ in 0 .. 10 { 86 | self.apu.clock_apu(&mut *self.mapper); 87 | } 88 | } 89 | 90 | pub fn reset(&mut self) { 91 | self.registers.s = self.registers.s.wrapping_sub(3); 92 | self.registers.flags.interrupts_disabled = true; 93 | 94 | // Silence the APU 95 | memory::write_byte(self, 0x4015, 0); 96 | 97 | let pc_low = memory::read_byte(self, 0xFFFC); 98 | let pc_high = memory::read_byte(self, 0xFFFD); 99 | self.registers.pc = pc_low as u16 + ((pc_high as u16) << 8); 100 | } 101 | 102 | pub fn cycle(&mut self) { 103 | cycle_cpu::run_one_clock(self); 104 | self.master_clock = self.master_clock + 12; 105 | // Three PPU clocks per every 1 CPU clock 106 | self.ppu.clock(&mut *self.mapper); 107 | self.ppu.clock(&mut *self.mapper); 108 | self.ppu.clock(&mut *self.mapper); 109 | self.event_tracker.current_scanline = self.ppu.current_scanline; 110 | self.event_tracker.current_cycle = self.ppu.current_scanline_cycle; 111 | self.apu.clock_apu(&mut *self.mapper); 112 | self.mapper.clock_cpu(); 113 | } 114 | 115 | pub fn step(&mut self) { 116 | // Always run at least one cycle 117 | self.cycle(); 118 | let mut i = 0; 119 | // Continue until either we loop back around to cycle 0 (a new instruction) 120 | // or this instruction has failed to reset (encountered a STP or an opcode bug) 121 | while self.cpu.tick >= 1 && i < 10 { 122 | self.cycle(); 123 | i += 1; 124 | } 125 | if self.ppu.current_frame != self.last_frame { 126 | self.event_tracker.swap_buffers(); 127 | self.last_frame = self.ppu.current_frame; 128 | } 129 | } 130 | 131 | pub fn run_until_hblank(&mut self) { 132 | let old_scanline = self.ppu.current_scanline; 133 | while old_scanline == self.ppu.current_scanline { 134 | self.step(); 135 | } 136 | } 137 | 138 | pub fn run_until_vblank(&mut self) { 139 | while self.ppu.current_scanline == 242 { 140 | self.step(); 141 | } 142 | while self.ppu.current_scanline != 242 { 143 | self.step(); 144 | } 145 | } 146 | 147 | pub fn nudge_ppu_alignment(&mut self) { 148 | // Give the PPU a swift kick: 149 | self.ppu.clock(&mut *self.mapper); 150 | self.event_tracker.current_scanline = self.ppu.current_scanline; 151 | self.event_tracker.current_cycle = self.ppu.current_scanline_cycle; 152 | } 153 | 154 | pub fn sram(&self) -> Vec { 155 | return self.mapper.get_sram(); 156 | } 157 | 158 | pub fn set_sram(&mut self, sram_data: Vec) { 159 | if sram_data.len() != self.mapper.get_sram().len() { 160 | println!("SRAM size mismatch, expected {} bytes but file is {} bytes!", self.mapper.get_sram().len(), sram_data.len()); 161 | } else { 162 | self.mapper.load_sram(sram_data); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/apu/triangle.rs: -------------------------------------------------------------------------------- 1 | use super::length_counter::LengthCounterState; 2 | use super::audio_channel::AudioChannelState; 3 | use super::audio_channel::PlaybackRate; 4 | use super::audio_channel::Volume; 5 | use super::audio_channel::Timbre; 6 | use super::ring_buffer::RingBuffer; 7 | use super::filters; 8 | use super::filters::DspFilter; 9 | 10 | pub struct TriangleChannelState { 11 | pub name: String, 12 | pub chip: String, 13 | pub debug_disable: bool, 14 | pub output_buffer: RingBuffer, 15 | pub edge_buffer: RingBuffer, 16 | pub last_edge: bool, 17 | pub debug_filter: filters::HighPassIIR, 18 | pub length_counter: LengthCounterState, 19 | 20 | pub control_flag: bool, 21 | pub linear_reload_flag: bool, 22 | pub linear_counter_initial: u8, 23 | pub linear_counter_current: u8, 24 | 25 | pub sequence_counter: u8, 26 | pub period_initial: u16, 27 | pub period_current: u16, 28 | pub length: u8, 29 | 30 | pub cpu_clock_rate: u64, 31 | } 32 | 33 | impl TriangleChannelState { 34 | pub fn new(channel_name: &str, chip_name: &str, cpu_clock_rate: u64) -> TriangleChannelState { 35 | return TriangleChannelState { 36 | name: String::from(channel_name), 37 | chip: String::from(chip_name), 38 | debug_disable: false, 39 | output_buffer: RingBuffer::new(32768), 40 | last_edge: false, 41 | debug_filter: filters::HighPassIIR::new(44100.0, 300.0), 42 | edge_buffer: RingBuffer::new(32768), 43 | length_counter: LengthCounterState::new(), 44 | control_flag: false, 45 | linear_reload_flag: false, 46 | linear_counter_initial: 0, 47 | linear_counter_current: 0, 48 | 49 | sequence_counter: 0, 50 | period_initial: 0, 51 | period_current: 0, 52 | length: 0, 53 | 54 | cpu_clock_rate: cpu_clock_rate, 55 | } 56 | } 57 | 58 | pub fn update_linear_counter(&mut self) { 59 | if self.linear_reload_flag { 60 | self.linear_counter_current = self.linear_counter_initial; 61 | } else { 62 | if self.linear_counter_current > 0 { 63 | self.linear_counter_current -= 1; 64 | } 65 | } 66 | if !(self.control_flag) { 67 | self.linear_reload_flag = false; 68 | } 69 | } 70 | 71 | pub fn clock(&mut self) { 72 | if self.linear_counter_current != 0 && self.length_counter.length > 0 { 73 | if self.period_current == 0 { 74 | // Reset the period timer, and clock the waveform generator 75 | self.period_current = self.period_initial; 76 | 77 | // The sequence counter starts at zero, but counts downwards, resulting in an odd 78 | // lookup sequence of 0, 7, 6, 5, 4, 3, 2, 1 79 | if self.sequence_counter >= 31 { 80 | self.sequence_counter = 0; 81 | self.last_edge = true; 82 | } else { 83 | self.sequence_counter += 1; 84 | } 85 | } else { 86 | self.period_current -= 1; 87 | } 88 | } else { 89 | // When the triangle is disabled, treat every sample as the last edge. This prevents 90 | // holding onto the trailing end of the waveform in the debug display for too long 91 | self.last_edge = true; 92 | } 93 | } 94 | 95 | pub fn output(&self) -> i16 { 96 | if self.period_initial <= 2 { 97 | // This frequency is so high that the hardware mixer can't keep up, and effectively 98 | // receives 7.5. We'll just return 7 here (close enough). Some games use this 99 | // to silence the channel, and returning 7 emulates the resulting clicks and pops. 100 | return 7; 101 | } else { 102 | let triangle_sequence = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15, 103 | 15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0]; 104 | return triangle_sequence[self.sequence_counter as usize]; 105 | } 106 | } 107 | } 108 | 109 | impl AudioChannelState for TriangleChannelState { 110 | fn name(&self) -> String { 111 | return self.name.clone(); 112 | } 113 | 114 | fn chip(&self) -> String { 115 | return self.chip.clone(); 116 | } 117 | 118 | fn sample_buffer(&self) -> &RingBuffer { 119 | return &self.output_buffer; 120 | } 121 | 122 | fn edge_buffer(&self) -> &RingBuffer { 123 | return &self.edge_buffer; 124 | } 125 | 126 | fn record_current_output(&mut self) { 127 | self.debug_filter.consume(self.output() as f32); 128 | self.output_buffer.push((self.debug_filter.output() * -4.0) as i16); 129 | self.edge_buffer.push(self.last_edge as i16); 130 | self.last_edge = false; 131 | } 132 | 133 | fn min_sample(&self) -> i16 { 134 | return -60; 135 | } 136 | 137 | fn max_sample(&self) -> i16 { 138 | return 60; 139 | } 140 | 141 | fn muted(&self) -> bool { 142 | return self.debug_disable; 143 | } 144 | 145 | fn mute(&mut self) { 146 | self.debug_disable = true; 147 | } 148 | 149 | fn unmute(&mut self) { 150 | self.debug_disable = false; 151 | } 152 | 153 | fn playing(&self) -> bool { 154 | return 155 | self.length_counter.length > 0 && 156 | self.linear_counter_current != 0 && 157 | self.period_initial > 2; 158 | } 159 | 160 | fn rate(&self) -> PlaybackRate { 161 | let frequency = self.cpu_clock_rate as f32 / (32.0 * (self.period_initial as f32 + 1.0)); 162 | return PlaybackRate::FundamentalFrequency {frequency: frequency}; 163 | } 164 | 165 | fn volume(&self) -> Option { 166 | return None; 167 | } 168 | 169 | fn timbre(&self) -> Option { 170 | return None; 171 | } 172 | 173 | fn amplitude(&self) -> f32 { 174 | if self.playing() { 175 | return 0.55; 176 | } 177 | return 0.0; 178 | } 179 | } -------------------------------------------------------------------------------- /external/rusticnes-core/src/tracked_events.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy)] 2 | pub enum EventType { 3 | NullEvent, 4 | CpuRead{program_counter: u16, address: u16, data: u8}, 5 | CpuWrite{program_counter: u16, address: u16, data: u8}, 6 | CpuExecute{program_counter: u16, data: u8}, 7 | } 8 | 9 | #[derive(Clone, Copy)] 10 | pub struct TrackedEvent { 11 | pub scanline: u16, 12 | pub cycle: u16, 13 | pub event_type: EventType, 14 | } 15 | 16 | pub struct EventTracker { 17 | pub tracked_events_a: Vec, 18 | pub size_a: usize, 19 | pub tracked_events_b: Vec, 20 | pub size_b: usize, 21 | pub a_active: bool, 22 | pub current_scanline: u16, 23 | pub current_cycle: u16, 24 | pub cpu_snoop_list: Vec, 25 | } 26 | 27 | const CPU_READ: u8 = 0b0000_0001; 28 | const CPU_WRITE: u8 = 0b0000_0010; 29 | const CPU_EXECUTE: u8 = 0b0000_0100; 30 | 31 | impl EventTracker { 32 | pub fn new() -> EventTracker { 33 | let mut default_cpu_snoops = vec![0u8; 0x10000]; 34 | 35 | // PPU registers 36 | default_cpu_snoops[0x2000] = CPU_WRITE; 37 | default_cpu_snoops[0x2001] = CPU_WRITE; 38 | default_cpu_snoops[0x2002] = CPU_WRITE | CPU_READ; 39 | default_cpu_snoops[0x2003] = CPU_WRITE; 40 | default_cpu_snoops[0x2004] = CPU_WRITE | CPU_READ; 41 | default_cpu_snoops[0x2005] = CPU_WRITE; 42 | default_cpu_snoops[0x2006] = CPU_WRITE; 43 | default_cpu_snoops[0x2007] = CPU_WRITE | CPU_READ; 44 | 45 | // APU registers 46 | // Pulse 1 47 | default_cpu_snoops[0x4000] = CPU_WRITE; 48 | default_cpu_snoops[0x4001] = CPU_WRITE; 49 | default_cpu_snoops[0x4002] = CPU_WRITE; 50 | default_cpu_snoops[0x4003] = CPU_WRITE; 51 | 52 | // Pulse 2 53 | default_cpu_snoops[0x4004] = CPU_WRITE; 54 | default_cpu_snoops[0x4005] = CPU_WRITE; 55 | default_cpu_snoops[0x4006] = CPU_WRITE; 56 | default_cpu_snoops[0x4007] = CPU_WRITE; 57 | 58 | // Triangle 59 | default_cpu_snoops[0x4008] = CPU_WRITE; 60 | default_cpu_snoops[0x400A] = CPU_WRITE; 61 | default_cpu_snoops[0x400B] = CPU_WRITE; 62 | 63 | // Noise 64 | default_cpu_snoops[0x400C] = CPU_WRITE; 65 | default_cpu_snoops[0x400E] = CPU_WRITE; 66 | default_cpu_snoops[0x400F] = CPU_WRITE; 67 | 68 | // DMC 69 | default_cpu_snoops[0x4010] = CPU_WRITE; 70 | default_cpu_snoops[0x4011] = CPU_WRITE; 71 | default_cpu_snoops[0x4012] = CPU_WRITE; 72 | default_cpu_snoops[0x4013] = CPU_WRITE; 73 | 74 | // Misc 75 | default_cpu_snoops[0x4014] = CPU_WRITE; 76 | default_cpu_snoops[0x4015] = CPU_WRITE | CPU_READ; 77 | default_cpu_snoops[0x4017] = CPU_WRITE; 78 | 79 | 80 | 81 | return EventTracker { 82 | // Way, way more events than we could *possibly* need, just to be safe 83 | // Manually indexed, and never resized, to avoid allocations at runtime 84 | tracked_events_a: vec![TrackedEvent{scanline: 0xFFFF, cycle: 0xFFFF, event_type: EventType::NullEvent}; 262*341], 85 | size_a: 0, 86 | tracked_events_b: vec![TrackedEvent{scanline: 0xFFFF, cycle: 0xFFFF, event_type: EventType::NullEvent}; 262*341], 87 | size_b: 0, 88 | a_active: true, 89 | current_scanline: 0, 90 | current_cycle: 0, 91 | cpu_snoop_list: default_cpu_snoops, 92 | } 93 | } 94 | 95 | pub fn track(&mut self, event: TrackedEvent) { 96 | match self.a_active { 97 | true => { 98 | self.tracked_events_a[self.size_a] = event; 99 | self.size_a += 1; 100 | }, 101 | false => { 102 | self.tracked_events_b[self.size_b] = event; 103 | self.size_b += 1; 104 | } 105 | } 106 | } 107 | 108 | pub fn swap_buffers(&mut self) { 109 | match self.a_active { 110 | true => { 111 | self.size_b = 0; 112 | self.a_active = false; 113 | }, 114 | false => { 115 | self.size_a = 0; 116 | self.a_active = true; 117 | } 118 | } 119 | } 120 | 121 | pub fn events_this_frame(&self) -> &[TrackedEvent] { 122 | match self.a_active { 123 | true => &self.tracked_events_a[..self.size_a], 124 | false => &self.tracked_events_b[..self.size_b], 125 | } 126 | } 127 | 128 | pub fn events_last_frame(&self) -> &[TrackedEvent] { 129 | match self.a_active { 130 | true => &self.tracked_events_b[..self.size_b], 131 | false => &self.tracked_events_a[..self.size_a], 132 | } 133 | } 134 | 135 | pub fn snoop_cpu_read(&mut self, program_counter: u16, address: u16, data: u8) { 136 | if (self.cpu_snoop_list[address as usize] & CPU_READ) != 0 { 137 | self.track(TrackedEvent{ 138 | scanline: self.current_scanline, 139 | cycle: self.current_cycle, 140 | event_type: EventType::CpuRead{ 141 | program_counter: program_counter, 142 | address: address, 143 | data: data, 144 | } 145 | }); 146 | } 147 | } 148 | 149 | pub fn snoop_cpu_write(&mut self, program_counter: u16, address: u16, data: u8) { 150 | if (self.cpu_snoop_list[address as usize] & CPU_WRITE) != 0 { 151 | self.track(TrackedEvent{ 152 | scanline: self.current_scanline, 153 | cycle: self.current_cycle, 154 | event_type: EventType::CpuWrite{ 155 | program_counter: program_counter, 156 | address: address, 157 | data: data, 158 | } 159 | }); 160 | } 161 | } 162 | 163 | pub fn snoop_cpu_execute(&mut self, program_counter: u16, data: u8) { 164 | if (self.cpu_snoop_list[program_counter as usize] & CPU_EXECUTE) != 0 { 165 | self.track(TrackedEvent{ 166 | scanline: self.current_scanline, 167 | cycle: self.current_cycle, 168 | event_type: EventType::CpuExecute{ 169 | program_counter: program_counter, 170 | data: data, 171 | } 172 | }); 173 | } 174 | } 175 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # NSFPresenter 6 | 7 | NSFPresenter is a tool I wrote to generate visualizations of my 8 | [Dn-FamiTracker][dn-ft] covers, based on [RusticNES][rusticnes], 9 | [FFmpeg][ffmpeg], and [Slint][slint]. 10 | You can see it in action on [my YouTube channel][yt]. I also wrote it 11 | to learn how to write Rust. 12 | 13 | ![Slint logo](assets/MadeWithSlint-logo-light.svg#gh-light-mode-only) 14 | ![Slint logo](assets/MadeWithSlint-logo-dark.svg#gh-dark-mode-only) 15 | 16 | ## Functionality 17 | 18 | NSFPresenter essentially runs your input NSF through RusticNES and 19 | sends the piano roll window's canvas and the emulated audio to FFmpeg 20 | to be encoded as a video. 21 | 22 | It supports NSF modules and some features of NSF2 modules. The output 23 | format is not very customizable (since FFmpeg is not easy to set up), 24 | but it should work for most usecases. Support for other containers and 25 | codecs is planned. 26 | 27 | ## Features 28 | 29 | - Supports NSF, NSFe, and NSF2 modules. 30 | - Supports all NSF expansion audio mappers. 31 | - Customized version of RusticNES: 32 | - Added FDS audio support. 33 | - Slight performance enhancements for NSF playback. 34 | - Outputs a video file: 35 | - Customizable resolution (default 1080p) at 60.10 FPS (the NES'/Famicom's true framerate). 36 | - MPEG-4 container with fast-start (`moov` atom at beginning of file). 37 | - Matroska (MKV) and QuickTime (MOV) containers are also supported. 38 | - yuv420p H.264 video stream encoded with libx264, crf: 16. 39 | - If using QuickTime, ProRes 4444 streams encoded with prores_ks are also supported. 40 | - Mono AAC LC audio stream encoded with FFmpeg's aac encoder, bitrate: 192k. 41 | - Video files are suitable for direct upload to most websites: 42 | - Outputs the recommended format for YouTube, Twitter, and Discord (w/ Nitro). 43 | - Typical H.264 exports (1080p, up to 5 minutes) are usually below 100MB. 44 | - Video files have metadata based on NSF metadata (title, artist, copyright, track index). 45 | - Loop detection for FamiTracker NSF exports. 46 | - NSFe/NSF2 features: 47 | - Support for extended metadata - no more 32-character limits! 48 | - Support for individual title/artist fields for each song in a multi-track NSF. 49 | - Support for NSFe duration field. 50 | - Support for custom VRC7 patches. 51 | - YM2413 (OPLL) support is planned but not yet available. 52 | - Support for custom mixing is planned but not yet available. 53 | 54 | ## Installation 55 | 56 | **Windows**: head to the Releases page and grab the latest binary release. Simply unzip 57 | and run the executable, and you're all set. 58 | 59 | **Linux**: no binaries yet, but you can compile from source. You'll need to have `ffmpeg` 60 | and optionally `Qt6` development packages installed, then clone the repo and run 61 | `cargo build --release` to build. 62 | 63 | ## Usage 64 | 65 | ### GUI 66 | 67 | 1. Click **Browse...** to select an input module. 68 | 2. The module's metadata, expansion chips, and supported features will 69 | be displayed. 70 | 3. Select a track to be rendered from the dropdown. 71 | 4. Select the duration of the output video. Available duration types are: 72 | - Seconds: explicit duration in seconds. 73 | - Frames: explicit duration in frames (1/60.1 of a second). 74 | - Loops: if loop detection is supported, number of loops to be played. 75 | - NSFe/NSF2 duration: if present, the track duration specified in the 76 | `time` field. 77 | 5. Select the duration of the fadeout in frames. This is not included in the 78 | video duration above, rather it's added on to the end. 79 | 6. Select the output video resolution. You can enter a custom resolution 80 | or use the 1080p/4K presets. 81 | 7. Optionally select a background for the visualization. You can select many 82 | common image and video formats to use as a background. 83 | - You can also elect to export a transparent video later if you would like 84 | to use a video editor. 85 | - *Note:* Video backgrounds must be 60 FPS, or they will play at 86 | the wrong speed. A fix for this is planned. 87 | 8. Select additional rendering options: 88 | - Famicom mode: Emulates the Famicom's audio filter chain instead of the 89 | NES', which results in a slightly noisier sound. 90 | - High-quality filtering: Uses more accurate filter emulation for slightly 91 | cleaner sound at the cost of increased render time. 92 | - Emulate multiplexing: Accurately emulates multiplexing in mappers like 93 | the N163. This results in a grittier sound, which may be desirable as 94 | it is sometimes used for effects. 95 | 9. Click **Render!** to select the output video filename and begin rendering 96 | the visualization. 97 | - If you would like to render a transparent video for editing, then choose 98 | a filename ending in `.mov` to export in a QuickTime container. When asked 99 | if you would like to export using ProRes 4444, select **OK**. 100 | 10. Once the render is complete, you can select another track or even change 101 | modules to render another tune. 102 | 103 | ### CLI 104 | 105 | If NSFPresenter is started with command line arguments, it runs in CLI mode. 106 | This allows for the automation of rendering visualizations which in turn 107 | allows for batch rendering and even automated uploads. 108 | 109 | The most basic invocation is this: 110 | ``` 111 | nsf-presenter-rs path/to/music.nsf path/to/output.mp4 112 | ``` 113 | 114 | Additional options: 115 | - `-R [rate]`: set the sample rate of the audio (default: 44100) 116 | - `-T [track]`: select the NSF track index (default: 1) 117 | - `-s [condition]`: select the output duration (default: `time:300`): 118 | - `time:[seconds]` 119 | - `frames:[frames]` 120 | - `loops:[loops]` (if supported) 121 | - `time:nsfe` (if supported) 122 | - `-S [fadeout]`: select the fadeout duration in frames (default: 180). 123 | - `--ow [width]`: select the output resolution width (default: 1920) 124 | - `--oh [height]`: select the output resolution height (default: 1080) 125 | - `-J`: emulate Famicom filter chain 126 | - `-L`: use low-quality filtering 127 | - `-X`: emulate multiplexing for mappers like the N163 128 | - `-h`: Additional help + options 129 | - Note: options not listed here are unstable and may cause crashes or 130 | other errors. 131 | 132 | [dn-ft]: https://github.com/Dn-Programming-Core-Management/Dn-FamiTracker 133 | [rusticnes]: https://github.com/zeta0134/rusticnes-core 134 | [ffmpeg]: https://github.com/FFmpeg/FFmpeg 135 | [slint]: https://slint-ui.com 136 | [yt]: https://youtube.com/@nununoisy 137 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/apu/filters.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | use std::f32::consts::PI; 4 | 5 | pub trait DspFilter: Send { 6 | fn consume(&mut self, sample: f32); 7 | fn output(&self) -> f32; 8 | } 9 | 10 | pub struct IdentityFilter { 11 | sample: f32 12 | } 13 | 14 | impl IdentityFilter { 15 | pub fn new() -> IdentityFilter { 16 | return IdentityFilter { 17 | sample: 0.0 18 | } 19 | } 20 | } 21 | 22 | impl DspFilter for IdentityFilter { 23 | fn consume(&mut self, new_input: f32) { 24 | self.sample = new_input; 25 | } 26 | 27 | fn output(&self) -> f32 { 28 | return self.sample; 29 | } 30 | } 31 | 32 | pub struct HighPassIIR { 33 | alpha: f32, 34 | previous_output: f32, 35 | previous_input: f32, 36 | delta: f32, 37 | } 38 | 39 | impl HighPassIIR { 40 | pub fn new(sample_rate: f32, cutoff_frequency: f32) -> HighPassIIR { 41 | let delta_t = 1.0 / sample_rate; 42 | let time_constant = 1.0 / cutoff_frequency; 43 | let alpha = time_constant / (time_constant + delta_t); 44 | return HighPassIIR { 45 | alpha: alpha, 46 | previous_output: 0.0, 47 | previous_input: 0.0, 48 | delta: 0.0, 49 | } 50 | } 51 | } 52 | 53 | impl DspFilter for HighPassIIR { 54 | fn consume(&mut self, new_input: f32) { 55 | self.previous_output = self.output(); 56 | self.delta = new_input - self.previous_input; 57 | self.previous_input = new_input; 58 | } 59 | 60 | fn output(&self) -> f32 { 61 | return self.alpha * self.previous_output + self.alpha * self.delta; 62 | } 63 | } 64 | 65 | pub struct LowPassIIR { 66 | alpha: f32, 67 | previous_output: f32, 68 | delta: f32, 69 | } 70 | 71 | impl LowPassIIR { 72 | pub fn new(sample_rate: f32, cutoff_frequency: f32) -> LowPassIIR { 73 | let delta_t = 1.0 / sample_rate; 74 | let time_constant = 1.0 / (2.0 * PI * cutoff_frequency); 75 | let alpha = delta_t / (time_constant + delta_t); 76 | return LowPassIIR { 77 | alpha: alpha, 78 | previous_output: 0.0, 79 | delta: 0.0, 80 | } 81 | } 82 | } 83 | 84 | impl DspFilter for LowPassIIR { 85 | fn consume(&mut self, new_input: f32) { 86 | self.previous_output = self.output(); 87 | self.delta = new_input - self.previous_output; 88 | } 89 | 90 | fn output(&self) -> f32 { 91 | return self.previous_output + self.alpha * self.delta; 92 | } 93 | } 94 | 95 | fn blackman_window(index: usize, window_size: usize) -> f32 { 96 | let i = index as f32; 97 | let M = window_size as f32; 98 | return 0.42 - 0.5 * ((2.0 * PI * i) / M).cos() + 0.08 * ((4.0 * PI * i) / M).cos(); 99 | } 100 | 101 | fn sinc(index: usize, window_size: usize, fc: f32) -> f32 { 102 | let i = index as f32; 103 | let M = window_size as f32; 104 | let shifted_index = i - (M / 2.0); 105 | if index == (window_size / 2) { 106 | return 2.0 * PI * fc; 107 | } else { 108 | return (2.0 * PI * fc * shifted_index).sin() / shifted_index; 109 | } 110 | } 111 | 112 | fn normalize(input: Vec) -> Vec { 113 | let sum: f32 = input.clone().into_iter().sum(); 114 | let output = input.into_iter().map(|x| x / sum).collect(); 115 | return output; 116 | } 117 | 118 | fn windowed_sinc_kernel(fc: f32, window_size: usize) -> Vec { 119 | let mut kernel: Vec = Vec::new(); 120 | for i in 0 ..= window_size { 121 | kernel.push(sinc(i, window_size, fc) * blackman_window(i, window_size)); 122 | } 123 | return normalize(kernel); 124 | } 125 | 126 | pub struct LowPassFIR { 127 | kernel: Vec, 128 | inputs: Vec, 129 | input_index: usize, 130 | } 131 | 132 | impl LowPassFIR { 133 | pub fn new(sample_rate: f32, cutoff_frequency: f32, window_size: usize) -> LowPassFIR { 134 | let fc = cutoff_frequency / sample_rate; 135 | let kernel = windowed_sinc_kernel(fc, window_size); 136 | let mut inputs = Vec::new(); 137 | inputs.resize(window_size + 1, 0.0); 138 | 139 | return LowPassFIR { 140 | kernel: kernel, 141 | inputs: inputs, 142 | input_index: 0, 143 | } 144 | } 145 | } 146 | 147 | impl DspFilter for LowPassFIR { 148 | fn consume(&mut self, new_input: f32) { 149 | self.inputs[self.input_index] = new_input; 150 | self.input_index = (self.input_index + 1) % self.inputs.len(); 151 | } 152 | 153 | fn output(&self) -> f32 { 154 | let mut output: f32 = 0.0; 155 | for i in 0 .. self.inputs.len() { 156 | let buffer_index = (self.input_index + i) % self.inputs.len(); 157 | output += self.kernel[i] * self.inputs[buffer_index]; 158 | } 159 | return output; 160 | } 161 | } 162 | 163 | // essentially a thin wrapper around a DspFilter, with some bonus data to track 164 | // state when used in a larger chain 165 | pub struct ChainedFilter { 166 | wrapped_filter: Box, 167 | sampling_period: f32, 168 | period_counter: f32, 169 | } 170 | 171 | pub struct FilterChain { 172 | filters: Vec, 173 | } 174 | 175 | impl FilterChain { 176 | pub fn new() -> FilterChain { 177 | let identity = IdentityFilter::new(); 178 | return FilterChain { 179 | filters: vec![ChainedFilter{ 180 | wrapped_filter: Box::new(identity), 181 | sampling_period: 1.0, 182 | period_counter: 0.0, 183 | }], 184 | } 185 | } 186 | 187 | pub fn add(&mut self, filter: Box, sample_rate: f32) { 188 | self.filters.push(ChainedFilter { 189 | wrapped_filter: filter, 190 | sampling_period: (1.0 / sample_rate), 191 | period_counter: 0.0 192 | }); 193 | } 194 | 195 | pub fn consume(&mut self, input_sample: f32, delta_time: f32) { 196 | // Always advance the identity filter with the new current sample 197 | self.filters[0].wrapped_filter.consume(input_sample); 198 | // Now for every remaining filter in the chain, advance and sample the previous 199 | // filter as required 200 | for i in 1 .. self.filters.len() { 201 | let previous = i - 1; 202 | let current = i; 203 | self.filters[current].period_counter += delta_time; 204 | while self.filters[current].period_counter >= self.filters[current].sampling_period { 205 | self.filters[current].period_counter -= self.filters[current].sampling_period; 206 | let previous_output = self.filters[previous].wrapped_filter.output(); 207 | self.filters[current].wrapped_filter.consume(previous_output); 208 | } 209 | } 210 | } 211 | 212 | pub fn output(&self) -> f32 { 213 | let final_filter = self.filters.last().unwrap(); 214 | return final_filter.wrapped_filter.output(); 215 | } 216 | } -------------------------------------------------------------------------------- /external/rusticnes-ui-common/src/game_window.rs: -------------------------------------------------------------------------------- 1 | use application::RuntimeState; 2 | use drawing; 3 | use drawing::Color; 4 | use drawing::Font; 5 | use drawing::SimpleBuffer; 6 | use events::Event; 7 | use panel::Panel; 8 | 9 | use std::time::Instant; 10 | 11 | use rusticnes_core::nes::NesState; 12 | use rusticnes_core::palettes::NTSC_PAL; 13 | 14 | pub struct GameWindow { 15 | pub canvas: SimpleBuffer, 16 | pub font: Font, 17 | pub shown: bool, 18 | pub scale: u32, 19 | pub simulate_overscan: bool, 20 | pub ntsc_filter: bool, 21 | pub display_fps: bool, 22 | 23 | pub frame_duration: Instant, 24 | pub durations: [f32; 60], 25 | pub duration_index: usize, 26 | pub measured_fps: f32, 27 | } 28 | 29 | impl GameWindow { 30 | pub fn new() -> GameWindow { 31 | let font = Font::from_raw(include_bytes!("assets/8x8_font.png"), 8); 32 | 33 | return GameWindow { 34 | canvas: SimpleBuffer::new(240, 224), 35 | font: font, 36 | shown: true, 37 | scale: 2, 38 | simulate_overscan: false, 39 | ntsc_filter: false, 40 | display_fps: false, 41 | 42 | frame_duration: Instant::now(), 43 | durations: [0f32; 60], 44 | duration_index: 0, 45 | measured_fps: 0.0, 46 | }; 47 | } 48 | 49 | fn update_fps(&mut self) { 50 | let time_since_last = self.frame_duration.elapsed().as_millis() as f32; 51 | self.frame_duration = Instant::now(); 52 | self.durations[self.duration_index] = time_since_last; 53 | self.duration_index = (self.duration_index + 1) % 60; 54 | let average_frame_duration_millis = self.durations.iter().sum::() as f32 / (self.durations.len() as f32); 55 | if average_frame_duration_millis > 0.0 { 56 | self.measured_fps = 1000.0 / average_frame_duration_millis; 57 | } 58 | } 59 | 60 | fn draw(&mut self, nes: &NesState) { 61 | let overscan: u32 = if self.simulate_overscan {8} else {0}; 62 | 63 | // Update the game screen 64 | for x in overscan .. 256 - overscan { 65 | for y in overscan .. 240 - overscan { 66 | if self.ntsc_filter { 67 | let scale = self.scale; 68 | let base_x = x * scale; 69 | let base_y = y * 256 * scale; 70 | 71 | for sx in 0 .. self.scale { 72 | let column_color = Color::from_raw(nes.ppu.filtered_screen[(base_y + base_x + sx) as usize]); 73 | for sy in 0 .. self.scale { 74 | self.canvas.put_pixel((x - overscan) * scale + sx, (y - overscan) * scale + sy, column_color); 75 | } 76 | } 77 | } else { 78 | let palette_index = ((nes.ppu.screen[(y * 256 + x) as usize]) as usize) * 3; 79 | self.canvas.put_pixel( 80 | x - overscan, 81 | y - overscan, 82 | Color::rgb( 83 | NTSC_PAL[palette_index + 0], 84 | NTSC_PAL[palette_index + 1], 85 | NTSC_PAL[palette_index + 2]) 86 | ); 87 | } 88 | } 89 | } 90 | 91 | if self.display_fps { 92 | let fps_display = format!("FPS: {:.2}", self.measured_fps); 93 | drawing::text(&mut self.canvas, &self.font, 5, 5, &fps_display, Color::rgba(255, 255, 255, 192)); 94 | } 95 | } 96 | 97 | fn increase_scale(&mut self) { 98 | if self.scale < 8 { 99 | self.scale += 1; 100 | } 101 | self.update_canvas_size(); 102 | } 103 | 104 | fn decrease_scale(&mut self) { 105 | if self.scale > 1 { 106 | self.scale -= 1; 107 | } 108 | self.update_canvas_size(); 109 | } 110 | 111 | fn update_canvas_size(&mut self) { 112 | let base_width = if self.simulate_overscan {240} else {256}; 113 | let base_height = if self.simulate_overscan {224} else {240}; 114 | let scaled_width = if self.ntsc_filter {base_width * self.scale} else {base_width}; 115 | let scaled_height = if self.ntsc_filter {base_height * self.scale} else {base_height}; 116 | self.canvas = SimpleBuffer::new(scaled_width, scaled_height); 117 | } 118 | } 119 | 120 | impl Panel for GameWindow { 121 | fn title(&self) -> &str { 122 | return "RusticNES"; 123 | } 124 | 125 | fn shown(&self) -> bool { 126 | return self.shown; 127 | } 128 | 129 | fn handle_event(&mut self, runtime: &RuntimeState, event: Event) -> Vec { 130 | let mut responses = Vec::::new(); 131 | match event { 132 | Event::RequestFrame => { 133 | self.update_fps(); 134 | self.draw(&runtime.nes); 135 | // Technically this will have us drawing one frame behind the filter. To fix 136 | // this, we'd need Application to manage filters instead. 137 | if self.ntsc_filter { 138 | responses.push(Event::NesRenderNTSC(256 * (self.scale as usize))); 139 | } 140 | }, 141 | Event::ShowGameWindow => {self.shown = true}, 142 | Event::CloseWindow => {self.shown = false}, 143 | 144 | Event::GameIncreaseScale => { 145 | self.increase_scale(); 146 | responses.push(Event::StoreIntegerSetting("video.scale_factor".to_string(), self.scale as i64)); 147 | }, 148 | Event::GameDecreaseScale => { 149 | self.decrease_scale(); 150 | responses.push(Event::StoreIntegerSetting("video.scale_factor".to_string(), self.scale as i64)); 151 | }, 152 | 153 | Event::ApplyBooleanSetting(path, value) => { 154 | match path.as_str() { 155 | "video.display_fps" => {self.display_fps = value}, 156 | "video.ntsc_filter" => {self.ntsc_filter = value; self.update_canvas_size()}, 157 | "video.simulate_overscan" => {self.simulate_overscan = value; self.update_canvas_size()}, 158 | _ => {} 159 | } 160 | }, 161 | Event::ApplyIntegerSetting(path, value) => { 162 | match path.as_str() { 163 | "video.scale_factor" => { 164 | if value > 0 && value < 8 { 165 | self.scale = value as u32; 166 | self.update_canvas_size(); 167 | } 168 | }, 169 | _ => {} 170 | } 171 | }, 172 | _ => {} 173 | } 174 | return responses; 175 | } 176 | 177 | fn active_canvas(&self) -> &SimpleBuffer { 178 | return &self.canvas; 179 | } 180 | 181 | fn scale_factor(&self) -> u32 { 182 | if self.ntsc_filter { 183 | return 1; // we handle scale in software 184 | } else { 185 | return self.scale; 186 | } 187 | } 188 | } -------------------------------------------------------------------------------- /external/rusticnes-core/src/apu/dmc.rs: -------------------------------------------------------------------------------- 1 | use mmc::mapper::Mapper; 2 | use super::audio_channel::AudioChannelState; 3 | use super::ring_buffer::RingBuffer; 4 | use super::filters; 5 | use super::filters::DspFilter; 6 | 7 | pub struct DmcState { 8 | pub name: String, 9 | pub chip: String, 10 | pub debug_disable: bool, 11 | pub output_buffer: RingBuffer, 12 | pub edge_buffer: RingBuffer, 13 | pub last_edge: bool, 14 | pub debug_filter: filters::HighPassIIR, 15 | 16 | pub looping: bool, 17 | pub period_initial: u16, 18 | pub period_current: u16, 19 | pub output_level: u8, 20 | pub starting_address: u16, 21 | pub sample_length: u16, 22 | 23 | pub current_address: u16, 24 | pub sample_buffer: u8, 25 | pub shift_register: u8, 26 | pub sample_buffer_empty: bool, 27 | pub bits_remaining: u8, 28 | pub bytes_remaining: u16, 29 | pub silence_flag: bool, 30 | 31 | pub interrupt_enabled: bool, 32 | pub interrupt_flag: bool, 33 | pub rdy_line: bool, 34 | pub rdy_delay: u8, 35 | } 36 | 37 | impl DmcState { 38 | pub fn new(channel_name: &str, chip_name: &str) -> DmcState { 39 | return DmcState { 40 | name: String::from(channel_name), 41 | chip: String::from(chip_name), 42 | debug_disable: false, 43 | output_buffer: RingBuffer::new(32768), 44 | edge_buffer: RingBuffer::new(32768), 45 | last_edge: false, 46 | debug_filter: filters::HighPassIIR::new(44100.0, 300.0), 47 | 48 | looping: false, 49 | period_initial: 428, 50 | period_current: 0, 51 | output_level: 0, 52 | starting_address: 0, 53 | sample_length: 0, 54 | 55 | current_address: 0, 56 | sample_buffer: 0, 57 | shift_register: 0, 58 | sample_buffer_empty: true, 59 | bits_remaining: 8, 60 | bytes_remaining: 0, 61 | silence_flag: false, 62 | interrupt_enabled: true, 63 | interrupt_flag: false, 64 | rdy_line: false, 65 | rdy_delay: 0, 66 | } 67 | } 68 | 69 | pub fn debug_status(&self) -> String { 70 | return format!("Rate: {:3} - Divisor: {:3} - Start: {:04X} - Current: {:04X} - Length: {:4} - R.Bytes: {:4} - R.Bits: {:1}", 71 | self.period_initial, self.period_current, self.starting_address, self.current_address, self.sample_length, 72 | self.bytes_remaining, self.bits_remaining); 73 | } 74 | 75 | pub fn read_next_sample(&mut self, mapper: &mut dyn Mapper) { 76 | match mapper.read_cpu(0x8000 | (self.current_address & 0x7FFF)) { 77 | Some(byte) => self.sample_buffer = byte, 78 | None => self.sample_buffer = 0, 79 | } 80 | self.current_address = self.current_address.wrapping_add(1); 81 | self.bytes_remaining -= 1; 82 | if self.bytes_remaining == 0 { 83 | if self.looping { 84 | self.current_address = self.starting_address; 85 | self.bytes_remaining = self.sample_length; 86 | self.last_edge = true; 87 | } else { 88 | if self.interrupt_enabled { 89 | self.interrupt_flag = true; 90 | } 91 | } 92 | } 93 | self.sample_buffer_empty = false; 94 | self.rdy_line = false; 95 | self.rdy_delay = 0; 96 | } 97 | 98 | pub fn begin_output_cycle(&mut self) { 99 | self.bits_remaining = 8; 100 | if self.sample_buffer_empty { 101 | self.silence_flag = true; 102 | } else { 103 | self.silence_flag = false; 104 | self.shift_register = self.sample_buffer; 105 | self.sample_buffer_empty = true; 106 | } 107 | } 108 | 109 | pub fn update_output_unit(&mut self) { 110 | if !(self.silence_flag) { 111 | let mut target_output = self.output_level; 112 | if (self.shift_register & 0b1) == 0 { 113 | if self.output_level >= 2 { 114 | target_output -= 2; 115 | } 116 | } else { 117 | if self.output_level <= 125 { 118 | target_output += 2; 119 | } 120 | } 121 | self.output_level = target_output; 122 | } 123 | self.shift_register = self.shift_register >> 1; 124 | self.bits_remaining -= 1; 125 | if self.bits_remaining == 0 { 126 | self.begin_output_cycle(); 127 | } 128 | } 129 | 130 | pub fn clock(&mut self, mapper: &mut dyn Mapper) { 131 | if self.period_current == 0 { 132 | self.period_current = self.period_initial - 1; 133 | self.update_output_unit(); 134 | } else { 135 | self.period_current -= 1; 136 | } 137 | if self.sample_buffer_empty && self.bytes_remaining > 0 { 138 | self.rdy_line = true; 139 | self.rdy_delay += 1; 140 | if self.rdy_delay > 2 { 141 | self.read_next_sample(mapper); 142 | } 143 | } else { 144 | self.rdy_line = false; 145 | self.rdy_delay = 0; 146 | } 147 | } 148 | 149 | pub fn output(&self) -> i16 { 150 | return self.output_level as i16; 151 | } 152 | } 153 | 154 | impl AudioChannelState for DmcState { 155 | fn name(&self) -> String { 156 | return self.name.clone(); 157 | } 158 | 159 | fn chip(&self) -> String { 160 | return self.chip.clone(); 161 | } 162 | 163 | fn edge_buffer(&self) -> &RingBuffer { 164 | return &self.edge_buffer; 165 | } 166 | 167 | fn sample_buffer(&self) -> &RingBuffer { 168 | return &self.output_buffer; 169 | } 170 | 171 | fn record_current_output(&mut self) { 172 | self.debug_filter.consume(self.output() as f32); 173 | self.output_buffer.push((self.debug_filter.output() * -4.0) as i16); 174 | self.edge_buffer.push(self.last_edge as i16); 175 | self.last_edge = false; 176 | } 177 | 178 | fn min_sample(&self) -> i16 { 179 | return -512; 180 | } 181 | 182 | fn max_sample(&self) -> i16 { 183 | return 512; 184 | } 185 | 186 | fn muted(&self) -> bool { 187 | return self.debug_disable; 188 | } 189 | 190 | fn mute(&mut self) { 191 | self.debug_disable = true; 192 | } 193 | 194 | fn unmute(&mut self) { 195 | self.debug_disable = false; 196 | } 197 | 198 | fn playing(&self) -> bool { 199 | return self.amplitude() > 0.0; 200 | } 201 | 202 | fn amplitude(&self) -> f32 { 203 | let buffer = self.output_buffer.buffer(); 204 | let mut index = (self.output_buffer.index() - 256) % buffer.len(); 205 | let mut max = buffer[index]; 206 | let mut min = buffer[index]; 207 | for _i in 0 .. 256 { 208 | if buffer[index] > max {max = buffer[index];} 209 | if buffer[index] < min {min = buffer[index];} 210 | index += 1; 211 | index = index % buffer.len(); 212 | } 213 | return (max - min) as f32 / 256.0; 214 | } 215 | } -------------------------------------------------------------------------------- /src/gui/slint/channel-config.slint: -------------------------------------------------------------------------------- 1 | import { VerticalBox, ComboBox, Switch, StandardButton, Button } from "std-widgets.slint"; 2 | import { ColorPicker } from "./color-picker.slint"; 3 | 4 | export struct ChannelConfig { 5 | name: string, 6 | hidden: bool, 7 | colors: [[int]] 8 | } 9 | 10 | component ChannelConfigRow { 11 | in property config; 12 | in property enabled: true; 13 | 14 | property i-config; 15 | 16 | callback updated(ChannelConfig); 17 | 18 | HorizontalLayout { 19 | alignment: stretch; 20 | 21 | Switch { 22 | text: root.config.name; 23 | checked: !root.config.hidden; 24 | enabled: root.enabled; 25 | width: 150px; 26 | 27 | toggled => { 28 | root.i-config = root.config; 29 | root.i-config.hidden = !root.i-config.hidden; 30 | root.updated(root.i-config); 31 | } 32 | } 33 | 34 | for color[i] in config.colors: Rectangle { 35 | Rectangle { 36 | background: (root.config.hidden || !root.enabled) ? root.grayscale(color) : Colors.rgb(color[0], color[1], color[2]); 37 | x: 2px; 38 | y: 2px; 39 | height: parent.height - 4px; 40 | width: parent.width - 4px; 41 | border-radius: 4px; 42 | } 43 | 44 | i-popup := PopupWindow { 45 | width: 350px; 46 | close-on-click: false; 47 | 48 | Rectangle { 49 | height: 100%; 50 | width: 100%; 51 | background: #1c1c1c; 52 | border-radius: 2px; 53 | } 54 | VerticalBox { 55 | alignment: start; 56 | ColorPicker { 57 | width: 350px; 58 | r: color[0]; 59 | g: color[1]; 60 | b: color[2]; 61 | changed(r, g, b) => { 62 | root.i-config = root.config; 63 | root.i-config.colors[i] = [r, g, b]; 64 | root.updated(root.i-config); 65 | } 66 | } 67 | StandardButton { 68 | kind: ok; 69 | clicked => { 70 | i-popup.close(); 71 | } 72 | } 73 | } 74 | } 75 | i-touch := TouchArea { 76 | mouse-cursor: (root.config.hidden || !root.enabled) ? default : pointer; 77 | clicked => { 78 | if (!root.config.hidden && root.enabled) { 79 | i-popup.show(); 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | function luma-gray(c: [int]) -> int { 87 | return Math.round(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]); 88 | } 89 | 90 | function grayscale(c: [int]) -> color { 91 | return Colors.rgb(luma-gray(c), luma-gray(c), luma-gray(c)); 92 | } 93 | } 94 | 95 | export component ChannelConfigView { 96 | in-out property<[ChannelConfig]> config-2a03; 97 | in-out property<[ChannelConfig]> config-mmc5; 98 | in-out property<[ChannelConfig]> config-n163; 99 | in-out property<[ChannelConfig]> config-vrc6; 100 | in-out property<[ChannelConfig]> config-vrc7; 101 | in-out property<[ChannelConfig]> config-s5b; 102 | in-out property<[ChannelConfig]> config-fds; 103 | in-out property<[ChannelConfig]> config-apu; 104 | 105 | in property<[string]> active-chips: []; 106 | in property enabled: true; 107 | 108 | VerticalBox { 109 | alignment: start; 110 | padding: 0; 111 | 112 | HorizontalLayout { 113 | alignment: stretch; 114 | spacing: 8px; 115 | Text { 116 | text: "Configure chip:"; 117 | vertical-alignment: center; 118 | } 119 | i-chip-select := ComboBox { 120 | model: root.active-chips; 121 | enabled: root.enabled; 122 | } 123 | } 124 | if i-chip-select.current-value == "2A03": VerticalBox { 125 | alignment: start; 126 | 127 | for config[i] in config-2a03: ChannelConfigRow { 128 | config: config; 129 | enabled: root.enabled; 130 | updated(new-config) => { 131 | root.config-2a03[i] = new-config; 132 | } 133 | } 134 | } 135 | if i-chip-select.current-value == "MMC5": VerticalBox { 136 | alignment: start; 137 | 138 | for config[i] in config-mmc5: ChannelConfigRow { 139 | config: config; 140 | enabled: root.enabled; 141 | updated(new-config) => { 142 | root.config-mmc5[i] = new-config; 143 | } 144 | } 145 | } 146 | if i-chip-select.current-value == "N163": VerticalBox { 147 | alignment: start; 148 | 149 | for config[i] in config-n163: ChannelConfigRow { 150 | config: config; 151 | enabled: root.enabled; 152 | updated(new-config) => { 153 | root.config-n163[i] = new-config; 154 | } 155 | } 156 | } 157 | if i-chip-select.current-value == "VRC6": VerticalBox { 158 | alignment: start; 159 | 160 | for config[i] in config-vrc6: ChannelConfigRow { 161 | config: config; 162 | enabled: root.enabled; 163 | updated(new-config) => { 164 | root.config-vrc6[i] = new-config; 165 | } 166 | } 167 | } 168 | if i-chip-select.current-value == "VRC7": VerticalBox { 169 | alignment: start; 170 | 171 | for config[i] in config-vrc7: ChannelConfigRow { 172 | config: config; 173 | enabled: root.enabled; 174 | updated(new-config) => { 175 | root.config-vrc7[i] = new-config; 176 | } 177 | } 178 | } 179 | if i-chip-select.current-value == "S5B": VerticalBox { 180 | alignment: start; 181 | 182 | for config[i] in config-s5b: ChannelConfigRow { 183 | config: config; 184 | enabled: root.enabled; 185 | updated(new-config) => { 186 | root.config-s5b[i] = new-config; 187 | } 188 | } 189 | } 190 | if i-chip-select.current-value == "FDS": VerticalBox { 191 | alignment: start; 192 | 193 | for config[i] in config-fds: ChannelConfigRow { 194 | config: config; 195 | enabled: root.enabled; 196 | updated(new-config) => { 197 | root.config-fds[i] = new-config; 198 | } 199 | } 200 | } 201 | if i-chip-select.current-value == "APU": VerticalBox { 202 | alignment: start; 203 | 204 | for config[i] in config-apu: ChannelConfigRow { 205 | config: config; 206 | enabled: root.enabled; 207 | updated(new-config) => { 208 | root.config-apu[i] = new-config; 209 | } 210 | } 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/mmc/action53.rs: -------------------------------------------------------------------------------- 1 | // A very simple Mapper with no esoteric features or bank switching. 2 | // Reference capabilities: https://wiki.nesdev.com/w/index.php/NROM 3 | 4 | use ines::INesCartridge; 5 | use memoryblock::MemoryBlock; 6 | 7 | use mmc::mapper::*; 8 | use mmc::mirroring; 9 | 10 | pub struct Action53 { 11 | prg_rom: MemoryBlock, 12 | prg_ram: MemoryBlock, 13 | chr: MemoryBlock, 14 | vram: Vec, 15 | 16 | register_select: u8, 17 | mirroring_mode: u8, 18 | chr_ram_a13_a14: usize, 19 | prg_inner_bank: usize, 20 | prg_outer_bank: usize, 21 | prg_mode: u8, 22 | prg_outer_bank_size: usize, 23 | } 24 | 25 | impl Action53 { 26 | pub fn from_ines(ines: INesCartridge) -> Result { 27 | let prg_rom_block = ines.prg_rom_block(); 28 | let prg_ram_block = ines.prg_ram_block()?; 29 | let chr_block = ines.chr_block()?; 30 | 31 | return Ok(Action53 { 32 | prg_rom: prg_rom_block.clone(), 33 | prg_ram: prg_ram_block.clone(), 34 | chr: chr_block.clone(), 35 | vram: vec![0u8; 0x1000], 36 | register_select: 0, 37 | mirroring_mode: 0, 38 | chr_ram_a13_a14: 0, 39 | prg_inner_bank: 0xFF, 40 | prg_outer_bank: 0xFF, 41 | prg_mode: 0, 42 | prg_outer_bank_size: 0, 43 | }); 44 | } 45 | 46 | fn prg_address(&self, cpu_address: u16) -> usize { 47 | let cpu_a14 = ((cpu_address & 0b0100_0000_0000_0000) >> 14) as usize; 48 | let bits_a22_to_a14: usize = match self.prg_mode { 49 | 0 | 1 => match self.prg_outer_bank_size { 50 | 0 => (self.prg_outer_bank << 1) | cpu_a14, 51 | 1 => ((self.prg_outer_bank & 0b1111_1110) << 1) | ((self.prg_inner_bank & 0b0000_0001) << 1) | cpu_a14, 52 | 2 => ((self.prg_outer_bank & 0b1111_1100) << 1) | ((self.prg_inner_bank & 0b0000_0011) << 1) | cpu_a14, 53 | 3 => ((self.prg_outer_bank & 0b1111_1000) << 1) | ((self.prg_inner_bank & 0b0000_0111) << 1) | cpu_a14, 54 | _ => 0 // unreachable 55 | }, 56 | 2 => match cpu_a14 { 57 | // $8000 58 | 0 => self.prg_outer_bank << 1, 59 | // $C000 60 | 1 => { 61 | let outer_bitmask = (0b1_1111_1110 << self.prg_outer_bank_size) & 0b1_1111_1110; 62 | let inner_bitmask = 0b0_0000_1111 >> (3 - self.prg_outer_bank_size); 63 | ((self.prg_outer_bank << 1) & outer_bitmask) | (self.prg_inner_bank & inner_bitmask) 64 | }, 65 | _ => 0 // unreachable 66 | }, 67 | 3 => match cpu_a14 { 68 | // $8000 69 | 0 => { 70 | let outer_bitmask = (0b1_1111_1110 << self.prg_outer_bank_size) & 0b1_1111_1110; 71 | let inner_bitmask = 0b0_0000_1111 >> (3 - self.prg_outer_bank_size); 72 | ((self.prg_outer_bank << 1) & outer_bitmask) | (self.prg_inner_bank & inner_bitmask) 73 | } 74 | // $C000 75 | 1 => self.prg_outer_bank << 1 | 1, 76 | _ => 0 // unreachable 77 | }, 78 | _ => 0 // unreachable 79 | }; 80 | return (bits_a22_to_a14 << 14) | ((cpu_address & 0b0011_1111_1111_1111) as usize); 81 | } 82 | 83 | fn chr_address(&self, ppu_address: u16) -> usize { 84 | return (self.chr_ram_a13_a14 << 13) | ((ppu_address & 0b1_1111_1111_1111) as usize); 85 | } 86 | } 87 | 88 | impl Mapper for Action53 { 89 | fn mirroring(&self) -> Mirroring { 90 | match self.mirroring_mode { 91 | 0 => Mirroring::OneScreenLower, 92 | 1 => Mirroring::OneScreenUpper, 93 | 2 => Mirroring::Vertical, 94 | 3 => Mirroring::Horizontal, 95 | _ => Mirroring::Horizontal // unreachable 96 | } 97 | } 98 | 99 | fn debug_read_cpu(&self, address: u16) -> Option { 100 | match address { 101 | 0x6000 ..= 0x7FFF => {self.prg_ram.wrapping_read((address - 0x6000) as usize)}, 102 | 0x8000 ..= 0xFFFF => { 103 | self.prg_rom.wrapping_read(self.prg_address(address)) 104 | }, 105 | _ => None 106 | } 107 | } 108 | 109 | fn write_cpu(&mut self, address: u16, data: u8) { 110 | match address { 111 | 0x5000 ..= 0x5FFF => {self.register_select = data & 0x81;}, 112 | 0x6000 ..= 0x7FFF => {self.prg_ram.wrapping_write((address - 0x6000) as usize, data);}, 113 | 0x8000 ..= 0xFFFF => { 114 | match self.register_select { 115 | 0x00 => { 116 | if (self.mirroring_mode & 0b10) == 0 { 117 | let mirroring_mode_bit_0: u8 = (data & 0b0001_0000) >> 4; 118 | self.mirroring_mode = (self.mirroring_mode & 0b10) | mirroring_mode_bit_0; 119 | } 120 | self.chr_ram_a13_a14 = (data & 0b0000_0011) as usize; 121 | }, 122 | 0x01 => { 123 | if (self.mirroring_mode & 0b10) == 0 { 124 | let mirroring_mode_bit_0: u8 = (data & 0b0001_0000) >> 4; 125 | self.mirroring_mode = (self.mirroring_mode & 0b10) | mirroring_mode_bit_0; 126 | } 127 | self.prg_inner_bank = (data & 0b0000_1111) as usize; 128 | }, 129 | 0x80 => { 130 | self.mirroring_mode = data & 0b0000_0011; 131 | self.prg_mode = (data & 0b0000_1100) >> 2; 132 | self.prg_outer_bank_size = ((data & 0b0011_0000) >> 4) as usize; 133 | }, 134 | 0x81 => { 135 | self.prg_outer_bank = data as usize; 136 | }, 137 | _ => {/* never reached */} 138 | } 139 | } 140 | _ => {} 141 | } 142 | } 143 | 144 | fn debug_read_ppu(&self, address: u16) -> Option { 145 | match address { 146 | 0x0000 ..= 0x1FFF => {return self.chr.wrapping_read(self.chr_address(address))}, 147 | 0x2000 ..= 0x3FFF => return match self.mirroring() { 148 | Mirroring::Horizontal => Some(self.vram[mirroring::horizontal_mirroring(address) as usize]), 149 | Mirroring::Vertical => Some(self.vram[mirroring::vertical_mirroring(address) as usize]), 150 | Mirroring::OneScreenLower => Some(self.vram[mirroring::one_screen_lower(address) as usize]), 151 | Mirroring::OneScreenUpper => Some(self.vram[mirroring::one_screen_upper(address) as usize]), 152 | _ => None 153 | }, 154 | _ => return None 155 | } 156 | } 157 | 158 | fn write_ppu(&mut self, address: u16, data: u8) { 159 | match address { 160 | 0x0000 ..= 0x1FFF => {self.chr.wrapping_write(self.chr_address(address), data);}, 161 | 0x2000 ..= 0x3FFF => match self.mirroring() { 162 | Mirroring::Horizontal => self.vram[mirroring::horizontal_mirroring(address) as usize] = data, 163 | Mirroring::Vertical => self.vram[mirroring::vertical_mirroring(address) as usize] = data, 164 | Mirroring::OneScreenLower => self.vram[mirroring::one_screen_lower(address) as usize] = data, 165 | Mirroring::OneScreenUpper => self.vram[mirroring::one_screen_upper(address) as usize] = data, 166 | _ => {} 167 | }, 168 | _ => {} 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /external/rusticnes-core/src/apu/pulse.rs: -------------------------------------------------------------------------------- 1 | use super::length_counter::LengthCounterState; 2 | use super::volume_envelope::VolumeEnvelopeState; 3 | use super::audio_channel::AudioChannelState; 4 | use super::audio_channel::PlaybackRate; 5 | use super::audio_channel::Volume; 6 | use super::audio_channel::Timbre; 7 | use super::ring_buffer::RingBuffer; 8 | use super::filters; 9 | use super::filters::DspFilter; 10 | 11 | pub struct PulseChannelState { 12 | pub name: String, 13 | pub chip: String, 14 | pub debug_disable: bool, 15 | pub output_buffer: RingBuffer, 16 | pub edge_buffer: RingBuffer, 17 | pub last_edge: bool, 18 | pub debug_filter: filters::HighPassIIR, 19 | pub envelope: VolumeEnvelopeState, 20 | pub length_counter: LengthCounterState, 21 | 22 | // Frequency Sweep 23 | pub sweep_enabled: bool, 24 | pub sweep_period: u8, 25 | pub sweep_divider: u8, 26 | pub sweep_negate: bool, 27 | pub sweep_shift: u8, 28 | pub sweep_reload: bool, 29 | // Variance between Pulse 1 and Pulse 2 causes negation to work slightly differently 30 | pub sweep_ones_compliment: bool, 31 | 32 | pub duty: u8, 33 | pub sequence_counter: u8, 34 | pub period_initial: u16, 35 | pub period_current: u16, 36 | 37 | pub cpu_clock_rate: u64, 38 | } 39 | 40 | impl PulseChannelState { 41 | pub fn new(channel_name: &str, chip_name: &str, cpu_clock_rate: u64, sweep_ones_compliment: bool) -> PulseChannelState { 42 | return PulseChannelState { 43 | name: String::from(channel_name), 44 | chip: String::from(chip_name), 45 | debug_disable: false, 46 | output_buffer: RingBuffer::new(32768), 47 | edge_buffer: RingBuffer::new(32768), 48 | last_edge: false, 49 | debug_filter: filters::HighPassIIR::new(44100.0, 300.0), // for visual flair, and also to remove DC offset 50 | 51 | envelope: VolumeEnvelopeState::new(), 52 | length_counter: LengthCounterState::new(), 53 | 54 | // Frequency Sweep 55 | sweep_enabled: false, 56 | sweep_period: 0, 57 | sweep_divider: 0, 58 | sweep_negate: false, 59 | sweep_shift: 0, 60 | sweep_reload: false, 61 | // Variance between Pulse 1 and Pulse 2 causes negation to work slightly differently 62 | sweep_ones_compliment: sweep_ones_compliment, 63 | 64 | duty: 0b0000_0001, 65 | sequence_counter: 0, 66 | period_initial: 0, 67 | period_current: 0, 68 | cpu_clock_rate: cpu_clock_rate, 69 | } 70 | } 71 | 72 | pub fn clock(&mut self) { 73 | if self.period_current == 0 { 74 | // Reset the period timer, and clock the waveform generator 75 | self.period_current = self.period_initial; 76 | 77 | // The sequence counter starts at zero, but counts downwards, resulting in an odd 78 | // lookup sequence of 0, 7, 6, 5, 4, 3, 2, 1 79 | if self.sequence_counter == 0 { 80 | self.sequence_counter = 7; 81 | self.last_edge = true; 82 | } else { 83 | self.sequence_counter -= 1; 84 | } 85 | } else { 86 | self.period_current -= 1; 87 | } 88 | } 89 | 90 | pub fn output(&self) -> i16 { 91 | if self.length_counter.length > 0 { 92 | let target_period = self.target_period(); 93 | if target_period > 0x7FF || self.period_initial < 8 { 94 | // Sweep unit mutes the channel, because the period is out of range 95 | return 0; 96 | } else { 97 | let mut sample = ((self.duty >> self.sequence_counter) & 0b1) as i16; 98 | sample *= self.envelope.current_volume() as i16; 99 | return sample; 100 | } 101 | } else { 102 | return 0; 103 | } 104 | } 105 | 106 | pub fn target_period(&self) -> u16 { 107 | let change_amount = self.period_initial >> self.sweep_shift; 108 | if self.sweep_negate { 109 | if self.sweep_ones_compliment { 110 | if self.sweep_shift == 0 || self.period_initial == 0 { 111 | // Special case: in one's compliment mode, this would overflow to 112 | // 0xFFFF, but that's not what real hardware appears to do. This solves 113 | // a muting bug with negate-mode sweep on Pulse 1 in some publishers 114 | // games. 115 | return 0; 116 | } 117 | return self.period_initial - change_amount - 1; 118 | } else { 119 | return self.period_initial - change_amount; 120 | } 121 | } else { 122 | return self.period_initial + change_amount; 123 | } 124 | } 125 | 126 | pub fn update_sweep(&mut self) { 127 | let target_period = self.target_period(); 128 | if self.sweep_divider == 0 && self.sweep_enabled && self.sweep_shift != 0 129 | && target_period <= 0x7FF && self.period_initial >= 8 { 130 | self.period_initial = target_period; 131 | } 132 | if self.sweep_divider == 0 || self.sweep_reload { 133 | self.sweep_divider = self.sweep_period; 134 | self.sweep_reload = false; 135 | } else { 136 | self.sweep_divider -= 1; 137 | } 138 | } 139 | } 140 | 141 | impl AudioChannelState for PulseChannelState { 142 | fn name(&self) -> String { 143 | return self.name.clone(); 144 | } 145 | 146 | fn chip(&self) -> String { 147 | return self.chip.clone(); 148 | } 149 | 150 | fn sample_buffer(&self) -> &RingBuffer { 151 | return &self.output_buffer; 152 | } 153 | 154 | fn edge_buffer(&self) -> &RingBuffer { 155 | return &self.edge_buffer; 156 | } 157 | 158 | fn record_current_output(&mut self) { 159 | self.debug_filter.consume(self.output() as f32); 160 | self.output_buffer.push((self.debug_filter.output() * -4.0) as i16); 161 | self.edge_buffer.push(self.last_edge as i16); 162 | self.last_edge = false; 163 | } 164 | 165 | fn min_sample(&self) -> i16 { 166 | return -60; 167 | } 168 | 169 | fn max_sample(&self) -> i16 { 170 | return 60; 171 | } 172 | 173 | fn muted(&self) -> bool { 174 | return self.debug_disable; 175 | } 176 | 177 | fn mute(&mut self) { 178 | self.debug_disable = true; 179 | } 180 | 181 | fn unmute(&mut self) { 182 | self.debug_disable = false; 183 | } 184 | 185 | fn playing(&self) -> bool { 186 | return 187 | (self.length_counter.length > 0) && 188 | (self.target_period() <= 0x7FF) && 189 | (self.period_initial > 8) && 190 | (self.envelope.current_volume() > 0); 191 | } 192 | 193 | fn rate(&self) -> PlaybackRate { 194 | let frequency = self.cpu_clock_rate as f32 / (16.0 * (self.period_initial as f32 + 1.0)); 195 | return PlaybackRate::FundamentalFrequency {frequency: frequency}; 196 | } 197 | 198 | fn volume(&self) -> Option { 199 | return Some(Volume::VolumeIndex{ index: self.envelope.current_volume() as usize, max: 15 }); 200 | } 201 | 202 | fn timbre(&self) -> Option { 203 | return match self.duty { 204 | 0b1000_0000 => Some(Timbre::DutyIndex{ index: 0, max: 3 }), 205 | 0b1100_0000 => Some(Timbre::DutyIndex{ index: 1, max: 3 }), 206 | 0b1111_0000 => Some(Timbre::DutyIndex{ index: 2, max: 3 }), 207 | 0b0011_1111 => Some(Timbre::DutyIndex{ index: 3, max: 3 }), 208 | _ => None 209 | } 210 | } 211 | } -------------------------------------------------------------------------------- /external/rusticnes-ui-common/src/memory_window.rs: -------------------------------------------------------------------------------- 1 | use application::RuntimeState; 2 | use drawing; 3 | use drawing::Color; 4 | use drawing::Font; 5 | use drawing::SimpleBuffer; 6 | use events::Event; 7 | use panel::Panel; 8 | 9 | use rusticnes_core::nes::NesState; 10 | use rusticnes_core::memory; 11 | 12 | pub struct MemoryWindow { 13 | pub canvas: SimpleBuffer, 14 | pub counter: u8, 15 | pub font: Font, 16 | pub shown: bool, 17 | pub view_ppu: bool, 18 | pub memory_page: u16, 19 | } 20 | 21 | impl MemoryWindow { 22 | pub fn new() -> MemoryWindow { 23 | let font = Font::from_raw(include_bytes!("assets/8x8_font.png"), 8); 24 | 25 | return MemoryWindow { 26 | canvas: SimpleBuffer::new(360, 220), 27 | counter: 0, 28 | font: font, 29 | shown: false, 30 | view_ppu: false, 31 | memory_page: 0x0000, 32 | }; 33 | } 34 | 35 | pub fn draw_memory_page(&mut self, nes: &NesState, sx: u32, sy: u32) { 36 | for y in 0 .. 16 { 37 | for x in 0 .. 16 { 38 | let address = self.memory_page + (x as u16) + (y as u16 * 16); 39 | let byte: u8; 40 | let mut bg_color = Color::rgb(32, 32, 32); 41 | if (x + y) % 2 == 0 { 42 | bg_color = Color::rgb(48, 48, 48); 43 | } 44 | if self.view_ppu { 45 | let masked_address = address & 0x3FFF; 46 | byte = nes.ppu.debug_read_byte(& *nes.mapper, masked_address); 47 | if masked_address == (nes.ppu.current_vram_address & 0x3FFF) { 48 | bg_color = Color::rgb(128, 32, 32); 49 | } else if nes.ppu.recent_reads.contains(&masked_address) { 50 | for i in 0 .. nes.ppu.recent_reads.len() { 51 | if nes.ppu.recent_reads[i] == masked_address { 52 | let brightness = 192 - (5 * i as u8); 53 | bg_color = Color::rgb(64, brightness, 64); 54 | break; 55 | } 56 | } 57 | } else if nes.ppu.recent_writes.contains(&masked_address) { 58 | for i in 0 .. nes.ppu.recent_writes.len() { 59 | if nes.ppu.recent_writes[i] == masked_address { 60 | let brightness = 192 - (5 * i as u8); 61 | bg_color = Color::rgb(brightness, brightness, 32); 62 | break; 63 | } 64 | } 65 | } 66 | } else { 67 | byte = memory::debug_read_byte(nes, address); 68 | if address == nes.registers.pc { 69 | bg_color = Color::rgb(128, 32, 32); 70 | } else if address == (nes.registers.s as u16 + 0x100) { 71 | bg_color = Color::rgb(32, 32, 128); 72 | } else if nes.memory.recent_reads.contains(&address) { 73 | for i in 0 .. nes.memory.recent_reads.len() { 74 | if nes.memory.recent_reads[i] == address { 75 | let brightness = 192 - (5 * i as u8); 76 | bg_color = Color::rgb(64, brightness, 64); 77 | break; 78 | } 79 | } 80 | } else if nes.memory.recent_writes.contains(&address) { 81 | for i in 0 .. nes.memory.recent_writes.len() { 82 | if nes.memory.recent_writes[i] == address { 83 | let brightness = 192 - (5 * i as u8); 84 | bg_color = Color::rgb(brightness, brightness, 32); 85 | break; 86 | } 87 | } 88 | } 89 | } 90 | let mut text_color = Color::rgba(255, 255, 255, 192); 91 | if byte == 0 { 92 | text_color = Color::rgba(255, 255, 255, 64); 93 | } 94 | let cell_x = sx + x * 19; 95 | let cell_y = sy + y * 11; 96 | drawing::rect(&mut self.canvas, cell_x, cell_y, 19, 11, bg_color); 97 | drawing::hex(&mut self.canvas, &self.font, 98 | cell_x + 2, cell_y + 2, 99 | byte as u32, 2, 100 | text_color); 101 | } 102 | } 103 | } 104 | 105 | pub fn draw(&mut self, nes: &NesState) { 106 | let width = self.canvas.width; 107 | let height = self.canvas.height; 108 | 109 | drawing::rect(&mut self.canvas, 0, 0, width, 33, Color::rgb(0,0,0)); 110 | drawing::rect(&mut self.canvas, 0, 0, 56, height, Color::rgb(0,0,0)); 111 | drawing::text(&mut self.canvas, &self.font, 0, 0, &format!("{} Page: 0x{:04X}", 112 | if self.view_ppu {"PPU"} else {"CPU"}, self.memory_page), 113 | Color::rgb(255, 255, 255)); 114 | 115 | // Draw memory region selector 116 | for i in 0x0 .. 0x10 { 117 | // Highest Nybble 118 | let cell_x = 56 + (i as u32 * 19); 119 | let mut cell_y = 11; 120 | let mut text_color = Color::rgba(255, 255, 255, 64); 121 | if ((self.memory_page & 0xF000) >> 12) == i { 122 | drawing::rect(&mut self.canvas, cell_x, cell_y, 19, 11, Color::rgb(64, 64, 64)); 123 | text_color = Color::rgba(255, 255, 255, 192); 124 | } 125 | drawing::hex(&mut self.canvas, &self.font, cell_x + 2, cell_y + 2, i as u32, 1, text_color); 126 | drawing::char(&mut self.canvas, &self.font, cell_x + 2 + 8, cell_y + 2, 'X', text_color); 127 | 128 | // Second-highest Nybble 129 | text_color = Color::rgba(255, 255, 255, 64); 130 | cell_y = 22; 131 | if ((self.memory_page & 0x0F00) >> 8) == i { 132 | drawing::rect(&mut self.canvas, cell_x, cell_y, 19, 11, Color::rgb(64, 64, 64)); 133 | text_color = Color::rgba(255, 255, 255, 192); 134 | } 135 | drawing::char(&mut self.canvas, &self.font, cell_x + 2, cell_y + 2, 'X', text_color); 136 | drawing::hex(&mut self.canvas, &self.font, cell_x + 2 + 8, cell_y + 2, i as u32, 1, text_color); 137 | } 138 | 139 | // Draw row labels 140 | for i in 0 .. 0x10 { 141 | drawing::text(&mut self.canvas, &self.font, 0, 44 + 2 + (i as u32 * 11), &format!("0x{:04X}", 142 | self.memory_page + (i as u16 * 0x10)), 143 | Color::rgba(255, 255, 255, 64)); 144 | } 145 | self.draw_memory_page(nes, 56, 44); 146 | } 147 | 148 | pub fn handle_click(&mut self, mx: i32, my: i32) { 149 | if my < 11 && mx < 32 { 150 | self.view_ppu = !self.view_ppu; 151 | } 152 | if my >= 11 && my < 22 && mx > 56 && mx < 360 { 153 | let high_nybble = ((mx - 56) / 19) as u16; 154 | self.memory_page = (self.memory_page & 0x0FFF) | (high_nybble << 12); 155 | } 156 | if my >= 22 && my < 33 && mx > 56 && mx < 360 { 157 | let low_nybble = ((mx - 56) / 19) as u16; 158 | self.memory_page = (self.memory_page & 0xF0FF) | (low_nybble << 8); 159 | } 160 | } 161 | } 162 | 163 | 164 | impl Panel for MemoryWindow { 165 | fn title(&self) -> &str { 166 | return "Memory Viewer"; 167 | } 168 | 169 | fn shown(&self) -> bool { 170 | return self.shown; 171 | } 172 | 173 | fn handle_event(&mut self, runtime: &RuntimeState, event: Event) -> Vec { 174 | match event { 175 | Event::RequestFrame => {self.draw(&runtime.nes)}, 176 | Event::ShowMemoryWindow => {self.shown = true}, 177 | Event::CloseWindow => {self.shown = false}, 178 | Event::MemoryViewerNextPage => { 179 | self.memory_page = self.memory_page.wrapping_add(0x100); 180 | }, 181 | Event::MemoryViewerPreviousPage => { 182 | self.memory_page = self.memory_page.wrapping_sub(0x100); 183 | }, 184 | Event::MemoryViewerNextBus => { 185 | self.view_ppu = !self.view_ppu; 186 | }, 187 | Event::MouseClick(x, y) => {self.handle_click(x, y);}, 188 | _ => {} 189 | } 190 | return Vec::::new(); 191 | } 192 | 193 | fn active_canvas(&self) -> &SimpleBuffer { 194 | return &self.canvas; 195 | } 196 | 197 | fn scale_factor(&self) -> u32 { 198 | return 2; 199 | } 200 | } -------------------------------------------------------------------------------- /src/renderer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod options; 2 | 3 | use anyhow::Result; 4 | use std::collections::VecDeque; 5 | use std::fs; 6 | use std::time::{Duration, Instant}; 7 | use crate::emulator; 8 | use crate::video_builder; 9 | use options::{RendererOptions, StopCondition}; 10 | use crate::emulator::SongPosition; 11 | 12 | pub struct Renderer { 13 | options: RendererOptions, 14 | 15 | video: video_builder::VideoBuilder, 16 | emulator: emulator::Emulator, 17 | 18 | encode_start: Instant, 19 | frame_timestamp: f64, 20 | frame_times: VecDeque, 21 | fadeout_timer: Option, 22 | expected_duration: Option 23 | } 24 | 25 | impl Renderer { 26 | pub fn new(options: RendererOptions) -> Result { 27 | let mut emulator = emulator::Emulator::new(); 28 | 29 | match options.config_import_path.clone() { 30 | Some(p) => emulator.init(Some(fs::read_to_string(p)?.as_str())), 31 | None => emulator.init(None) 32 | }; 33 | emulator.open(&options.input_path)?; 34 | emulator.select_track(options.track_index); 35 | emulator.config_audio(options.video_options.sample_rate as _, 0x10000, options.famicom, options.high_quality, options.multiplexing); 36 | emulator.apply_channel_settings(&options.channel_settings); 37 | 38 | let mut video_options = options.video_options.clone(); 39 | emulator.set_piano_roll_size(video_options.resolution_in.0, video_options.resolution_in.1); 40 | 41 | match emulator.nsf_metadata() { 42 | Ok(Some((title, artist, copyright))) => { 43 | video_options.metadata.insert("title".to_string(), title); 44 | video_options.metadata.insert("artist".to_string(), artist); 45 | video_options.metadata.insert("album".to_string(), copyright); 46 | video_options.metadata.insert("track".to_string(), format!("{}/{}", options.track_index, emulator.track_count())); 47 | video_options.metadata.insert("comment".to_string(), "Encoded with NSFPresenter".to_string()); 48 | }, 49 | _ => () 50 | } 51 | 52 | let video = video_builder::VideoBuilder::new(video_options)?; 53 | 54 | Ok(Self { 55 | options: options.clone(), 56 | video, 57 | emulator, 58 | encode_start: Instant::now(), 59 | frame_timestamp: 0.0, 60 | frame_times: VecDeque::new(), 61 | fadeout_timer: None, 62 | expected_duration: None 63 | }) 64 | } 65 | 66 | pub fn start_encoding(&mut self) -> Result<()> { 67 | self.encode_start = Instant::now(); 68 | self.video.start_encoding()?; 69 | 70 | // Run for a frame and clear the audio buffer to prevent the pop during initialization 71 | self.emulator.step(); 72 | self.emulator.clear_sample_buffer(); 73 | 74 | Ok(()) 75 | } 76 | 77 | pub fn step(&mut self) -> Result { 78 | self.emulator.step(); 79 | 80 | self.video.push_video_data(&self.emulator.get_piano_roll_frame())?; 81 | let volume_divisor = match self.fadeout_timer { 82 | Some(t) => (self.options.fadeout_length as f64 / t as f64) as i16, 83 | None => 1i16 84 | }; 85 | if let Some(audio_data) = self.emulator.get_audio_samples(self.video.audio_frame_size(), volume_divisor) { 86 | self.video.push_audio_data(video_builder::as_u8_slice(&audio_data))?; 87 | } 88 | 89 | self.video.step_encoding()?; 90 | 91 | let elapsed_secs = self.elapsed().as_secs_f64(); 92 | let frame_time = elapsed_secs - self.frame_timestamp; 93 | self.frame_timestamp = elapsed_secs; 94 | 95 | self.frame_times.push_front(frame_time); 96 | self.frame_times.truncate(600); 97 | 98 | self.expected_duration = self.next_expected_duration(); 99 | self.fadeout_timer = self.next_fadeout_timer(); 100 | 101 | if let Some(t) = self.fadeout_timer { 102 | if t == 0 { 103 | return Ok(false) 104 | } 105 | } 106 | 107 | Ok(true) 108 | } 109 | 110 | pub fn finish_encoding(&mut self) -> Result<()> { 111 | self.video.finish_encoding()?; 112 | 113 | Ok(()) 114 | } 115 | 116 | pub fn current_frame(&self) -> u64 { 117 | self.emulator.last_frame() as u64 118 | } 119 | 120 | pub fn elapsed(&self) -> Duration { 121 | self.encode_start.elapsed() 122 | } 123 | 124 | fn next_expected_duration(&self) -> Option { 125 | if self.expected_duration.is_some() { 126 | return self.expected_duration; 127 | } 128 | 129 | match self.options.stop_condition { 130 | StopCondition::Frames(stop_duration) => Some((stop_duration + self.options.fadeout_length) as usize), 131 | StopCondition::Loops(stop_loop_count) => { 132 | match self.emulator.loop_duration() { 133 | Some((s, l)) => Some(self.options.fadeout_length as usize + s + l * stop_loop_count), 134 | None => None 135 | } 136 | }, 137 | StopCondition::NsfeLength => { 138 | Some(self.emulator.nsfe_duration().unwrap() + self.options.fadeout_length as usize) 139 | } 140 | } 141 | } 142 | 143 | fn next_fadeout_timer(&self) -> Option { 144 | match self.fadeout_timer { 145 | Some(0) => Some(0), 146 | Some(t) => Some(t - 1), 147 | None => { 148 | match self.options.stop_condition { 149 | StopCondition::Loops(stop_loop_count) => { 150 | let song_ended = match self.emulator.get_song_position() { 151 | Some(position) => position.end, 152 | None => false 153 | }; 154 | if song_ended { 155 | return Some(self.options.fadeout_length); 156 | } 157 | 158 | let loop_count = self.emulator.loop_count() 159 | .expect("Loop detection not supported for this NSF"); 160 | 161 | if loop_count >= stop_loop_count { 162 | Some(self.options.fadeout_length) 163 | } else { 164 | None 165 | } 166 | }, 167 | StopCondition::Frames(stop_duration) => { 168 | if self.current_frame() >= stop_duration { 169 | Some(self.options.fadeout_length) 170 | } else { 171 | None 172 | } 173 | }, 174 | StopCondition::NsfeLength => { 175 | let stop_duration = self.emulator.nsfe_duration() 176 | .expect("No NSFe/NSF2 duration specified for this track"); 177 | 178 | if self.current_frame() >= stop_duration as u64 { 179 | Some(self.options.fadeout_length) 180 | } else { 181 | None 182 | } 183 | } 184 | } 185 | } 186 | } 187 | } 188 | 189 | pub fn song_position(&self) -> Option { 190 | self.emulator.get_song_position() 191 | } 192 | 193 | pub fn loop_count(&self) -> Option { 194 | self.emulator.loop_count() 195 | } 196 | 197 | pub fn instantaneous_fps(&self) -> u32 { 198 | let frame_time = match self.frame_times.front() { 199 | Some(t) => t.clone(), 200 | None => 1.0 201 | }; 202 | (1.0 / frame_time) as u32 203 | } 204 | 205 | pub fn average_fps(&self) -> u32 { 206 | if self.frame_times.is_empty() { 207 | return 0; 208 | } 209 | (self.frame_times.len() as f64 / self.frame_times.iter().sum::()) as u32 210 | } 211 | 212 | pub fn encode_rate(&self) -> f64 { 213 | self.average_fps() as f64 / emulator::NES_NTSC_FRAMERATE 214 | } 215 | 216 | pub fn encoded_duration(&self) -> Duration { 217 | self.video.encoded_video_duration() 218 | } 219 | 220 | pub fn encoded_size(&self) -> usize { 221 | self.video.encoded_video_size() 222 | } 223 | 224 | pub fn expected_duration_frames(&self) -> Option { 225 | self.expected_duration 226 | } 227 | 228 | pub fn expected_duration(&self) -> Option { 229 | Some(Duration::from_secs_f64(self.expected_duration? as f64 / emulator::NES_NTSC_FRAMERATE)) 230 | } 231 | 232 | pub fn eta_duration(&self) -> Option { 233 | match self.expected_duration { 234 | Some(expected_duration) => { 235 | let remaining_frames = expected_duration - self.current_frame() as usize; 236 | let average_fps = u32::max(self.average_fps(), 1) as f64; 237 | let remaining_secs = remaining_frames as f64 / average_fps; 238 | Some(Duration::from_secs_f64(self.elapsed().as_secs_f64() + remaining_secs)) 239 | }, 240 | None => None 241 | } 242 | } 243 | 244 | pub fn emulator_progress(&self) -> String { 245 | self.emulator.progress() 246 | } 247 | } 248 | --------------------------------------------------------------------------------