├── byte_emu ├── Trunk.toml ├── src │ ├── emu │ │ ├── mod.rs │ │ ├── ram.rs │ │ ├── rand.rs │ │ └── core.rs │ ├── app │ │ ├── ui │ │ │ ├── mod.rs │ │ │ ├── emu_controls.rs │ │ │ ├── about.rs │ │ │ ├── menu_bar.rs │ │ │ ├── memory_monitor.rs │ │ │ ├── byte_console.rs │ │ │ └── code_editor.rs │ │ ├── file_processor.rs │ │ └── mod.rs │ └── main.rs ├── assets │ ├── demo.bin │ └── demo.s ├── README.md ├── Cargo.toml └── index.html ├── byte_asm ├── src │ ├── lib.rs │ ├── scanner │ │ ├── mod.rs │ │ ├── error.rs │ │ ├── token.rs │ │ ├── cursor.rs │ │ └── scan.rs │ └── main.rs └── Cargo.toml ├── byte_core ├── tests │ ├── 6502_functional_tests.bin │ ├── 6502_functional_tests.rs │ ├── common.rs │ └── instructions.rs ├── Cargo.toml ├── src │ ├── lib.rs │ ├── bus.rs │ └── cpu.rs └── README.md ├── .cargo └── config.toml ├── Cargo.toml ├── byte_common ├── Cargo.toml ├── src │ ├── lib.rs │ └── opcode_types.rs ├── build.rs └── misc │ └── instructions.json ├── .gitignore ├── README.md └── .github └── workflows ├── pages.yml └── rust.yml /byte_emu/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | -------------------------------------------------------------------------------- /byte_asm/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod scanner; 2 | -------------------------------------------------------------------------------- /byte_emu/src/emu/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod core; 2 | pub mod ram; 3 | pub mod rand; 4 | -------------------------------------------------------------------------------- /byte_emu/assets/demo.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seg6/byte/HEAD/byte_emu/assets/demo.bin -------------------------------------------------------------------------------- /byte_core/tests/6502_functional_tests.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seg6/byte/HEAD/byte_core/tests/6502_functional_tests.bin -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | rustflags = ["--cfg=web_sys_unstable_apis"] 3 | 4 | # [build] 5 | # target = "wasm32-unknown-unknown" -------------------------------------------------------------------------------- /byte_emu/src/app/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod about; 2 | pub mod byte_console; 3 | pub mod code_editor; 4 | pub mod emu_controls; 5 | pub mod memory_monitor; 6 | pub mod menu_bar; 7 | -------------------------------------------------------------------------------- /byte_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "byte_core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | bitflags = "1.3.2" 8 | byte_common = { path = "../byte_common" } -------------------------------------------------------------------------------- /byte_emu/README.md: -------------------------------------------------------------------------------- 1 | # byte-emu 2 | 3 | This crate implements a really simple platform based on the 6502 CPU. 4 | 5 | # TODO 6 | 7 | - [ ] compress two pixels into one byte so that we don't waste much memory on video memory 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "byte_asm", 4 | "byte_common", 5 | "byte_core", 6 | "byte_emu", 7 | ] 8 | resolver = "2" 9 | 10 | [profile.release] 11 | opt-level = 2 12 | 13 | [profile.dev.package."*"] 14 | opt-level = 2 -------------------------------------------------------------------------------- /byte_asm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "byte_asm" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | byte_common = { path = "../byte_common" } 8 | thiserror = "1.0.40" 9 | strum = { version = "0.25", features = ["derive"] } 10 | -------------------------------------------------------------------------------- /byte_common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "byte_common" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | strum = { version = "0.25", features = ["derive"] } 8 | 9 | [build-dependencies] 10 | serde = { version = "1.0.152", features = ["derive"] } 11 | serde_json = "1.0.93" 12 | -------------------------------------------------------------------------------- /byte_asm/src/scanner/mod.rs: -------------------------------------------------------------------------------- 1 | mod cursor; 2 | pub mod error; 3 | pub mod scan; 4 | pub mod token; 5 | 6 | pub use error::ScannerError; 7 | pub use scan::Scanner; 8 | pub use token::{Directive, Location, Token, TokenKind, TokenValue}; 9 | 10 | pub type ScannerResult = std::result::Result; 11 | -------------------------------------------------------------------------------- /byte_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod bus; 2 | pub mod cpu; 3 | 4 | #[derive(Debug)] 5 | pub enum Error { 6 | UnrecognizedOpcode(u8), 7 | } 8 | 9 | impl core::fmt::Display for Error { 10 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 11 | match self { 12 | Error::UnrecognizedOpcode(code) => { 13 | write!(f, "Unrecognized Opcode: {code:#04X}") 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /byte_emu/src/app/ui/emu_controls.rs: -------------------------------------------------------------------------------- 1 | use crate::app::ByteEmuApp; 2 | 3 | impl ByteEmuApp { 4 | pub fn show_emu_controls(&mut self, ctx: &egui::Context) { 5 | let mut open = self.state.is_emu_controls_open; 6 | egui::Window::new("Emulator Controls") 7 | .open(&mut open) 8 | .show(ctx, |ui| { 9 | ui.label("TODO"); 10 | }); 11 | self.state.is_emu_controls_open = open; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # will have compiled files and executables 2 | debug/ 3 | target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # MSVC Windows builds of rustc generate these, which store debugging information 13 | *.pdb 14 | 15 | .ignore/ 16 | dist -------------------------------------------------------------------------------- /byte_emu/src/emu/ram.rs: -------------------------------------------------------------------------------- 1 | use byte_core::bus::Peripheral; 2 | 3 | pub struct Ram { 4 | data: [u8; 0x10000], 5 | } 6 | 7 | impl Default for Ram { 8 | fn default() -> Self { 9 | Self { data: [0; 0x10000] } 10 | } 11 | } 12 | 13 | impl Peripheral for Ram { 14 | fn read(&self, addr: u16) -> u8 { 15 | self.data[addr as usize] 16 | } 17 | 18 | fn write(&mut self, addr: u16, byte: u8) { 19 | self.data[addr as usize] = byte; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /byte_common/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod opcode_types; 2 | 3 | pub mod opcode { 4 | pub use super::opcode_types::*; 5 | 6 | pub const OPCODE_MAP: [Option; 255] = 7 | include!(concat!(env!("OUT_DIR"), "/opcode_arr.rs")); 8 | 9 | // TODO: make this into a comp time hash map of some sort 10 | pub fn get_opcode(mnemonic: Mnemonic, mode: AddressingMode) -> Option<&'static Opcode> { 11 | OPCODE_MAP 12 | .iter() 13 | .flatten() 14 | .find(|opcode| opcode.mnemonic == mnemonic && opcode.mode == mode) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /byte_emu/src/emu/rand.rs: -------------------------------------------------------------------------------- 1 | // from https://matklad.github.io/2023/01/04/on-random-numbers.html 2 | pub fn random_seed() -> u64 { 3 | std::hash::Hasher::finish(&std::hash::BuildHasher::build_hasher( 4 | &std::collections::hash_map::RandomState::new(), 5 | )) 6 | } 7 | 8 | pub fn random_numbers(seed: u32) -> impl Iterator { 9 | let mut random = seed; 10 | 11 | std::iter::repeat_with(move || { 12 | random ^= random << 13; 13 | random ^= random >> 17; 14 | random ^= random << 5; 15 | random 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /byte_asm/src/main.rs: -------------------------------------------------------------------------------- 1 | use byte_asm::scanner::Scanner; 2 | 3 | fn main() { 4 | let file = std::env::args().nth(1).unwrap_or("test.s".to_owned()); 5 | let data = std::fs::read_to_string(file).expect("failed to read the provided file"); 6 | 7 | let mut scanner = Scanner::new(&data); 8 | loop { 9 | match scanner.scan_token() { 10 | Ok(token) => { 11 | println!("{:?}", token); 12 | if token.eof() { 13 | break; 14 | } 15 | } 16 | Err(err) => println!("{:?}", err), 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /byte_core/tests/6502_functional_tests.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(rustfmt, rustfmt_skip)] 2 | 3 | mod common; 4 | 5 | #[test] 6 | fn test_6502_functional_tests() { 7 | let mut cpu = common::init_cpu(); 8 | 9 | cpu.reg.pc = 0x0400; 10 | cpu.load( 11 | include_bytes!("6502_functional_tests.bin"), 0x0000); 12 | 13 | let mut pc = [0xdead, 0xbeef]; 14 | let mut ip = 0; 15 | 16 | loop { 17 | pc[ip % 2] = cpu.reg.pc; 18 | ip += 1; 19 | 20 | if cpu.reg.pc == 0x3469 { break; } 21 | if pc[0] == pc[1] { panic!("test failed: {:#X?}", cpu.reg) } 22 | 23 | cpu.step().unwrap(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /byte_emu/src/app/ui/about.rs: -------------------------------------------------------------------------------- 1 | use crate::app::ByteEmuApp; 2 | 3 | const ABOUT_TEXT: &str = "\ 4 | Byte is a fantasy console that runs on the 6502 microprocessor \ 5 | and features a compact 64x64 screen and an 8-key gamepad keyboard."; 6 | 7 | impl ByteEmuApp { 8 | pub fn show_about(&mut self, ctx: &egui::Context) { 9 | let mut open = self.state.is_about_open; 10 | egui::Window::new("About") 11 | .open(&mut open) 12 | .default_width(460.0) 13 | .show(ctx, |ui| { 14 | self.ui_about(ui); 15 | }); 16 | self.state.is_about_open = open; 17 | } 18 | 19 | fn ui_about(&mut self, ui: &mut egui::Ui) { 20 | ui.heading("Byte"); 21 | ui.add_space(12.0); 22 | ui.label(ABOUT_TEXT); 23 | ui.add_space(12.0); 24 | ui.hyperlink_to("gh/seg6/byte", "https://github.com/seg6/byte"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /byte_asm/src/scanner/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug, Clone)] 4 | pub enum ScannerError { 5 | #[error("[{line}:{column}] unknown assembler directive: {directive}")] 6 | UnknownDirective { 7 | line: usize, 8 | column: usize, 9 | directive: String, 10 | }, 11 | #[error("[{line}:{column}] unknown character: {character}")] 12 | UnknownCharacter { 13 | line: usize, 14 | column: usize, 15 | character: char, 16 | }, 17 | #[error("[{line}:{column}] no number is specified after number symbol: {symbol}")] 18 | NumberExpected { 19 | line: usize, 20 | column: usize, 21 | symbol: char, 22 | }, 23 | #[error("[{line}:{column}] unterminated string quote")] 24 | UnterminatedString { 25 | line: usize, 26 | column: usize, 27 | quote: char, 28 | }, 29 | // is this even needed? 30 | #[error("{0}")] 31 | Generic(String), 32 | } 33 | -------------------------------------------------------------------------------- /byte_core/tests/common.rs: -------------------------------------------------------------------------------- 1 | pub use byte_core::*; 2 | 3 | pub struct MockRAM { 4 | pub data: Vec, 5 | } 6 | 7 | impl MockRAM { 8 | pub fn new(size: usize) -> Self { 9 | Self { 10 | data: vec![0; size], 11 | } 12 | } 13 | } 14 | 15 | impl bus::Peripheral for MockRAM { 16 | fn read(&self, addr: u16) -> u8 { 17 | self.data[addr as usize] 18 | } 19 | 20 | fn write(&mut self, addr: u16, byte: u8) { 21 | self.data[addr as usize] = byte; 22 | } 23 | } 24 | 25 | pub fn init_cpu() -> cpu::CPU { 26 | let mut cpu = cpu::CPU::default(); 27 | 28 | cpu.bus 29 | .attach(0x0000, 0xffff, MockRAM::new(0x10000)) 30 | .unwrap(); 31 | cpu 32 | } 33 | 34 | #[allow(dead_code)] 35 | pub fn execute_nsteps(config: fn(&mut cpu::CPU), program: &[u8], addr: u16, n: usize) -> cpu::CPU { 36 | let mut cpu = init_cpu(); 37 | config(&mut cpu); 38 | 39 | cpu.reg.pc = addr; 40 | cpu.load(program, addr); 41 | 42 | (0..n).for_each(|_| cpu.step().unwrap()); 43 | cpu 44 | } 45 | -------------------------------------------------------------------------------- /byte_emu/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "byte_emu" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.81" 6 | 7 | [dependencies] 8 | byte_asm = { path = "../byte_asm" } 9 | byte_core = { path = "../byte_core" } 10 | byte_common = { path = "../byte_common" } 11 | vfs = { git = "https://github.com/seg6/rust-vfs.git", features = ["serde"] } 12 | 13 | rfd = "0.11.1" 14 | egui = "0.30.0" 15 | eframe = { version = "0.30.0", default-features = false, features = [ 16 | "default_fonts", # embed the default egui fonts 17 | "glow", # use the glow rendering backend. alternative: "wgpu" 18 | "persistence", # enable restoring app state when restarting the app 19 | "wayland", # To support Linux (and CI) 20 | ] } 21 | log = "0.4" 22 | 23 | serde = { version = "1", features = ["derive"] } 24 | bitflags = "1.3.2" 25 | 26 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 27 | env_logger = "0.11" 28 | pollster = "0.3.0" 29 | 30 | [target.'cfg(target_arch = "wasm32")'.dependencies] 31 | wasm-bindgen-futures = "0.4" 32 | web-sys = "0.3.70" 33 | -------------------------------------------------------------------------------- /byte_common/src/opcode_types.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 4 | pub enum TickModifier { 5 | Branch, 6 | PageCrossed, 7 | } 8 | 9 | include!(concat!(env!("OUT_DIR"), "/mnemonics.rs")); 10 | 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 12 | pub enum AddressingMode { 13 | Implied, 14 | Immediate, 15 | Relative, 16 | Accumulator, 17 | ZeroPage, 18 | ZeroPageX, 19 | ZeroPageY, 20 | Absolute, 21 | AbsoluteX, 22 | AbsoluteY, 23 | Indirect, 24 | IndirectX, 25 | IndirectY, 26 | } 27 | 28 | #[derive(Clone, Copy, PartialEq, Eq)] 29 | pub struct Opcode { 30 | pub code: u8, 31 | pub size: u8, 32 | pub tick: u8, 33 | pub mnemonic: Mnemonic, 34 | pub mode: AddressingMode, 35 | pub tick_modifier: Option, 36 | } 37 | 38 | impl fmt::Debug for Opcode { 39 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 40 | write!( 41 | f, 42 | "{:?}:{:02x}:{}:{}:{:?}:{:?}", 43 | self.mnemonic, self.code, self.size, self.tick, self.mode, self.tick_modifier 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /byte_core/README.md: -------------------------------------------------------------------------------- 1 | # byte_core 2 | 3 | The `byte_core` crate emulates the [MOS Technology 6502](https://en.wikipedia.org/wiki/MOS_Technology_6502) CPU and is designed to match its features. This implementation is passes [Klaus's test suite for 6502](https://github.com/Klaus2m5/6502_65C02_functional_tests) and can be used as an independent component, separate from the `byte` project. 4 | 5 | # Example Usage 6 | ```rust 7 | use byte_core::*; 8 | 9 | struct RAM { 10 | pub data: Vec, 11 | } 12 | 13 | impl RAM { 14 | pub fn new(size: usize) -> Self { 15 | Self { data: vec![0; size] } 16 | } 17 | } 18 | 19 | impl bus::Peripheral for RAM { 20 | fn read(&self, addr: u16) -> u8 { 21 | self.data[addr as usize] 22 | } 23 | 24 | fn write(&mut self, addr: u16, byte: u8) { 25 | self.data[addr as usize] = byte; 26 | } 27 | } 28 | 29 | fn main() { 30 | let mut cpu = cpu::CPU::default(); 31 | cpu.bus 32 | .attach(0x0000, 0xffff, RAM::new(0x10000)) 33 | .unwrap(); 34 | 35 | cpu.reg.pc = 0x8000; 36 | cpu.load(&[ 37 | 0xa9, 0xc0, // lda #$c0 38 | 0xaa, // tax 39 | 0xe8, // inx 40 | 0x00, // brk 41 | ], 0x8000); 42 | for _ in 0..4 { cpu.step(); } 43 | 44 | println!("{:#x?}", cpu.reg); 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /byte_asm/src/scanner/token.rs: -------------------------------------------------------------------------------- 1 | use byte_common::opcode::Mnemonic; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq)] 4 | pub struct Location { 5 | pub column: usize, 6 | pub length: usize, 7 | pub line: usize, 8 | pub start: usize, 9 | } 10 | 11 | #[derive(Debug, Clone, PartialEq)] 12 | pub enum TokenValue { 13 | String(String), 14 | Number(u64), 15 | Directive(Directive), 16 | Instruction(Mnemonic), 17 | } 18 | 19 | #[derive(Debug, Clone, Copy, PartialEq, strum::EnumString)] 20 | pub enum Directive { 21 | DB, 22 | DW, 23 | EQU, 24 | INCLUDE, 25 | ORG, 26 | } 27 | 28 | #[derive(Debug, Clone, Copy, PartialEq)] 29 | pub enum TokenKind { 30 | CloseParen, 31 | Colon, 32 | Comma, 33 | Comment, 34 | Directive, 35 | EOF, 36 | Hash, 37 | Identifier, 38 | Instruction, 39 | Minus, 40 | NewLine, 41 | Number, 42 | OpenParen, 43 | Plus, 44 | Semicolon, 45 | Slash, 46 | Star, 47 | String, 48 | } 49 | 50 | #[derive(Debug, Clone, PartialEq)] 51 | pub struct Token { 52 | pub kind: TokenKind, 53 | pub value: Option, 54 | pub location: Location, 55 | } 56 | 57 | impl Token { 58 | pub fn eof(&self) -> bool { 59 | self.kind == TokenKind::EOF 60 | } 61 | 62 | pub fn text<'a>(&self, source: &'a str) -> &'a str { 63 | &source[self.location.start..self.location.start + self.location.length] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /byte_asm/src/scanner/cursor.rs: -------------------------------------------------------------------------------- 1 | use super::Location; 2 | use std::{iter::Peekable, str::Chars}; 3 | 4 | pub struct Cursor<'a> { 5 | pub chars: Peekable>, 6 | pub column: usize, 7 | pub current: usize, 8 | pub line: usize, 9 | pub start: usize, 10 | } 11 | 12 | impl<'a> Cursor<'a> { 13 | pub fn new(source: &'a str) -> Self { 14 | Self { 15 | chars: source.chars().peekable(), 16 | column: 0, 17 | current: 0, 18 | line: 1, 19 | start: 0, 20 | } 21 | } 22 | 23 | pub fn is_at_end(&mut self) -> bool { 24 | self.chars.peek().is_none() 25 | } 26 | 27 | pub fn peek(&mut self) -> Option { 28 | self.chars.peek().copied() 29 | } 30 | 31 | pub fn sync(&mut self) { 32 | self.start = self.current; 33 | } 34 | 35 | pub fn advance(&mut self) -> Option { 36 | if self.is_at_end() { 37 | None 38 | } else { 39 | self.column += 1; 40 | self.current += 1; 41 | self.chars.next() 42 | } 43 | } 44 | 45 | pub fn advance_line(&mut self) { 46 | self.line += 1; 47 | self.column = 0; 48 | } 49 | 50 | pub fn location(&self) -> Location { 51 | Location { 52 | column: self.column, 53 | length: self.current - self.start, 54 | line: self.line, 55 | start: self.start, 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # byte 2 | 3 | A work-in-progress fantasy console designed to provide a user-friendly platform for those who want to learn 6502 assembly. 4 | 5 | # Progress 6 | 7 | - [x] core emulation of 6502, passes [Klaus' test suite](https://github.com/Klaus2m5/6502_65C02_functional_tests) 8 | - [ ] functional emulator 9 | - [x] loading binary/text files 10 | - [x] base emulator implementation (the console with a screen and keypad) 11 | - [x] interactive memory monitor 12 | - [ ] step debugger 13 | - [ ] code editor 14 | - [ ] in memory virtual file system for the wasm target 15 | - [ ] custom assembler 16 | - [ ] custom programming language 17 | 18 | There is a simple PoC deployed at [seg6.github.io/byte](https://seg6.github.io/byte/), running [demo.s](byte_emu/assets/demo.s). 19 | 20 | # Special Registers 21 | 22 | * **0xfd**: **Video Page Pointer** 23 | - This register contains a pointer to the page that will contain the framebuffer. 24 | * **0xfe**: **RNG Source** 25 | - This register resets after each executed instruction and serves as a source of random numbers. 26 | * **0xff**: **Input Register** 27 | - This register holds the key that is currently being pressed down. 28 | 29 | **Key mapping**: 30 | 31 | | Key | Mapping | Mask | 32 | |--------|------------|------| 33 | | Right | ArrowRight | 0x01 | 34 | | Left | ArrowLeft | 0x02 | 35 | | Down | ArrowDown | 0x04 | 36 | | Up | ArrowUp | 0x08 | 37 | | Start | S | 0x10 | 38 | | Select | A | 0x20 | 39 | | B | F | 0x40 | 40 | | A | D | 0x80 | 41 | -------------------------------------------------------------------------------- /byte_emu/src/app/file_processor.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc; 2 | 3 | #[derive(Debug)] 4 | pub struct FileProcesser { 5 | tx: mpsc::Sender, 6 | rx: mpsc::Receiver, 7 | } 8 | 9 | impl FileProcesser 10 | where 11 | T: Send + 'static, 12 | { 13 | pub fn new() -> Self { 14 | let (tx, rx) = mpsc::channel(); 15 | Self { tx, rx } 16 | } 17 | 18 | pub fn consume_messages(&self) -> Vec { 19 | let mut messages = Vec::new(); 20 | 21 | while let Ok(fm) = self.rx.try_recv() { 22 | messages.push(fm); 23 | } 24 | 25 | messages 26 | } 27 | 28 | pub fn read(&mut self, message_fn: F) 29 | where 30 | F: FnOnce(String, Vec) -> T, 31 | F: Send + 'static, 32 | { 33 | let tx = self.tx.clone(); 34 | 35 | execute(async move { 36 | if let Some(file) = rfd::AsyncFileDialog::new().pick_file().await { 37 | let name = file.file_name(); 38 | let data = file.read().await; 39 | 40 | // ignore the error 41 | tx.send(message_fn(name, data)).ok(); 42 | } 43 | }); 44 | } 45 | } 46 | 47 | use std::future::Future; 48 | 49 | #[cfg(not(target_arch = "wasm32"))] 50 | fn execute + Send + 'static>(f: F) { 51 | use pollster::FutureExt as _; 52 | std::thread::spawn(move || f.block_on()); 53 | } 54 | 55 | #[cfg(target_arch = "wasm32")] 56 | fn execute + 'static>(f: F) { 57 | wasm_bindgen_futures::spawn_local(f); 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Github Pages 2 | 3 | # By default, runs if you push to master. keeps your deployed app in sync with master branch. 4 | on: 5 | push: 6 | branches: 7 | - main 8 | # to only run when you do a new github release, comment out above part and uncomment the below trigger. 9 | # on: 10 | # release: 11 | # types: 12 | # - published 13 | 14 | permissions: 15 | contents: write # for committing to gh-pages branch. 16 | 17 | jobs: 18 | build-github-pages: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 # repo checkout 22 | - uses: actions-rs/toolchain@v1 # get rust toolchain for wasm 23 | with: 24 | profile: minimal 25 | toolchain: stable 26 | target: wasm32-unknown-unknown 27 | override: true 28 | - name: Rust Cache # cache the rust build artefacts 29 | uses: Swatinem/rust-cache@v2 30 | - name: Download and install Trunk binary 31 | run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- 32 | - name: Build # build 33 | # "${GITHUB_REPOSITORY#*/}" evaluates into the name of the repository 34 | # using --public-url something will allow trunk to modify all the href paths like from favicon.ico to repo_name/favicon.ico . 35 | # this is necessary for github pages where the site is deployed to username.github.io/repo_name and all files must be requested 36 | # relatively as eframe_template/favicon.ico. if we skip public-url option, the href paths will instead request username.github.io/favicon.ico which 37 | # will obviously return error 404 not found. 38 | run: cd byte_emu && ../trunk build --release --public-url "/${GITHUB_REPOSITORY#*/}" 39 | - name: Deploy 40 | uses: JamesIves/github-pages-deploy-action@v4 41 | with: 42 | folder: byte_emu/dist 43 | # this option will not maintain any history of your previous pages deployment 44 | # set to false if you want all page build to be committed to your gh-pages branch history 45 | single-commit: true 46 | -------------------------------------------------------------------------------- /byte_emu/src/app/ui/menu_bar.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{ByteEmuApp, FileProcesserMessage, State}; 2 | 3 | impl ByteEmuApp { 4 | pub fn show_menu_bar(&mut self, ctx: &egui::Context) { 5 | egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { 6 | self.ui_menu_bar(ui); 7 | }); 8 | } 9 | 10 | fn ui_menu_bar(&mut self, ui: &mut egui::Ui) { 11 | egui::menu::bar(ui, |ui| { 12 | self.ui_file_button(ui); 13 | self.ui_tools_button(ui); 14 | 15 | if ui.button("About").clicked() { 16 | self.state.is_about_open = !self.state.is_about_open; 17 | } 18 | }); 19 | } 20 | 21 | fn ui_file_button(&mut self, ui: &mut egui::Ui) { 22 | ui.menu_button("File", |ui| { 23 | use FileProcesserMessage::*; 24 | 25 | if ui.button("Load binary program").clicked() { 26 | self.file_processer 27 | .read(|name, data| BinaryFile((name, data))); 28 | ui.close_menu(); 29 | } 30 | if ui.button("Load source file").clicked() { 31 | self.file_processer 32 | .read(|name, data| SourceFile((name, data))); 33 | ui.close_menu(); 34 | } 35 | 36 | ui.separator(); 37 | 38 | if ui.button("Reset GUI state").clicked() { 39 | ui.ctx().memory_mut(|mem| *mem = Default::default()); 40 | ui.close_menu(); 41 | } 42 | if ui.button("Reset everything").clicked() { 43 | self.state = State::default(); 44 | ui.ctx().memory_mut(|mem| *mem = Default::default()); 45 | ui.close_menu(); 46 | } 47 | }); 48 | } 49 | 50 | fn ui_tools_button(&mut self, ui: &mut egui::Ui) { 51 | ui.menu_button("Tools", |ui| { 52 | if ui.button("Code Editor").clicked() { 53 | self.state.is_code_editor_open = !self.state.is_code_editor_open; 54 | ui.close_menu(); 55 | } 56 | 57 | if ui.button("Emulator Controls").clicked() { 58 | self.state.is_emu_controls_open = !self.state.is_emu_controls_open; 59 | ui.close_menu(); 60 | } 61 | 62 | if ui.button("Memory Monitor").clicked() { 63 | self.state.is_memory_monitor_open = !self.state.is_memory_monitor_open; 64 | ui.close_menu(); 65 | } 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request, workflow_dispatch] 2 | 3 | name: CI 4 | 5 | env: 6 | RUSTFLAGS: -D warnings 7 | RUSTDOCFLAGS: -D warnings 8 | 9 | jobs: 10 | check: 11 | name: Check 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | profile: minimal 18 | toolchain: stable 19 | override: true 20 | - run: sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libglib2.0-dev libatk1.0-dev libgtk-3-dev 21 | - uses: actions-rs/cargo@v1 22 | with: 23 | command: check 24 | args: --all-features 25 | 26 | check_wasm: 27 | name: Check wasm32 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: actions-rs/toolchain@v1 32 | with: 33 | profile: minimal 34 | toolchain: stable 35 | target: wasm32-unknown-unknown 36 | override: true 37 | - uses: actions-rs/cargo@v1 38 | with: 39 | command: check 40 | args: --all-features --target wasm32-unknown-unknown 41 | 42 | test: 43 | name: Test Suite 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions-rs/toolchain@v1 48 | with: 49 | profile: minimal 50 | toolchain: stable 51 | override: true 52 | - run: sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libglib2.0-dev libatk1.0-dev libgtk-3-dev 53 | - uses: actions-rs/cargo@v1 54 | with: 55 | command: test 56 | 57 | clippy: 58 | name: Clippy 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v4 62 | - uses: actions-rs/toolchain@v1 63 | with: 64 | profile: minimal 65 | toolchain: stable 66 | override: true 67 | components: clippy 68 | - run: sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libglib2.0-dev libatk1.0-dev libgtk-3-dev 69 | - uses: actions-rs/cargo@v1 70 | with: 71 | command: clippy 72 | args: -- -D warnings 73 | 74 | trunk: 75 | name: trunk 76 | runs-on: ubuntu-latest 77 | steps: 78 | - uses: actions/checkout@v4 79 | - uses: actions-rs/toolchain@v1 80 | with: 81 | profile: minimal 82 | toolchain: stable 83 | target: wasm32-unknown-unknown 84 | override: true 85 | - name: Download and install Trunk binary 86 | run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- 87 | - name: Build 88 | run: cd byte_emu && ../trunk build 89 | -------------------------------------------------------------------------------- /byte_emu/src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, rust_2018_idioms)] 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | mod app; 5 | mod emu; 6 | 7 | const DEFAULT_BINARY: &[u8; 1 << 16] = include_bytes!("../assets/demo.bin"); 8 | const DEFAULT_SOURCE: &str = include_str!("../assets/demo.s"); 9 | 10 | #[cfg(not(target_arch = "wasm32"))] 11 | fn main() -> eframe::Result<()> { 12 | use std::env::args; 13 | use std::fs::File; 14 | use std::io::Read; 15 | 16 | env_logger::init(); 17 | 18 | let native_options = eframe::NativeOptions { 19 | viewport: egui::ViewportBuilder::default() 20 | .with_inner_size([400.0, 300.0]) 21 | .with_min_inner_size([300.0, 220.0]), 22 | ..Default::default() 23 | }; 24 | eframe::run_native( 25 | "byte-emu", 26 | native_options, 27 | Box::new(|cc| { 28 | let program = match args().nth(1) { 29 | Some(path) => { 30 | let mut data = Vec::new(); 31 | let mut file = File::open(path).expect("failed to open the file"); 32 | file.read_to_end(&mut data) 33 | .expect("failed to read the file"); 34 | 35 | Some((data, 0x000)) 36 | } 37 | None => None, 38 | }; 39 | 40 | Ok(Box::new(app::ByteEmuApp::new(cc, program))) 41 | }), 42 | ) 43 | } 44 | 45 | #[cfg(target_arch = "wasm32")] 46 | fn main() { 47 | use eframe::wasm_bindgen::JsCast as _; 48 | 49 | eframe::WebLogger::init(log::LevelFilter::Debug).ok(); 50 | let web_options = eframe::WebOptions::default(); 51 | 52 | wasm_bindgen_futures::spawn_local(async { 53 | let document = web_sys::window() 54 | .expect("No window") 55 | .document() 56 | .expect("No document"); 57 | 58 | let canvas = document 59 | .get_element_by_id("byte_emu_app") 60 | .expect("Failed to find byte_emu_app") 61 | .dyn_into::() 62 | .expect("byte_emu_app was not a HtmlCanvasElement"); 63 | 64 | let start_result = eframe::WebRunner::new() 65 | .start( 66 | canvas, 67 | web_options, 68 | Box::new(|cc| Ok(Box::new(app::ByteEmuApp::new(cc, None)))), 69 | ) 70 | .await; 71 | 72 | if let Some(loading_text) = document.get_element_by_id("loading_text") { 73 | match start_result { 74 | Ok(_) => { 75 | loading_text.remove(); 76 | } 77 | Err(e) => { 78 | loading_text.set_inner_html( 79 | "

The app has crashed. See the developer console for details.

", 80 | ); 81 | panic!("Failed to start eframe: {e:?}"); 82 | } 83 | } 84 | } 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /byte_emu/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | byte 8 | 9 | 10 | 11 | 12 | 13 | 90 | 91 | 92 | 93 | 94 | 95 |
96 |

97 | Loading 98 |

99 |
100 |
101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /byte_emu/src/app/ui/memory_monitor.rs: -------------------------------------------------------------------------------- 1 | use egui::{Color32, Label, RichText, ScrollArea}; 2 | 3 | use crate::app::ByteEmuApp; 4 | 5 | impl ByteEmuApp { 6 | pub fn show_memory_monitor(&mut self, ctx: &egui::Context) { 7 | let mut open = self.state.is_memory_monitor_open; 8 | egui::Window::new("Memory Monitor") 9 | .open(&mut open) 10 | .default_width(610.0) 11 | .show(ctx, |ui| { 12 | self.ui_memory_monitor(ui); 13 | }); 14 | self.state.is_memory_monitor_open = open; 15 | } 16 | 17 | fn ui_memory_monitor(&mut self, ui: &mut egui::Ui) { 18 | let addr_str = &mut self.state.memory_window_range_str.0; 19 | let size_str = &mut self.state.memory_window_range_str.1; 20 | self.state.memory_window_text_area.clear(); 21 | 22 | ui.style_mut().override_font_id = Some(egui::FontId::monospace(14.0)); 23 | ui.horizontal(|ui| { 24 | ui.label("addr:"); 25 | ui.text_edit_singleline(addr_str); 26 | }); 27 | ui.horizontal(|ui| { 28 | ui.label("size:"); 29 | ui.text_edit_singleline(size_str); 30 | }); 31 | ui.add_space(10.0); 32 | 33 | if let (Ok(start), Ok(size)) = ( 34 | u16::from_str_radix(addr_str.trim_start_matches("0x"), 16), 35 | u32::from_str_radix(size_str.trim_start_matches("0x"), 16), 36 | ) { 37 | self.state.memory_window_range = (start, size.saturating_sub(1) as u16); 38 | } 39 | 40 | ScrollArea::both().show(ui, |ui| { 41 | ui.vertical(|ui| { 42 | self.ui_memory_monitor_scroll(ui); 43 | }); 44 | }); 45 | } 46 | 47 | fn ui_memory_monitor_scroll(&mut self, ui: &mut egui::Ui) { 48 | let mut count = self.state.memory_window_range.0 as usize; 49 | let mem_slice = self.emu.get_memory_region(self.state.memory_window_range); 50 | 51 | mem_slice.chunks(16).for_each(|chunk| { 52 | let ascii = format!( 53 | "{: <16}", 54 | chunk 55 | .iter() 56 | .map(|b| match b { 57 | // printable range 58 | 0x20..=0x7e => *b as char, 59 | _ => '.', 60 | }) 61 | .collect::() 62 | ); 63 | let mut bytes = format!("{chunk:02x?}").replace(['[', ']', ','], ""); 64 | if bytes.len() > 24 { 65 | bytes.insert(24, ' '); 66 | } 67 | 68 | ui.horizontal(|ui| { 69 | ui.add(Label::new( 70 | RichText::new(format!("{count:04x}")).color(Color32::from_rgb(100, 149, 237)), 71 | )); 72 | ui.add(Label::new( 73 | RichText::new(format!("{bytes: <48}")).color(Color32::LIGHT_GRAY), 74 | )); 75 | ui.add(Label::new( 76 | RichText::new(format!("|{ascii}|")).color(Color32::from_rgb(100, 149, 237)), 77 | )); 78 | }); 79 | count += chunk.len(); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /byte_common/build.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | fn main() { 4 | let opcodes: Vec> = { 5 | let mut result = vec![None; 0xff]; 6 | let opcodes: Vec = serde_json::from_str(include_str!("misc/instructions.json")) 7 | .expect("failed to parse json file"); 8 | 9 | opcodes 10 | .into_iter() 11 | .for_each(|opcode| result[opcode.code as usize] = Some(opcode)); 12 | 13 | result 14 | }; 15 | 16 | let mut identifier = String::new(); 17 | let mut opcode_map = String::new(); 18 | 19 | identifier.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumString)]\n"); 20 | identifier.push_str("pub enum Mnemonic {\n"); 21 | opcode_map.push_str("[\n"); 22 | 23 | for opcode in opcodes.iter() { 24 | match opcode { 25 | Some(op) => { 26 | let mnemonic = op.name.to_uppercase(); 27 | let tick_modifier = 28 | format!("{:?}", op.tick_modifier).replace("Some(", "Some(TickModifier::"); 29 | 30 | if !identifier.contains(mnemonic.as_str()) { 31 | identifier.push_str(format!("{},", mnemonic).as_str()); 32 | } 33 | opcode_map.push_str( 34 | format!( 35 | "Some(Opcode {{ 36 | code: {}, 37 | size: {}, 38 | tick: {}, 39 | tick_modifier: {}, 40 | mnemonic: Mnemonic::{}, 41 | mode: AddressingMode::{:?} 42 | }}),", 43 | op.code, op.size, op.tick, tick_modifier, mnemonic, op.mode, 44 | ) 45 | .as_str(), 46 | ); 47 | } 48 | None => opcode_map.push_str("None,"), 49 | } 50 | } 51 | 52 | identifier.push('}'); 53 | opcode_map.push(']'); 54 | 55 | write_to_out_dir("mnemonics.rs", identifier.as_str()); 56 | write_to_out_dir("opcode_arr.rs", opcode_map.as_str()); 57 | } 58 | 59 | fn write_to_out_dir(filename: &str, content: &str) { 60 | let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set"); 61 | let dest_path = std::path::PathBuf::from(out_dir).join(filename); 62 | std::fs::write(dest_path, content).expect("Could not write file"); 63 | } 64 | 65 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 66 | pub enum TickModifier { 67 | Branch, 68 | PageCrossed, 69 | } 70 | 71 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 72 | pub enum AddressingMode { 73 | Implied, 74 | Immediate, 75 | Relative, 76 | Accumulator, 77 | ZeroPage, 78 | ZeroPageX, 79 | ZeroPageY, 80 | Absolute, 81 | AbsoluteX, 82 | AbsoluteY, 83 | Indirect, 84 | IndirectX, 85 | IndirectY, 86 | } 87 | 88 | #[derive(Serialize, Deserialize, Clone, Copy)] 89 | pub struct Opcode { 90 | pub code: u8, 91 | pub size: u8, 92 | pub tick: u8, 93 | pub name: &'static str, 94 | pub mode: AddressingMode, 95 | pub tick_modifier: Option, 96 | } 97 | -------------------------------------------------------------------------------- /byte_core/src/bus.rs: -------------------------------------------------------------------------------- 1 | pub trait Peripheral { 2 | fn read(&self, addr: u16) -> u8; 3 | fn write(&mut self, addr: u16, byte: u8); 4 | } 5 | 6 | struct PeripheralItem { 7 | range: (u16, u16), 8 | peripheral: Box, 9 | } 10 | 11 | pub struct Bus { 12 | mirror: [u8; 1 << 16], 13 | peripherals: Vec, 14 | } 15 | 16 | impl Default for Bus { 17 | fn default() -> Self { 18 | Self { 19 | mirror: [0; 1 << 16], 20 | peripherals: Vec::new(), 21 | } 22 | } 23 | } 24 | 25 | impl PeripheralItem { 26 | fn new(range: (u16, u16), peripheral: Box) -> Self { 27 | Self { range, peripheral } 28 | } 29 | 30 | fn handles(&self, addr: u16) -> bool { 31 | self.range.0 <= addr && self.range.1 >= addr 32 | } 33 | 34 | fn overlaps(&self, range: (u16, u16)) -> bool { 35 | range.0 < self.range.1 && range.1 > self.range.0 36 | } 37 | } 38 | 39 | impl Bus { 40 | pub fn read(&self, addr: u16) -> u8 { 41 | if let Some((i, addr)) = self.get_peripheral_index(addr) { 42 | self.peripherals[i].peripheral.read(addr) 43 | } else { 44 | 0 45 | } 46 | } 47 | 48 | pub fn write(&mut self, addr: u16, byte: u8) { 49 | // mirror everything written into memory 50 | self.mirror[addr as usize] = byte; 51 | 52 | if let Some((i, addr)) = self.get_peripheral_index(addr) { 53 | self.peripherals[i].peripheral.write(addr, byte); 54 | } 55 | } 56 | 57 | pub fn read_u16(&self, addr: u16) -> u16 { 58 | if let Some((i, addr)) = self.get_peripheral_index(addr) { 59 | let hi = self.peripherals[i].peripheral.read(addr + 1); 60 | let lo = self.peripherals[i].peripheral.read(addr); 61 | 62 | (hi as u16) << 8 | (lo as u16) 63 | } else { 64 | 0 65 | } 66 | } 67 | 68 | pub fn write_u16(&mut self, addr: u16, data: u16) { 69 | if let Some((i, addr)) = self.get_peripheral_index(addr) { 70 | self.peripherals[i] 71 | .peripheral 72 | .write(addr, (data & 0xff) as u8); // low byte 73 | self.peripherals[i] 74 | .peripheral 75 | .write(addr + 1, (data >> 8) as u8); // high byte 76 | } 77 | } 78 | 79 | pub fn get_peripheral_index(&self, addr: u16) -> Option<(usize, u16)> { 80 | for (i, peripheral) in self.peripherals.iter().enumerate() { 81 | if peripheral.handles(addr) { 82 | return Some((i, addr - peripheral.range.0)); 83 | } 84 | } 85 | 86 | None 87 | } 88 | 89 | pub fn attach

(&mut self, lo: u16, hi: u16, peripheral: P) -> Result<(), String> 90 | where 91 | P: Peripheral + 'static, 92 | { 93 | for item in self.peripherals.iter() { 94 | if item.overlaps((lo, hi)) { 95 | return Err(format!( 96 | "overlapping ranges: [{:x}:{:x}] and [{:x}:{:x}]", 97 | lo, hi, item.range.0, item.range.1, 98 | )); 99 | } 100 | } 101 | 102 | self.peripherals 103 | .push(PeripheralItem::new((lo, hi), Box::new(peripheral))); 104 | Ok(()) 105 | } 106 | 107 | pub fn get_memory_region(&self, range: (u16, u16)) -> &[u8] { 108 | let lower = (range.0 as usize).clamp(0, u16::MAX as usize); 109 | let upper = (lower + range.1 as usize).clamp(0, u16::MAX as usize); 110 | 111 | &self.mirror[lower..=upper] 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /byte_emu/src/emu/core.rs: -------------------------------------------------------------------------------- 1 | use super::rand; 2 | use std::collections::HashSet; 3 | 4 | use bitflags::bitflags; 5 | use byte_core::*; 6 | 7 | const COLOR_PALETTE: [u32; 16] = [ 8 | 0x000000FF, 0xFFFFFFFF, 0x880000FF, 0xAAFFEEFF, 0xCC44CCFF, 0x00CC55FF, 0x0000AAFF, 0xEEEE77FF, 9 | 0x664400FF, 0xFF7777FF, 0x333333FF, 0x777777FF, 0xAAFF66FF, 0x0088FFFF, 0x0088FFFF, 0xBBBBBBFF, 10 | ]; 11 | const INSTRUCTIONS_PER_FRAME: usize = 6400000 / 60; 12 | 13 | const REG_VIDEO: u16 = 0xfd; 14 | const REG_RANDOM: u16 = 0xfe; 15 | const REG_INPUT: u16 = 0xff; 16 | const FRAMEBUFFER_SIZE: usize = 64 * 64; 17 | 18 | pub struct ByteEmu { 19 | cpu: cpu::CPU, 20 | rand: Box>, 21 | } 22 | 23 | bitflags! { 24 | pub struct ByteInputState: u8 { 25 | const RIGHT = 0b00000001; 26 | const LEFT = 0b00000010; 27 | const DOWN = 0b00000100; 28 | const UP = 0b00001000; 29 | const START = 0b00010000; 30 | const SELECT = 0b00100000; 31 | const B = 0b01000000; 32 | const A = 0b10000000; 33 | } 34 | } 35 | 36 | impl From> for ByteInputState { 37 | fn from(val: HashSet) -> ByteInputState { 38 | use egui::Key::*; 39 | let mut state = ByteInputState::empty(); 40 | 41 | #[rustfmt::skip] 42 | val.iter().for_each(|key| match key { 43 | A => state.insert(ByteInputState::SELECT), 44 | S => state.insert(ByteInputState::START), 45 | D => state.insert(ByteInputState::A), 46 | F => state.insert(ByteInputState::B), 47 | ArrowUp => state.insert(ByteInputState::UP), 48 | ArrowDown => state.insert(ByteInputState::DOWN), 49 | ArrowLeft => state.insert(ByteInputState::LEFT), 50 | ArrowRight => state.insert(ByteInputState::RIGHT), 51 | _ => () 52 | }); 53 | 54 | state 55 | } 56 | } 57 | 58 | impl Default for ByteEmu { 59 | fn default() -> Self { 60 | let mut cpu = cpu::CPU::default(); 61 | cpu.bus 62 | .attach(0x0000, 0xffff, super::ram::Ram::default()) 63 | .unwrap(); 64 | 65 | Self { 66 | cpu, 67 | rand: Box::new(rand::random_numbers(rand::random_seed() as u32)), 68 | } 69 | } 70 | } 71 | 72 | impl ByteEmu { 73 | pub fn load_program(&mut self, program: &[u8], start: u16) { 74 | self.cpu.load(program, start); 75 | self.cpu.interrupt(cpu::Interrupt::RST); 76 | } 77 | 78 | pub fn framebuffer(&self) -> [u32; FRAMEBUFFER_SIZE] { 79 | let mut frame = [0u32; FRAMEBUFFER_SIZE]; 80 | let video_ptr = (self.cpu.bus.read(REG_VIDEO) as u16 & 0xf) << 0xc; 81 | 82 | frame.iter_mut().enumerate().for_each(|(i, p)| { 83 | let color = self.cpu.bus.read(video_ptr + i as u16) & 0xf; 84 | *p = COLOR_PALETTE[color as usize]; 85 | }); 86 | frame 87 | } 88 | 89 | pub fn step(&mut self, input_state: ByteInputState) { 90 | self.cpu.bus.write(REG_INPUT, input_state.bits()); 91 | 92 | for _ in 0..INSTRUCTIONS_PER_FRAME { 93 | if let Some(n) = self.rand.next() { 94 | self.cpu.bus.write(REG_RANDOM, n as u8); 95 | } 96 | if let Err(err) = self.cpu.step() { 97 | log::error!("{err}"); 98 | }; 99 | } 100 | 101 | self.cpu.interrupt(cpu::Interrupt::IRQ); 102 | } 103 | 104 | pub fn get_memory_region(&self, range: (u16, u16)) -> &[u8] { 105 | self.cpu.bus.get_memory_region(range) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /byte_emu/src/app/ui/byte_console.rs: -------------------------------------------------------------------------------- 1 | use crate::{app::ByteEmuApp, emu::core::ByteInputState}; 2 | use egui::{load::SizedTexture, Color32, ColorImage}; 3 | 4 | const M: f32 = 14.0; // margin 5 | const A: f32 = 36.0; // height 6 | const B: f32 = 20.0; // width 7 | const S: f32 = 320.0; // screen size 8 | const K: f32 = S - 2.0 * M - 4.0 * A - B - B / 4.0; // uhh 9 | 10 | impl ByteEmuApp { 11 | pub fn show_byte_console(&mut self, ctx: &egui::Context, input_state: &mut ByteInputState) { 12 | let framebuffer = self.framebuffer(); 13 | self.texture.set(framebuffer, egui::TextureOptions::NEAREST); 14 | 15 | egui::Window::new("byte console") 16 | .resizable(false) 17 | .default_pos(egui::pos2(170.0, 125.0)) 18 | .show(ctx, |ui| { 19 | // hacky way to ensure that we update the 20 | // input_state only when this window is focused 21 | let current_layer = ui.layer_id(); 22 | let top_middle_layer = ctx.memory(|mem| { 23 | mem.layer_ids() 24 | .filter(|l| l.order == egui::layers::Order::Middle) 25 | .last() 26 | }); 27 | if Some(current_layer) == top_middle_layer { 28 | input_state.insert(ctx.input(|i| i.keys_down.clone()).into()); 29 | } 30 | 31 | self.ui_byte_console(ui, input_state); 32 | }); 33 | } 34 | 35 | // TODO: this kinda sucks, find some other way to draw the gamepad ui 36 | fn ui_byte_console(&mut self, ui: &mut egui::Ui, input_state: &mut ByteInputState) { 37 | #[rustfmt::skip] 38 | ui.vertical(|ui| { 39 | ui.add_space(M); 40 | ui.horizontal(|ui| { 41 | ui.add_space(M); 42 | ui.image(SizedTexture::new(self.texture.id(), egui::vec2(S, S))); 43 | ui.add_space(M); 44 | }); 45 | ui.add_space(M * 3.0); 46 | 47 | ui.scope(|ui| { 48 | ui.spacing_mut().item_spacing = egui::vec2(0.0, 0.0); 49 | 50 | ui.horizontal(|ui| { 51 | ui.add_space(A + M * 2.0); btn(ui, [B, A], input_state, ByteInputState::UP); 52 | }); 53 | ui.horizontal(|ui| { 54 | ui.add_space(M * 2.0); btn(ui, [A, B], input_state, ByteInputState::LEFT); 55 | ui.add_space(B); btn(ui, [A, B], input_state, ByteInputState::RIGHT); 56 | ui.add_space(K); btn(ui, [A, B], input_state, ByteInputState::A); 57 | ui.add_space(B / 4.0); btn(ui, [A, B], input_state, ByteInputState::B); 58 | }); 59 | ui.horizontal(|ui| { 60 | ui.add_space(A + M * 2.0); btn(ui, [B, A], input_state, ByteInputState::DOWN); 61 | ui.add_space((K - B / 4.0) / 2.0); btn(ui, [A, B], input_state, ByteInputState::SELECT); 62 | ui.add_space(B / 4.0); btn(ui, [A, B], input_state, ByteInputState::START); 63 | }); 64 | }); 65 | 66 | ui.add_space(M * 2.0); 67 | }); 68 | } 69 | 70 | fn framebuffer(&mut self) -> ColorImage { 71 | let pixels = self 72 | .emu 73 | .framebuffer() 74 | .iter() 75 | .map(|c| { 76 | let [r, g, b, a] = c.to_be_bytes(); 77 | Color32::from_rgba_unmultiplied(r, g, b, a) 78 | }) 79 | .collect::>(); 80 | 81 | ColorImage { 82 | size: [64, 64], 83 | pixels, 84 | } 85 | } 86 | } 87 | 88 | fn btn( 89 | ui: &mut egui::Ui, 90 | size: impl Into, 91 | input_state: &mut ByteInputState, 92 | state: ByteInputState, 93 | ) { 94 | let (rect, response) = ui.allocate_exact_size(size.into(), egui::Sense::click_and_drag()); 95 | let visuals = if input_state.contains(state) { 96 | &ui.style().visuals.widgets.active 97 | } else { 98 | ui.style().interact(&response) 99 | }; 100 | 101 | if ui.is_rect_visible(rect) { 102 | ui.painter() 103 | .rect(rect, 1.0, visuals.bg_fill, visuals.bg_stroke); 104 | } 105 | 106 | if response.is_pointer_button_down_on() { 107 | input_state.insert(state); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /byte_emu/src/app/mod.rs: -------------------------------------------------------------------------------- 1 | mod file_processor; 2 | mod ui; 3 | 4 | use self::ui::code_editor::Theme as CodeEditorTheme; 5 | use crate::{ 6 | emu::core::{ByteEmu, ByteInputState}, 7 | DEFAULT_BINARY, DEFAULT_SOURCE, 8 | }; 9 | use file_processor::FileProcesser; 10 | 11 | #[derive(Debug)] 12 | pub enum FileProcesserMessage { 13 | BinaryFile((String, Vec)), 14 | SourceFile((String, Vec)), 15 | } 16 | 17 | // `State` that we would like to persist (serialize). 18 | #[derive(serde::Deserialize, serde::Serialize)] 19 | pub struct State { 20 | // TODO: this is getting out of hand 21 | text: String, 22 | memory_window_range: (u16, u16), 23 | memory_window_range_str: (String, String), 24 | memory_window_text_area: String, 25 | code_editor_theme: CodeEditorTheme, 26 | 27 | is_about_open: bool, 28 | is_code_editor_open: bool, 29 | is_emu_controls_open: bool, 30 | is_memory_monitor_open: bool, 31 | 32 | file_system: vfs::MemoryFS, 33 | } 34 | 35 | pub struct ByteEmuApp { 36 | emu: ByteEmu, 37 | file_processer: FileProcesser, 38 | state: State, 39 | texture: egui::TextureHandle, 40 | } 41 | 42 | impl Default for State { 43 | fn default() -> Self { 44 | Self { 45 | text: DEFAULT_SOURCE.to_string(), 46 | memory_window_range: (0, 0x100), 47 | memory_window_range_str: ("0x0000".into(), "0x100".into()), 48 | memory_window_text_area: String::new(), 49 | code_editor_theme: CodeEditorTheme::Default, 50 | 51 | is_about_open: true, 52 | is_code_editor_open: true, 53 | is_emu_controls_open: false, 54 | is_memory_monitor_open: false, 55 | 56 | file_system: vfs::MemoryFS::new(), 57 | } 58 | } 59 | } 60 | 61 | impl eframe::App for ByteEmuApp { 62 | fn save(&mut self, storage: &mut dyn eframe::Storage) { 63 | eframe::set_value(storage, eframe::APP_KEY, &self.state); 64 | } 65 | 66 | fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { 67 | let mut input_state = ByteInputState::empty(); 68 | 69 | self.show_menu_bar(ctx); 70 | self.show_code_editor(ctx); 71 | self.show_emu_controls(ctx); 72 | self.show_memory_monitor(ctx); 73 | self.show_about(ctx); 74 | self.show_byte_console(ctx, &mut input_state); 75 | 76 | self.process_files(); 77 | self.emu.step(input_state); 78 | 79 | // TODO: this might cause some problems when 80 | // `State` (specifically `file_system`) gets too big 81 | if let Some(storage) = frame.storage_mut() { 82 | self.save(storage); 83 | storage.flush(); 84 | } 85 | 86 | ctx.request_repaint(); 87 | } 88 | } 89 | 90 | impl ByteEmuApp { 91 | pub fn new(cc: &eframe::CreationContext<'_>, program: Option<(Vec, u16)>) -> Self { 92 | cc.egui_ctx.set_visuals(egui::Visuals::dark()); 93 | 94 | let mut app = Self { 95 | emu: ByteEmu::default(), 96 | file_processer: FileProcesser::new(), 97 | state: State::default(), 98 | texture: cc.egui_ctx.load_texture( 99 | "framebuffer", 100 | egui::ColorImage::new([64, 64], egui::Color32::BLACK), 101 | Default::default(), 102 | ), 103 | }; 104 | 105 | if let Some(storage) = cc.storage { 106 | if let Some(state) = eframe::get_value(storage, eframe::APP_KEY) { 107 | app.state = state; 108 | } 109 | } 110 | 111 | match program { 112 | Some((program, start)) => app.emu.load_program(&program, start), 113 | None => app.emu.load_program(DEFAULT_BINARY, 0x0000), 114 | } 115 | 116 | app 117 | } 118 | 119 | fn process_files(&mut self) { 120 | self.file_processer 121 | .consume_messages() 122 | .iter() 123 | .for_each(|m| match m { 124 | FileProcesserMessage::BinaryFile((_, data)) => { 125 | // load the program 126 | // and then issue a RST interrupt 127 | self.emu.load_program(data, 0x0000); 128 | } 129 | FileProcesserMessage::SourceFile((_, data)) => { 130 | self.state.text = String::from_utf8_lossy(data).to_string() 131 | } 132 | }); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /byte_emu/assets/demo.s: -------------------------------------------------------------------------------- 1 | ; Simple square demo with position and color control. 2 | ; Position controlled by arrow keys, color cycled by SELECT. 3 | ; 4 | ; Key Mappings: 5 | ; SELECT: 'A' 6 | ; START: 'S' 7 | ; A: 'D' 8 | ; B: 'F' 9 | ; UP: Up arrow key 10 | ; DOWN: Down arrow key 11 | ; LEFT: Left arrow key 12 | ; RIGHT: Right arrow key 13 | 14 | ; TODO: Simplify input handling with macros. 15 | 16 | VIDEO EQU $fd 17 | RANDOM EQU $fe 18 | INPUT EQU $ff 19 | COLOR EQU $fc 20 | 21 | POS_L EQU $15 22 | POS_H EQU $16 23 | PREV_KEY EQU $17 24 | CNT_L EQU $18 25 | CNT_H EQU $19 26 | 27 | SQ_SIZE EQU $09 28 | CNT_ROW EQU $20 29 | 30 | .org $0000 31 | .DB "some random string" 32 | .org $8000 33 | 34 | reset: 35 | cld ; disable decimal mode 36 | ldx #$ff 37 | txs ; initialize the stack pointer 38 | lda #$1 39 | sta VIDEO ; set the video page to $1000 40 | 41 | jsr init 42 | jmp loop 43 | 44 | init: 45 | lda #$1c 46 | sta POS_L ; low byte of the counter 47 | lda #$17 48 | sta POS_H ; high byte of the counter 49 | 50 | lda #$00 ; initial color 51 | sta COLOR 52 | rts 53 | 54 | loop: 55 | jmp loop 56 | 57 | VBLANK_IRQ: 58 | jsr handle_input 59 | jsr update 60 | jsr draw 61 | rti 62 | 63 | draw: 64 | jsr clear 65 | jsr draw_player 66 | rts 67 | 68 | update: 69 | rts 70 | 71 | handle_input: 72 | lda INPUT 73 | beq handle_input_save ; return if no key is being pressed 74 | handle_input_select: 75 | and #%00100000 76 | beq handle_input_up ; branch if another key is being pressed 77 | lda PREV_KEY 78 | and #%00100000 ; check if the prev key is SELECT 79 | bne handle_input_up 80 | inc COLOR 81 | jmp handle_input_save 82 | handle_input_up: 83 | lda INPUT 84 | and #%00001000 85 | beq handle_input_down 86 | jsr move_up 87 | jmp handle_input_save 88 | handle_input_down: 89 | lda INPUT 90 | and #%00000100 91 | beq handle_input_left 92 | jsr move_down 93 | jmp handle_input_save 94 | handle_input_left: 95 | lda INPUT 96 | and #%00000010 97 | beq handle_input_right 98 | jsr move_left 99 | jmp handle_input_save 100 | handle_input_right: 101 | lda INPUT 102 | and #%00000001 103 | beq handle_input_save 104 | jsr move_right 105 | handle_input_save: 106 | lda INPUT 107 | sta PREV_KEY 108 | handle_input_ret: 109 | rts 110 | 111 | move_left: 112 | lda POS_L 113 | sec 114 | sbc #$01 115 | sta POS_L 116 | bcs move_left_ret: 117 | dec POS_H 118 | move_left_ret: 119 | rts 120 | 121 | move_right: 122 | lda POS_L 123 | clc 124 | adc #$01 125 | sta POS_L 126 | bcc move_right_ret 127 | inc POS_H 128 | move_right_ret: 129 | rts 130 | 131 | move_down: 132 | lda POS_L 133 | clc 134 | adc #$40 135 | sta POS_L 136 | bcc move_down_ret 137 | inc POS_H 138 | move_down_ret: 139 | rts 140 | 141 | move_up: 142 | lda POS_L 143 | sec 144 | sbc #$40 145 | sta POS_L 146 | bcs move_up_ret 147 | dec POS_H 148 | move_up_ret: 149 | rts 150 | 151 | clear: 152 | lda #$00 153 | sta CNT_L 154 | lda #$10 155 | sta CNT_H 156 | clear_loop: 157 | lda RANDOM 158 | ldy #$00 159 | sta (CNT_L), y 160 | lda CNT_L 161 | clc 162 | adc #$01 163 | sta CNT_L 164 | bcc clear_loop 165 | lda CNT_H 166 | clc 167 | adc #$01 168 | sta CNT_H 169 | cmp #$20 170 | bne clear_loop 171 | rts 172 | 173 | draw_player: 174 | lda POS_H 175 | pha 176 | lda POS_L 177 | pha 178 | lda #SQ_SIZE 179 | sta CNT_ROW 180 | ldy #$00 181 | draw_player_loop_outer: 182 | ldx #SQ_SIZE 183 | draw_player_loop_inner: 184 | lda COLOR 185 | sta (POS_L), y 186 | lda POS_L 187 | clc 188 | adc #$01 189 | sta POS_L 190 | bcc draw_no_carry_inner 191 | lda POS_H 192 | clc 193 | adc #$01 194 | sta POS_H 195 | draw_no_carry_inner: 196 | dex 197 | bne draw_player_loop_inner 198 | 199 | lda POS_L 200 | clc 201 | adc #($40 - SQ_SIZE) 202 | sta POS_L 203 | bcc draw_no_carry_outer 204 | lda POS_H 205 | clc 206 | adc #$01 207 | sta POS_H 208 | draw_no_carry_outer: 209 | dec CNT_ROW 210 | bne draw_player_loop_outer 211 | draw_ret: 212 | pla 213 | sta POS_L 214 | pla 215 | sta POS_H 216 | rts 217 | 218 | .org $fffc 219 | .DW reset 220 | .DW VBLANK_IRQ 221 | -------------------------------------------------------------------------------- /byte_asm/src/scanner/scan.rs: -------------------------------------------------------------------------------- 1 | use byte_common::opcode::Mnemonic; 2 | 3 | use super::cursor::Cursor; 4 | use super::{Directive, Token, TokenKind, TokenValue}; 5 | use super::{ScannerError, ScannerResult}; 6 | 7 | pub struct Scanner<'a> { 8 | cursor: Cursor<'a>, 9 | source: &'a str, 10 | } 11 | 12 | impl<'a> Scanner<'a> { 13 | pub fn new(source: &'a str) -> Self { 14 | Self { 15 | cursor: Cursor::new(source), 16 | source, 17 | } 18 | } 19 | 20 | pub fn scan_token(&mut self) -> ScannerResult { 21 | self.skip_whitespace(); 22 | self.cursor.sync(); 23 | 24 | let token = match self.cursor.advance() { 25 | None => self.make_token(TokenKind::EOF, None), 26 | Some(c) => match c { 27 | ')' => self.make_token(TokenKind::CloseParen, None), 28 | ',' => self.make_token(TokenKind::Comma, None), 29 | ':' => self.make_token(TokenKind::Colon, None), 30 | '#' => self.make_token(TokenKind::Hash, None), 31 | '-' => self.make_token(TokenKind::Minus, None), 32 | '(' => self.make_token(TokenKind::OpenParen, None), 33 | '+' => self.make_token(TokenKind::Plus, None), 34 | '/' => self.make_token(TokenKind::Slash, None), 35 | '*' => self.make_token(TokenKind::Star, None), 36 | 37 | '\n' => { 38 | let token = self.make_token(TokenKind::NewLine, None); 39 | self.cursor.advance_line(); 40 | token 41 | } 42 | 43 | '%' => { 44 | let value = TokenValue::Number(self.scan_number(2, 1)?); 45 | self.make_token(TokenKind::Number, Some(value)) 46 | } 47 | '$' => { 48 | let value = TokenValue::Number(self.scan_number(16, 1)?); 49 | self.make_token(TokenKind::Number, Some(value)) 50 | } 51 | _ if c.is_ascii_digit() => { 52 | let value = TokenValue::Number(self.scan_number(10, 0)?); 53 | self.make_token(TokenKind::Number, Some(value)) 54 | } 55 | 56 | ';' => { 57 | self.scan_comment(); 58 | self.make_token(TokenKind::Comment, None) 59 | } 60 | 61 | c if c == '\'' || c == '"' => { 62 | let string = TokenValue::String(self.scan_string(c)?); 63 | self.make_token(TokenKind::String, Some(string)) 64 | } 65 | 66 | '.' => { 67 | let identifier = self.scan_identifier()?.to_lowercase(); 68 | let directive = Directive::try_from(identifier[1..].to_uppercase().as_str()) 69 | .map_err(|_| ScannerError::UnknownDirective { 70 | line: self.cursor.line, 71 | column: self.cursor.column, 72 | directive: identifier.to_owned(), 73 | })?; 74 | 75 | self.make_token(TokenKind::Directive, Some(TokenValue::Directive(directive))) 76 | } 77 | 78 | _ if c.is_alphabetic() => { 79 | let identifier = self.scan_identifier()?.to_uppercase(); 80 | 81 | match Mnemonic::try_from(identifier.as_str()) { 82 | Ok(mnemonic) => self.make_token( 83 | TokenKind::Instruction, 84 | Some(TokenValue::Instruction(mnemonic)), 85 | ), 86 | Err(_) => self.make_token(TokenKind::Identifier, None), 87 | } 88 | } 89 | 90 | n => { 91 | return Err(ScannerError::UnknownCharacter { 92 | line: self.cursor.line, 93 | column: self.cursor.column, 94 | character: n, 95 | }) 96 | } 97 | }, 98 | }; 99 | 100 | Ok(token) 101 | } 102 | 103 | fn make_token(&mut self, kind: TokenKind, value: Option) -> Token { 104 | Token { 105 | kind, 106 | value, 107 | location: self.cursor.location(), 108 | } 109 | } 110 | 111 | fn skip_whitespace(&mut self) { 112 | while let Some(' ' | '\r' | '\t') = self.cursor.peek() { 113 | self.cursor.advance(); 114 | } 115 | } 116 | 117 | fn scan_comment(&mut self) { 118 | while let Some(c) = self.cursor.peek() { 119 | if c == '\n' { 120 | break; 121 | } 122 | 123 | self.cursor.advance(); 124 | } 125 | } 126 | 127 | fn scan_identifier(&mut self) -> ScannerResult<&str> { 128 | while let Some(c) = self.cursor.peek() { 129 | if c.is_ascii_alphanumeric() || c == '_' { 130 | self.cursor.advance(); 131 | } else { 132 | break; 133 | } 134 | } 135 | 136 | Ok(&self.source[self.cursor.start..self.cursor.current]) 137 | } 138 | 139 | fn scan_string(&mut self, quote: char) -> ScannerResult { 140 | let mut string = String::new(); 141 | 142 | while let Some(c) = self.cursor.peek() { 143 | if c == quote || c == '\n' { 144 | break; 145 | } 146 | self.cursor.advance(); 147 | 148 | // if c isn't a `\\` just push it 149 | // to `string` and continue processing 150 | if c != '\\' { 151 | string.push(c); 152 | continue; 153 | } 154 | 155 | // if c is a `\\` we potentially have to decode 156 | // an escape sequence 157 | match self.cursor.peek() { 158 | Some('n') => string.push('\n'), 159 | Some('r') => string.push('\r'), 160 | Some('t') => string.push('\t'), 161 | Some('"') => string.push('"'), 162 | Some('\'') => string.push('\''), 163 | Some('\\') => string.push('\\'), 164 | // if the char after `\\` isn't recognized, 165 | // just push `e` into the string 166 | Some(e) => string.push(e), 167 | // don't call `advance` in this case 168 | None => continue, 169 | } 170 | 171 | self.cursor.advance(); 172 | } 173 | 174 | if let None | Some('\n') = self.cursor.peek() { 175 | Err(ScannerError::UnterminatedString { 176 | line: self.cursor.line, 177 | column: self.cursor.column, 178 | quote, 179 | }) 180 | } else { 181 | // consume the second quote 182 | self.cursor.advance(); 183 | Ok(string) 184 | } 185 | } 186 | 187 | fn scan_number(&mut self, radix: u32, start_offset: usize) -> ScannerResult { 188 | while let Some(c) = self.cursor.peek() { 189 | if c.is_digit(radix) { 190 | self.cursor.advance(); 191 | } else { 192 | break; 193 | } 194 | } 195 | 196 | // `scan_number` is called at three different places. 197 | // -- 198 | // if the radix is either `2` or `16`, `cursor.start` 199 | // points to either `%` or `$`. so if `cursor.start` is only 200 | // `1` char away from `cursor.current`, the loop above failed to 201 | // parse any valid digit in base `radix`. 202 | if self.cursor.current - self.cursor.start == 1 && radix != 10 { 203 | return Err(ScannerError::NumberExpected { 204 | line: self.cursor.line, 205 | column: self.cursor.column, 206 | symbol: if radix == 16 { '$' } else { '%' }, 207 | }); 208 | } 209 | 210 | // offset the `start` by `1` if the radix is not `10`. 211 | // essentially skips `%` and `$`. 212 | u64::from_str_radix( 213 | &self.source[self.cursor.start + start_offset..self.cursor.current], 214 | radix, 215 | ) 216 | // this should be unreachable 217 | .map_err(|why| ScannerError::Generic(why.to_string())) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /byte_emu/src/app/ui/code_editor.rs: -------------------------------------------------------------------------------- 1 | use egui::{text::LayoutJob, Color32}; 2 | use std::collections::HashSet; 3 | 4 | use crate::app::ByteEmuApp; 5 | use byte_asm::scanner::{Scanner, Token, TokenKind}; 6 | 7 | type HighlightCache = egui::util::cache::FrameCache; 8 | 9 | impl egui::util::cache::ComputerMut<(&str, Theme), LayoutJob> for Highlighter { 10 | fn compute(&mut self, data: (&str, Theme)) -> LayoutJob { 11 | let (string, theme) = data; 12 | self.highlight(string, theme) 13 | } 14 | } 15 | 16 | impl ByteEmuApp { 17 | pub fn show_code_editor(&mut self, ctx: &egui::Context) { 18 | let mut open = self.state.is_code_editor_open; 19 | egui::Window::new("code editor") 20 | .open(&mut open) 21 | .default_size(egui::vec2(666.0, 588.0)) 22 | .default_pos(egui::pos2(693.0, 216.0)) 23 | .show(ctx, |ui| { 24 | self.ui_code_editor(ui); 25 | }); 26 | self.state.is_code_editor_open = open; 27 | } 28 | 29 | fn ui_code_editor(&mut self, ui: &mut egui::Ui) { 30 | egui::ComboBox::from_label("Select Theme") 31 | .selected_text(format!("{:?}", self.state.code_editor_theme)) 32 | .show_ui(ui, |ui: &mut egui::Ui| { 33 | ui.selectable_value(&mut self.state.code_editor_theme, Theme::Default, "Default"); 34 | ui.selectable_value( 35 | &mut self.state.code_editor_theme, 36 | Theme::EmbersLight, 37 | "EmbersLight", 38 | ); 39 | }); 40 | ui.separator(); 41 | 42 | let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| { 43 | let mut layout_job = highlight(ui.ctx(), string, self.state.code_editor_theme); 44 | layout_job.wrap.max_width = wrap_width; 45 | ui.fonts(|f| f.layout_job(layout_job)) 46 | }; 47 | 48 | egui::ScrollArea::both().show(ui, |ui| { 49 | ui.style_mut().visuals.extreme_bg_color = self 50 | .state 51 | .code_editor_theme 52 | .colorize(HighlighterType::Background); 53 | ui.add_sized( 54 | ui.available_size(), 55 | egui::TextEdit::multiline(&mut self.state.text) 56 | .code_editor() 57 | .desired_width(f32::INFINITY) 58 | .layouter(&mut layouter), 59 | ); 60 | }); 61 | } 62 | } 63 | 64 | fn highlight(ctx: &egui::Context, string: &str, theme: Theme) -> LayoutJob { 65 | ctx.memory_mut(|mem| mem.caches.cache::().get((string, theme))) 66 | } 67 | 68 | #[derive(Default)] 69 | struct Highlighter; 70 | 71 | impl Highlighter { 72 | fn scan_tokens(&self, src: &str) -> Vec { 73 | let mut scanner = Scanner::new(src); 74 | let mut tokens = Vec::new(); 75 | 76 | loop { 77 | match scanner.scan_token() { 78 | Ok(token) if token.eof() => break, 79 | Ok(token) => tokens.push(token), 80 | // handle syntax errors here 81 | _ => (), 82 | } 83 | } 84 | 85 | tokens 86 | } 87 | 88 | #[rustfmt::skip] 89 | fn process_tokens(&self, src: &str, tokens: Vec) -> Vec<(HighlighterType, Token)> { 90 | use TokenKind::*; 91 | 92 | let mut tokens_iter = tokens.iter().peekable(); 93 | let mut label_table = HashSet::new(); 94 | let mut variable_table = HashSet::new(); 95 | 96 | while let Some(token) = tokens_iter.next() { 97 | if token.kind != TokenKind::Identifier { 98 | continue; 99 | } 100 | 101 | if let Some(next_token) = tokens_iter.peek() { 102 | if next_token.kind == Colon { 103 | label_table.insert(token.text(src)); 104 | continue; 105 | } 106 | } 107 | 108 | variable_table.insert(token.text(src)); 109 | } 110 | 111 | let tokens_iter = tokens.into_iter().peekable(); 112 | let mut result = Vec::new(); 113 | 114 | for token in tokens_iter { 115 | let kind = match token.kind { 116 | Number => HighlighterType::Number, 117 | String => HighlighterType::String, 118 | Comment => HighlighterType::Comment, 119 | Instruction => HighlighterType::Instruction, 120 | Directive => HighlighterType::Keyword, 121 | 122 | Identifier => { 123 | if label_table.contains(token.text(src)) { 124 | HighlighterType::Label 125 | } else if variable_table.contains(token.text(src)) { 126 | HighlighterType::Variable 127 | } else { 128 | HighlighterType::Foreground 129 | } 130 | } 131 | 132 | _ => HighlighterType::Foreground, 133 | }; 134 | 135 | result.push((kind, token)); 136 | } 137 | 138 | result 139 | } 140 | 141 | fn highlight(&self, src: &str, theme: Theme) -> LayoutJob { 142 | let tokens = self.scan_tokens(src); 143 | let processed_tokens = self.process_tokens(src, tokens); 144 | 145 | let mut layout_job = LayoutJob::default(); 146 | let mut prev: Option = None; 147 | 148 | let mut append = |text: &str, color: Color32| { 149 | layout_job.append( 150 | text, 151 | 0.0, 152 | egui::TextFormat::simple(egui::FontId::monospace(10.0), color), 153 | ); 154 | }; 155 | 156 | for (kind, token) in processed_tokens { 157 | match prev { 158 | None => { 159 | append( 160 | &src[0..token.location.start], 161 | theme.colorize(HighlighterType::Foreground), 162 | ); 163 | } 164 | Some(prev) => { 165 | let prev_end = prev.location.start + prev.location.length; 166 | if token.location.start - prev_end > 0 { 167 | append( 168 | &src[prev_end..token.location.start], 169 | theme.colorize(HighlighterType::Foreground), 170 | ); 171 | } 172 | } 173 | } 174 | 175 | append(token.text(src), theme.colorize(kind)); 176 | prev = Some(token); 177 | } 178 | 179 | layout_job 180 | } 181 | } 182 | 183 | pub enum HighlighterType { 184 | Background, 185 | Comment, 186 | Foreground, 187 | Instruction, 188 | Keyword, 189 | Label, 190 | Number, 191 | String, 192 | Variable, 193 | } 194 | 195 | #[rustfmt::skip] 196 | #[derive(serde::Deserialize, serde::Serialize)] 197 | #[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)] 198 | pub enum Theme { 199 | Default, 200 | EmbersLight, 201 | } 202 | 203 | impl Theme { 204 | pub fn colorize(&self, kind: HighlighterType) -> Color32 { 205 | use Theme::*; 206 | 207 | match self { 208 | Default => self.default_theme(kind), 209 | EmbersLight => self.embers_light_theme(kind), 210 | } 211 | } 212 | } 213 | 214 | impl Theme { 215 | fn embers_light_theme(&self, kind: HighlighterType) -> Color32 { 216 | use HighlighterType::*; 217 | 218 | match kind { 219 | Background => self.color(0xdbd6d1), 220 | Comment => self.color(0xb19b90), 221 | Foreground => self.color(0x433b32), 222 | Instruction => self.color(0x648a77), 223 | Keyword => self.color(0x6d638c), 224 | Label => self.color(0x6d8257), 225 | Number => self.color(0x8b7586), 226 | String => self.color(0x68858a), 227 | Variable => self.color(0x8e8a70), 228 | } 229 | } 230 | 231 | fn default_theme(&self, kind: HighlighterType) -> Color32 { 232 | use HighlighterType::*; 233 | 234 | match kind { 235 | Background => self.color(0x0a0a0a), 236 | Comment => self.color(0x6a6a69), 237 | Foreground => self.color(0xffffff), 238 | Instruction => self.color(0xffc591), 239 | Keyword => self.color(0x63aacf), 240 | Label => self.color(0x72975f), 241 | Number => self.color(0xd898a4), 242 | String => self.color(0x7baf95), 243 | Variable => self.color(0x96ced8), 244 | } 245 | } 246 | 247 | fn color(&self, color: usize) -> Color32 { 248 | Color32::from_rgb((color >> 16) as u8, (color >> 8) as u8, color as u8) 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /byte_core/tests/instructions.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(rustfmt, rustfmt_skip)] 2 | 3 | mod common; 4 | 5 | use common::cpu::Flags; 6 | use common::execute_nsteps; 7 | 8 | #[test] 9 | fn opcode_0x29_immediate_and() { 10 | // AND #$80 11 | // BRK 12 | let cpu = execute_nsteps( 13 | |cpu| cpu.reg.a = 0b1111_1111, &[0x29, 0x80], 0x8000, 1); 14 | 15 | assert!(cpu.reg.a == 0x80); 16 | assert!(cpu.reg.p.contains(Flags::NEGATIVE)); 17 | } 18 | 19 | #[test] 20 | fn opcode_0x0a_accumulator_asl() { 21 | // ASL A 22 | // BRK 23 | let cpu = execute_nsteps( 24 | |cpu| cpu.reg.a = 0b1010_1010, &[0x0a, 0x00], 0x8000, 1); 25 | 26 | assert_eq!(cpu.reg.a, 0b0101_0100); 27 | assert!(cpu.reg.p.contains(Flags::CARRY)); 28 | assert!(!cpu.reg.p.contains(Flags::NEGATIVE)); 29 | } 30 | 31 | #[test] 32 | fn opcode_0x06_zeropage_asl() { 33 | // ASL $aa 34 | // BRK 35 | let cpu = execute_nsteps( 36 | |cpu| cpu.bus.write(0xaa, 0b1010_1010), &[0x06, 0xaa, 0x00], 0x8000, 1); 37 | 38 | assert_eq!(cpu.bus.read(0xaa), 0b0101_0100); 39 | assert!(cpu.reg.p.contains(Flags::CARRY)); 40 | assert!(!cpu.reg.p.contains(Flags::NEGATIVE)); 41 | } 42 | 43 | #[test] 44 | fn opcode_0x24_zeropage_bit() { 45 | let cpu = execute_nsteps(|cpu| { 46 | cpu.reg.a = 0b1100_0000; 47 | cpu.bus.write(0x00aa, 0b1111_1111); 48 | }, 49 | &[0x24, 0xaa], 0x8000, 1); 50 | 51 | assert!(cpu.reg.p.contains(Flags::NEGATIVE)); 52 | assert!(cpu.reg.p.contains(Flags::OVERFLOW)); 53 | assert!(!cpu.reg.p.contains(Flags::ZERO)); 54 | } 55 | 56 | #[test] 57 | fn opcode_0x90_relative_bcc_1() { 58 | // BCC rel(-5) 59 | // BRK 60 | let cpu = execute_nsteps( 61 | |_| {}, &[0x90, 0xfb, 0x00], 0x8000, 1); 62 | 63 | assert_eq!(cpu.reg.pc, 0x7ffd); // 0x8000 - 5 64 | } 65 | 66 | #[test] 67 | fn opcode_0x90_relative_bcc_2() { 68 | // BCC rel(5) 69 | // BRK 70 | let cpu = execute_nsteps( 71 | |_| {}, &[0x90, 0x05, 0x00], 0x8000, 1); 72 | 73 | assert_eq!(cpu.reg.pc, 0x8007); // 0x8000 + 5 74 | } 75 | 76 | #[test] 77 | fn opcode_0xb0_relative_bcs() { 78 | // BCS rel(-5) 79 | // BRK 80 | let cpu = execute_nsteps( 81 | |cpu| cpu.reg.p.insert(Flags::CARRY), &[0xb0, 0xfb, 0x00], 0x8000, 1); 82 | 83 | assert_eq!(cpu.reg.pc, 0x7ffd); // 0x8000 - 5 84 | } 85 | 86 | #[test] 87 | fn opcode_0xf0_relative_beq() { 88 | // BEQ rel(-5) 89 | // BRK 90 | let cpu = execute_nsteps( 91 | |cpu| cpu.reg.p.insert(Flags::ZERO), &[0xf0, 0xfb, 0x00], 0x8000, 1); 92 | 93 | assert_eq!(cpu.reg.pc, 0x7ffd); // 0x8000 - 5 94 | } 95 | 96 | #[test] 97 | fn opcode_0x30_relative_bmi() { 98 | // BMI rel(-5) 99 | // BRK 100 | let cpu = execute_nsteps( 101 | |cpu| cpu.reg.p.insert(Flags::NEGATIVE), &[0x30, 0xfb, 0x00], 0x8000, 1); 102 | 103 | assert_eq!(cpu.reg.pc, 0x7ffd); // 0x8000 - 5 104 | } 105 | 106 | #[test] 107 | fn opcode_0xd0_relative_bne() { 108 | // BNE rel(-5) 109 | // BRK 110 | let cpu = execute_nsteps( 111 | |_| {}, &[0xd0, 0xfb, 0x00], 0x8000, 1); 112 | 113 | assert_eq!(cpu.reg.pc, 0x7ffd); // 0x8000 - 5 114 | } 115 | 116 | #[test] 117 | fn opcode_0x10_relative_bpl() { 118 | // BPL rel(-5) 119 | // BRK 120 | let cpu = execute_nsteps( 121 | |_| {}, &[0x10, 0xfb, 0x00], 0x8000, 1); 122 | 123 | assert_eq!(cpu.reg.pc, 0x7ffd); // 0x8000 - 5 124 | } 125 | 126 | #[test] 127 | fn opcode_0x50_relative_bvc() { 128 | // BVC rel(-5) 129 | // BRK 130 | let cpu = execute_nsteps( 131 | |_| {}, &[0x50, 0xfb, 0x00], 0x8000, 1); 132 | 133 | assert_eq!(cpu.reg.pc, 0x7ffd); // 0x8000 - 5 134 | } 135 | 136 | #[test] 137 | fn opcode_0x70_relative_bvs() { 138 | // BVS rel(-5) 139 | // BRK 140 | let cpu = execute_nsteps( 141 | |cpu| cpu.reg.p.insert(Flags::OVERFLOW), &[0x70, 0xfb, 0x00], 0x8000, 1); 142 | 143 | assert_eq!(cpu.reg.pc, 0x7ffd); // 0x8000 - 5 144 | } 145 | 146 | #[test] 147 | fn opcode_0x18_implied_clc() { 148 | // CLC 149 | // BRK 150 | let cpu = execute_nsteps( 151 | |cpu| cpu.set_flag(Flags::CARRY, true), &[0x18, 0x00], 0x8000, 1); 152 | 153 | assert!(!cpu.reg.p.contains(Flags::CARRY)); 154 | } 155 | 156 | #[test] 157 | fn opcode_0xd8_implied_cld() { 158 | // CLD 159 | // BRK 160 | let cpu = execute_nsteps( 161 | |cpu| cpu.set_flag(Flags::DECIMAL, true), &[0xd8, 0x00], 0x8000, 1); 162 | 163 | assert!(!cpu.reg.p.contains(Flags::DECIMAL)); 164 | } 165 | 166 | #[test] 167 | fn opcode_0x58_implied_cli() { 168 | // CLI 169 | // BRK 170 | let cpu = execute_nsteps( 171 | |cpu| cpu.set_flag(Flags::INTERRUPT, true), &[0x58, 0x00], 0x8000, 1); 172 | 173 | assert!(!cpu.reg.p.contains(Flags::INTERRUPT)); 174 | } 175 | 176 | #[test] 177 | fn opcode_0xb8_implied_clv() { 178 | // CLV 179 | // BRK 180 | let cpu = execute_nsteps( 181 | |cpu| cpu.set_flag(Flags::OVERFLOW, false), &[0xb8], 0x8000, 1); 182 | 183 | assert!(!cpu.reg.p.contains(Flags::OVERFLOW)); 184 | } 185 | 186 | #[test] 187 | fn opcode_0xc9_immediate_cmp() { 188 | // CMP #$aa 189 | // BRK 190 | let cpu = execute_nsteps( 191 | |cpu| cpu.reg.a = 0xaa, &[0xc9, 0xaa, 0x00], 0x8000, 1); 192 | 193 | assert!(cpu.reg.p.contains(Flags::CARRY)); 194 | assert!(cpu.reg.p.contains(Flags::ZERO)); 195 | } 196 | 197 | #[test] 198 | fn opcode_0xc5_zeropage_cmp() { 199 | // CMP $4a 200 | // BRK 201 | let cpu = execute_nsteps( 202 | |cpu| { 203 | cpu.reg.a = 0xca; 204 | cpu.bus.write(0x4a, 0x4a); 205 | }, 206 | &[0xc5, 0x4a, 0x00], 0x8000, 1); 207 | 208 | assert!(cpu.reg.p.contains(Flags::NEGATIVE)) 209 | } 210 | 211 | #[test] 212 | fn opcode_0xc6_zeropage_dec() { 213 | // DEC $aa 214 | // BRK 215 | let cpu = execute_nsteps( 216 | |cpu| cpu.bus.write(0xaa, 0xff), &[0xc6, 0xaa, 0x00], 0x8000, 1); 217 | 218 | assert_eq!(cpu.bus.read(0x00aa), 0xfe); 219 | } 220 | 221 | #[test] 222 | fn opcode_0xca_implied_dex() { 223 | // DEX 224 | // DEX 225 | // DEX 226 | // BRK 227 | let cpu = execute_nsteps( 228 | |cpu| cpu.reg.x = 0x3, &[0xca, 0xca, 0xca, 0x00], 0x8000, 3); 229 | 230 | assert_eq!(cpu.reg.x, 0x0); 231 | assert!(cpu.reg.p.contains(Flags::ZERO)); 232 | } 233 | 234 | #[test] 235 | fn opcode_0x88_implied_dey() { 236 | // DEY 237 | // BRK 238 | let cpu = execute_nsteps( 239 | |cpu| cpu.reg.y = 0xff, &[0x88, 0x00], 0x8000, 1); 240 | 241 | assert_eq!(cpu.reg.y, 0xfe); 242 | } 243 | 244 | #[test] 245 | fn opcode_0x49_immediate_eor() { 246 | // CMP #$ff 247 | // BRK 248 | let cpu = execute_nsteps( 249 | |cpu| cpu.reg.a = 0b00111100, &[0x49, 0xff, 0x00], 0x8000, 1); 250 | 251 | assert_eq!(cpu.reg.a, 0b11000011); 252 | } 253 | 254 | #[test] 255 | fn opcode_0xe6_zeropage_inc() { 256 | // INC $aa 257 | // BRK 258 | let cpu = execute_nsteps( 259 | |cpu| cpu.bus.write(0xaa, 0xfe), &[0xe6, 0xaa, 0x00], 0x8000, 1); 260 | 261 | assert_eq!(cpu.bus.read(0x00aa), 0xff); 262 | } 263 | 264 | #[test] 265 | fn opcode_0xe8_implied_inx() { 266 | // INX 267 | // INX 268 | // INX 269 | // INX 270 | // INX 271 | // BRK 272 | let cpu = execute_nsteps( 273 | |_| {}, &[0xe8, 0xe8, 0xe8, 0xe8, 0xe8, 0x00], 0x8000, 5); 274 | 275 | assert_eq!(cpu.reg.x, 0x5); 276 | } 277 | 278 | #[test] 279 | fn opcode_0xc8_implied_iny() { 280 | // INY 281 | // INY 282 | // BRK 283 | let cpu = execute_nsteps( 284 | |_| {}, &[0xc8, 0xc8, 0x00], 0x8000, 2); 285 | 286 | assert_eq!(cpu.reg.y, 0x2); 287 | } 288 | 289 | #[test] 290 | fn opcode_0x4c_absolute_jmp() { 291 | // JMP $dead ; absolute jump 292 | // BRK 293 | let cpu = execute_nsteps( 294 | |_| {}, &[0x4c, 0xad, 0xde, 0x00], 0x8000, 1); 295 | 296 | assert_eq!(cpu.reg.pc, 0xdead); 297 | } 298 | 299 | #[test] 300 | fn opcode_0x6c_indirect_jmp() { 301 | // JMP $dead ; indirect jump 302 | // BRK 303 | let cpu = execute_nsteps( 304 | |cpu| cpu.bus.write_u16(0xdead, 0xbeef), &[0x6c, 0xad, 0xde, 0x00], 0x8000, 1); 305 | 306 | assert_eq!(cpu.reg.pc, 0xbeef); 307 | } 308 | 309 | #[test] 310 | fn opcode_0x6c_indirect_jmp_bug() { 311 | // JMP $ccff ; indirect jump 312 | // BRK 313 | let cpu = execute_nsteps( 314 | |cpu| { 315 | cpu.bus.write(0xcc00, 0xde); 316 | cpu.bus.write_u16(0xccff, 0xdead); 317 | }, 318 | &[0x6c, 0xff, 0xcc], 0x8000, 1); 319 | 320 | assert_eq!(cpu.reg.pc, 0xdead); 321 | } 322 | 323 | #[test] 324 | fn opcode_0xa9_immediate_lda() { 325 | // LDA #$ff 326 | // BRK 327 | let cpu = execute_nsteps( 328 | |_| {}, &[0xa9, 0xff, 0x00], 0x8000, 1); 329 | 330 | assert_eq!(cpu.reg.a, 0xff); 331 | assert!(cpu.reg.p.contains(Flags::NEGATIVE)); 332 | } 333 | 334 | #[test] 335 | fn opcode_0xa5_zeropage_lda() { 336 | // LDA $ee 337 | // BRK 338 | let cpu = execute_nsteps( 339 | |cpu| cpu.bus.write(0xee, 0xff), &[0xa5, 0xee, 0x00], 0x8000, 1); 340 | 341 | assert_eq!(cpu.reg.a, 0xff); 342 | assert!(cpu.reg.p.contains(Flags::NEGATIVE)); 343 | } 344 | 345 | #[test] 346 | fn opcode_0xb5_zeropagex_lda() { 347 | // LDA $eb,x 348 | // BRK 349 | let cpu = execute_nsteps( 350 | |cpu| { 351 | cpu.reg.x = 0x3; 352 | cpu.bus.write(0xee, 0xff); 353 | }, 354 | &[0xb5, 0xeb], 0x800, 1); 355 | 356 | assert_eq!(cpu.reg.a, 0xff); 357 | assert!(cpu.reg.p.contains(Flags::NEGATIVE)); 358 | } 359 | 360 | #[test] 361 | fn opcode_0xad_absolute_lda() { 362 | // LDA $faaf 363 | // BRK 364 | let cpu = execute_nsteps( 365 | |cpu| cpu.bus.write(0xfaaf, 0xff), &[0xad, 0xaf, 0xfa, 0x00], 0x8000, 1); 366 | 367 | assert_eq!(cpu.reg.a, 0xff); 368 | assert!(cpu.reg.p.contains(Flags::NEGATIVE)); 369 | } 370 | 371 | #[test] 372 | fn opcode_0xbd_absolutex_lda() { 373 | // LDA $faac,x 374 | // BRK 375 | let cpu = execute_nsteps( 376 | |cpu| { 377 | cpu.reg.x = 0x3; 378 | cpu.bus.write(0xfaaf, 0xff); 379 | }, 380 | &[0xbd, 0xac, 0xfa, 0x00], 0x8000, 1); 381 | 382 | assert_eq!(cpu.reg.a, 0xff); 383 | assert!(cpu.reg.p.contains(Flags::NEGATIVE)); 384 | } 385 | 386 | #[test] 387 | fn opcode_0xb9_absolutey_lda() { 388 | // LDA $faac,y 389 | // BRK 390 | let cpu = execute_nsteps( 391 | |cpu| { 392 | cpu.reg.y = 0x3; 393 | cpu.bus.write(0xfaaf, 0xff); 394 | }, 395 | &[0xb9, 0xac, 0xfa, 0x00], 0x8000, 1); 396 | 397 | assert_eq!(cpu.reg.a, 0xff); 398 | assert!(cpu.reg.p.contains(Flags::NEGATIVE)); 399 | } 400 | 401 | #[test] 402 | fn opcode_0xa1_indirectx_lda() { 403 | // LDA ($05,x) 404 | // BRK 405 | let cpu = execute_nsteps( 406 | |cpu| { 407 | cpu.reg.x = 0x4; 408 | cpu.bus.write(0xfaaf, 0xff); 409 | cpu.bus.write_u16(0x09, 0xfaaf); 410 | }, 411 | &[0xa1, 0x05, 0x00], 0x8000, 1); 412 | 413 | assert_eq!(cpu.reg.a, 0xff); 414 | assert!(cpu.reg.p.contains(Flags::NEGATIVE)); 415 | } 416 | 417 | #[test] 418 | fn opcode_0xb1_indirecty_lda() { 419 | // LDA ($10),y 420 | // BRK 421 | let cpu = execute_nsteps( 422 | |cpu| { 423 | cpu.reg.y = 0x4; 424 | cpu.bus.write(0xff04, 0xff); 425 | cpu.bus.write_u16(0x10, 0xff00); 426 | }, 427 | &[0xb1, 0x10, 0x00], 0x8000, 1); 428 | 429 | assert_eq!(cpu.reg.a, 0xff); 430 | assert!(cpu.reg.p.contains(Flags::NEGATIVE)); 431 | } 432 | 433 | #[test] 434 | fn opcode_0xa2_immediate_ldx() { 435 | let cpu = execute_nsteps( 436 | |_| {}, &[0xa2, 0xff, 0x00], 0x8000, 1); 437 | 438 | assert_eq!(cpu.reg.x, 0xff); 439 | } 440 | 441 | #[test] 442 | fn opcode_0xa0_immediate_ldy() { 443 | let cpu = execute_nsteps( 444 | |_| {}, &[0xa0, 0xff, 0x00], 0x8000, 1); 445 | 446 | assert_eq!(cpu.reg.y, 0xff); 447 | } 448 | 449 | #[test] 450 | fn opcode_0x4a_accumulator_lsr() { 451 | let cpu = execute_nsteps( 452 | |cpu| cpu.reg.a = 0x81, &[0x4a, 0x00], 0x8000, 1); 453 | 454 | assert!(cpu.reg.a == 0x40); 455 | assert!(cpu.reg.p.contains(Flags::CARRY)); 456 | } 457 | 458 | #[test] 459 | fn opcode_0x46_zeropage_lsr() { 460 | let cpu = execute_nsteps( 461 | |cpu| cpu.bus.write(0xde, 0xad), &[0x46, 0xde, 0x00], 0x8000, 1); 462 | 463 | assert!(cpu.bus.read(0xde) == 0xadu8.wrapping_shr(1)); 464 | assert!(cpu.reg.p.contains(Flags::CARRY)); 465 | } 466 | 467 | #[test] 468 | fn opcode_0x09_immediate_ora() { 469 | let cpu = execute_nsteps( 470 | |_| {}, &[0x09, 0xff], 0x8000, 1); 471 | 472 | assert_eq!(cpu.reg.a, 0xff); 473 | } 474 | 475 | #[test] 476 | fn opcode_0x48_implied_pha() { 477 | let mut cpu = execute_nsteps( 478 | |cpu| cpu.reg.a = 0xff, &[0x48, 0x00], 0x8000, 1); 479 | 480 | assert_eq!(cpu.stack_pull(), 0xff); 481 | } 482 | 483 | #[test] 484 | fn opcode_0x08_implied_php() { 485 | let mut cpu = execute_nsteps( 486 | |cpu| cpu.reg.p.insert(Flags::CARRY), &[0x08, 0x00], 0x8000, 1); 487 | 488 | assert!(Flags::from_bits(cpu.stack_pull()).unwrap().contains(Flags::CARRY)); 489 | } 490 | 491 | #[test] 492 | fn opcode_0x68_implied_pla() { 493 | let cpu = execute_nsteps( 494 | |cpu| { 495 | cpu.reg.sp = 0xff; 496 | cpu.stack_push(0xff); 497 | }, &[0x68, 0x00], 0x8000, 1); 498 | 499 | assert_eq!(cpu.reg.a, 0xff); 500 | } 501 | 502 | #[test] 503 | fn opcode_0x28_implied_plp() { 504 | let cpu = execute_nsteps(|cpu| { 505 | cpu.reg.sp = 0xff; 506 | cpu.stack_push(0b1111_0111); 507 | }, &[0x28, 0x00], 0x8000, 1); 508 | 509 | assert_eq!(cpu.reg.p.bits(), 0b1111_0111); 510 | } 511 | 512 | #[test] 513 | fn opcode_0x2a_accumulator_rol() { 514 | let cpu = execute_nsteps(|cpu| { 515 | cpu.set_flag(Flags::CARRY, true); 516 | cpu.reg.a = 0x88; 517 | }, 518 | &[0x2a, 0x00], 0x8000, 1); 519 | 520 | assert!(cpu.reg.a == 0x11); 521 | assert!(cpu.reg.p.contains(Flags::CARRY)); 522 | } 523 | 524 | #[test] 525 | fn opcode_0x6a_accumulator_ror() { 526 | let cpu = execute_nsteps( 527 | |cpu| { 528 | cpu.set_flag(Flags::CARRY, true); 529 | cpu.reg.a = 0x81; 530 | }, 531 | &[0x6a, 0x00], 0x8000, 1); 532 | 533 | assert!(cpu.reg.a == 0xc0); 534 | assert!(cpu.reg.p.contains(Flags::CARRY)); 535 | } 536 | 537 | #[test] 538 | fn opcode_0x40_implied_rti() { 539 | // BRK ; 0xfffe: 0x8003 540 | // NOP 541 | // NOP 542 | // LDA #$ff ; 0x8003 543 | // RTI 544 | let cpu = execute_nsteps( 545 | |cpu| cpu.bus.write_u16(0xfffe, 0x8003), 546 | &[0x00, 0xea, 0xea, 0xa9, 0xff, 0x40], 0x8000, 3); 547 | 548 | assert_eq!(cpu.reg.pc, 0x8002); 549 | } 550 | 551 | #[test] 552 | fn opcode_0x60_implied_rts() { 553 | // [ 0x80 ]: ff \ 554 | // [ 0x01 ]: fe => stack state 555 | // [ 0x00 ]: fd / 556 | // 557 | // RTS 558 | // NOP 559 | let cpu = execute_nsteps( 560 | |cpu| cpu.stack_push_u16(0x8001), &[0x60, 0xea], 0x8000, 1); 561 | assert_eq!(cpu.reg.pc, 0x8002); 562 | } 563 | 564 | #[test] 565 | fn opcode_0x20_absolute_jsr() { 566 | // JSR $80003 567 | // NOP ; 0x8003 568 | let cpu = execute_nsteps( 569 | |cpu| { 570 | cpu.reg.sp = 0xff; // initialize the stack pointer 571 | cpu.bus.write_u16(0x0001, 0x8003); 572 | }, &[0x20, 0x03, 0x80, 0xea], 0x8000, 1); 573 | 574 | assert_eq!(cpu.reg.sp, 0x00fd); 575 | assert_eq!(cpu.reg.pc, 0x8003); 576 | } 577 | 578 | #[test] 579 | fn opcode_0x85_zeropage_sta() { 580 | let cpu = execute_nsteps( 581 | |cpu| cpu.reg.a = 0xad, &[0x85, 0xde, 0x00], 0x8000, 1); 582 | 583 | assert_eq!(cpu.bus.read(0xde), 0xad); 584 | } 585 | 586 | #[test] 587 | fn opcode_0x86_zeropage_stx() { 588 | let cpu = execute_nsteps( 589 | |cpu| cpu.reg.x = 0xef, &[0x86, 0xbe, 0x00], 0x8000, 1); 590 | 591 | assert_eq!(cpu.bus.read(0xbe), 0xef); 592 | } 593 | 594 | #[test] 595 | fn opcode_0x84_zeropage_sty() { 596 | let cpu = execute_nsteps( 597 | |cpu| cpu.reg.y = 0xed, &[0x84, 0xfe, 0x00], 0x8000, 1); 598 | 599 | assert_eq!(cpu.bus.read(0xfe), 0xed); 600 | } 601 | 602 | #[test] 603 | fn opcode_0xaa_implied_tax() { 604 | // TAX 605 | // BRK 606 | let cpu = execute_nsteps( 607 | |cpu| cpu.reg.a = 0x5, &[0xaa, 0x00], 0x8000, 1); 608 | 609 | assert_eq!(cpu.reg.x, 0x5); 610 | } 611 | 612 | #[test] 613 | fn opcode_0xa8_implied_tay() { 614 | // TAY 615 | // BRK 616 | let cpu = execute_nsteps( 617 | |cpu| cpu.reg.a = 0x5, &[0xa8, 0x00], 0x8000, 1); 618 | 619 | assert_eq!(cpu.reg.y, 5); 620 | } 621 | 622 | #[test] 623 | fn opcode_0x8a_implied_txa() { 624 | // TXA 625 | // BRK 626 | let cpu = execute_nsteps( 627 | |cpu| cpu.reg.x = 0x5, &[0x8a, 0x00], 0x8000, 1); 628 | 629 | assert_eq!(cpu.reg.a, 0x5); 630 | } 631 | 632 | #[test] 633 | fn opcode_0x98_implied_tya() { 634 | // TYA 635 | // BRK 636 | let cpu = execute_nsteps( 637 | |cpu| cpu.reg.y = 0x5, &[0x98, 0x00], 0x8000, 1); 638 | 639 | assert_eq!(cpu.reg.a, 0x5); 640 | } 641 | 642 | #[test] 643 | fn opcode_0xba_implied_tsx() { 644 | let cpu = execute_nsteps( 645 | |cpu| cpu.reg.sp = 0xde, &[0xba, 0x00], 0x8000, 1); 646 | 647 | assert_eq!(cpu.reg.x, 0xde); 648 | } 649 | 650 | #[test] 651 | fn opcode_0x9a_implied_txs() { 652 | let cpu = execute_nsteps( 653 | |cpu| cpu.reg.x = 0xad, &[0x9a, 0x00], 0x8000, 1); 654 | 655 | assert_eq!(cpu.reg.sp, 0xad); 656 | } 657 | -------------------------------------------------------------------------------- /byte_core/src/cpu.rs: -------------------------------------------------------------------------------- 1 | use crate::bus::Bus; 2 | use crate::Error; 3 | 4 | use bitflags::bitflags; 5 | use byte_common::opcode::*; 6 | 7 | pub const STACK_BASE: u16 = 0x0100; 8 | pub const NMI_VECTOR: u16 = 0xfffa; 9 | pub const RST_VECTOR: u16 = 0xfffc; 10 | pub const IRQ_VECTOR: u16 = 0xfffe; 11 | 12 | #[derive(Debug)] 13 | pub enum Operand { 14 | Accumulator, 15 | Address(u16), 16 | } 17 | 18 | #[derive(Debug, PartialEq, Eq)] 19 | pub enum Interrupt { 20 | IRQ, 21 | NMI, 22 | BRK, 23 | RST, 24 | } 25 | 26 | bitflags! { 27 | #[derive(Default)] 28 | pub struct Flags: u8 { 29 | const NEGATIVE = 0b10000000; 30 | const OVERFLOW = 0b01000000; 31 | const UNUSED = 0b00100000; 32 | const BREAK = 0b00010000; 33 | const DECIMAL = 0b00001000; 34 | const INTERRUPT = 0b00000100; 35 | const ZERO = 0b00000010; 36 | const CARRY = 0b00000001; 37 | } 38 | } 39 | 40 | #[derive(Default, Debug, Clone, Copy)] 41 | pub struct Registers { 42 | pub sp: u8, 43 | pub pc: u16, 44 | 45 | pub x: u8, 46 | pub y: u8, 47 | pub a: u8, 48 | pub p: Flags, 49 | } 50 | 51 | #[derive(Default)] 52 | pub struct CPU { 53 | pub bus: Bus, 54 | pub cycle: u64, 55 | pub reg: Registers, 56 | } 57 | 58 | impl CPU { 59 | pub fn load(&mut self, program: &[u8], start: u16) { 60 | program 61 | .iter() 62 | .enumerate() 63 | .for_each(|(i, b)| self.bus.write(start + i as u16, *b)); 64 | } 65 | 66 | pub fn interrupt(&mut self, interrupt: Interrupt) { 67 | let (pc, vector) = match interrupt { 68 | Interrupt::BRK => (self.reg.pc + 1, IRQ_VECTOR), 69 | Interrupt::IRQ => (self.reg.pc, IRQ_VECTOR), 70 | Interrupt::NMI => (self.reg.pc, NMI_VECTOR), 71 | Interrupt::RST => (0, RST_VECTOR), 72 | }; 73 | 74 | if interrupt != Interrupt::RST { 75 | let mut p = self.reg.p; 76 | p.set(Flags::UNUSED, true); 77 | p.set(Flags::BREAK, interrupt == Interrupt::BRK); 78 | 79 | self.stack_push_u16(pc); 80 | self.stack_push(p.bits()); 81 | self.set_flag(Flags::INTERRUPT, true); 82 | } 83 | 84 | self.reg.pc = self.bus.read_u16(vector); 85 | self.cycle += 7; 86 | } 87 | 88 | // attrs on expressions is still experimental 89 | // move this to the line where we match on `opcode.code` 90 | #[rustfmt::skip] 91 | pub fn step(&mut self) -> Result<(), Error> { 92 | let opcode = self.bus.read(self.reg.pc); 93 | self.reg.pc = self.reg.pc.wrapping_add(1); 94 | let pc_copy = self.reg.pc; 95 | 96 | let opcode = OPCODE_MAP 97 | .get(opcode as usize) 98 | .and_then(|opcode| opcode.as_ref()) 99 | .ok_or(Error::UnrecognizedOpcode(opcode))?; 100 | 101 | match opcode.code { 102 | 0x69 | 0x65 | 0x75 | 0x6d | 0x7d | 0x79 | 0x61 | 0x71 => self.adc(opcode), 103 | 0x29 | 0x25 | 0x35 | 0x2d | 0x3d | 0x39 | 0x21 | 0x31 => self.and(opcode), 104 | 0x0a | 0x06 | 0x16 | 0x0e | 0x1e => self.asl(opcode), 105 | 0x24 | 0x2c => self.bit(opcode), 106 | 0xc9 | 0xc5 | 0xd5 | 0xcd | 0xdd | 0xd9 | 0xc1 | 0xd1 => self.cmp(opcode, self.reg.a), 107 | 0xe0 | 0xe4 | 0xec => self.cmp(opcode, self.reg.x), 108 | 0xc0 | 0xc4 | 0xcc => self.cmp(opcode, self.reg.y), 109 | 0xc6 | 0xd6 | 0xce | 0xde => self.dec(opcode), 110 | 0x49 | 0x45 | 0x55 | 0x4d | 0x5d | 0x59 | 0x41 | 0x51 => self.eor(opcode), 111 | 0xe6 | 0xf6 | 0xee | 0xfe => self.inc(opcode), 112 | 0x4c | 0x6c => self.jmp(opcode), 113 | 0xa9 | 0xa5 | 0xb5 | 0xad | 0xbd | 0xb9 | 0xa1 | 0xb1 => self.lda(opcode), 114 | 0xa2 | 0xa6 | 0xb6 | 0xae | 0xbe => self.ldx(opcode), 115 | 0xa0 | 0xa4 | 0xb4 | 0xac | 0xbc => self.ldy(opcode), 116 | 0x4a | 0x46 | 0x56 | 0x4e | 0x5e => self.lsr(opcode), 117 | 0x09 | 0x05 | 0x15 | 0x0d | 0x1d | 0x19 | 0x01 | 0x11 => self.ora(opcode), 118 | 0x2a | 0x26 | 0x36 | 0x2e | 0x3e => self.rol(opcode), 119 | 0x6a | 0x66 | 0x76 | 0x6e | 0x7e => self.ror(opcode), 120 | 0xe9 | 0xe5 | 0xf5 | 0xed | 0xfd | 0xf9 | 0xe1 | 0xf1 => self.sbc(opcode), 121 | 0x85 | 0x95 | 0x8d | 0x9d | 0x99 | 0x81 | 0x91 => self.str(opcode, self.reg.a), 122 | 0x86 | 0x96 | 0x8e => self.str(opcode, self.reg.x), 123 | 0x84 | 0x94 | 0x8c => self.str(opcode, self.reg.y), 124 | 125 | 0x90 => self.branch(opcode, !self.reg.p.contains(Flags::CARRY)), 126 | 0xb0 => self.branch(opcode, self.reg.p.contains(Flags::CARRY)), 127 | 0xf0 => self.branch(opcode, self.reg.p.contains(Flags::ZERO)), 128 | 0xd0 => self.branch(opcode, !self.reg.p.contains(Flags::ZERO)), 129 | 0x10 => self.branch(opcode, !self.reg.p.contains(Flags::NEGATIVE)), 130 | 0x30 => self.branch(opcode, self.reg.p.contains(Flags::NEGATIVE)), 131 | 0x70 => self.branch(opcode, self.reg.p.contains(Flags::OVERFLOW)), 132 | 0x50 => self.branch(opcode, !self.reg.p.contains(Flags::OVERFLOW)), 133 | 134 | 0xca => self.dex(opcode), 135 | 0x88 => self.dey(opcode), 136 | 137 | 0xe8 => self.inx(opcode), 138 | 0xc8 => self.iny(opcode), 139 | 140 | 0x18 => self.set_flag(Flags::CARRY, false), 141 | 0xd8 => self.set_flag(Flags::DECIMAL, false), 142 | 0x58 => self.set_flag(Flags::INTERRUPT, false), 143 | 0xb8 => self.set_flag(Flags::OVERFLOW, false), 144 | 145 | 0x48 => self.pha(opcode), 146 | 0x08 => self.php(opcode), 147 | 0x68 => self.pla(opcode), 148 | 0x28 => self.plp(opcode), 149 | 150 | 0x40 => self.rti(opcode), 151 | 0x60 => self.rts(opcode), 152 | 153 | 0x38 => self.set_flag(Flags::CARRY, true), 154 | 0xf8 => self.set_flag(Flags::DECIMAL, true), 155 | 0x78 => self.set_flag(Flags::INTERRUPT, true), 156 | 157 | 0xaa => self.tax(opcode), 158 | 0x8a => self.txa(opcode), 159 | 0xa8 => self.tay(opcode), 160 | 0x98 => self.tya(opcode), 161 | 0xba => self.tsx(opcode), 162 | 0x9a => self.txs(opcode), 163 | 164 | 0x00 => { 165 | self.interrupt(Interrupt::BRK); 166 | return Ok(()); 167 | } 168 | 0x20 => self.jsr(opcode), 169 | 0xea => {}, 170 | _ => {} 171 | } 172 | 173 | if pc_copy == self.reg.pc { 174 | self.reg.pc = self.reg.pc.wrapping_add((opcode.size - 1) as u16); 175 | } 176 | 177 | self.cycle += opcode.tick as u64; 178 | Ok(()) 179 | } 180 | 181 | pub fn stack_push(&mut self, byte: u8) { 182 | self.bus 183 | .write(STACK_BASE.wrapping_add(self.reg.sp as u16), byte); 184 | self.reg.sp = self.reg.sp.wrapping_sub(1); 185 | } 186 | 187 | pub fn stack_push_u16(&mut self, data: u16) { 188 | self.stack_push((data >> 0x8) as u8); 189 | self.stack_push((data & 0xff) as u8); 190 | } 191 | 192 | pub fn stack_pull(&mut self) -> u8 { 193 | self.reg.sp = self.reg.sp.wrapping_add(1); 194 | self.bus.read(STACK_BASE.wrapping_add(self.reg.sp as u16)) 195 | } 196 | 197 | pub fn stack_pull_u16(&mut self) -> u16 { 198 | let lo = self.stack_pull() as u16; 199 | let hi = self.stack_pull() as u16; 200 | 201 | (hi << 8) | lo 202 | } 203 | 204 | pub fn set_flag(&mut self, flag: Flags, value: bool) { 205 | self.reg.p.set(flag, value); 206 | } 207 | 208 | fn update_nz_flags(&mut self, value: u8) { 209 | self.set_flag(Flags::ZERO, value == 0); 210 | self.set_flag(Flags::NEGATIVE, value & 0x80 > 0); 211 | } 212 | 213 | // this function doesn't actually get called on `TickModifier::Branch`, instead 214 | // the `branch` function keeps track of branching to a different page 215 | fn on_tick_modifier(&mut self, lo: u8, hi: u8, byte: u8, _modifier: TickModifier) -> Operand { 216 | let addr = ((hi as u16) << 8 | (lo as u16)).wrapping_add(byte as u16); 217 | 218 | if hi != (addr >> 8) as u8 { 219 | self.cycle += 1 220 | } 221 | 222 | Operand::Address(addr) 223 | } 224 | 225 | fn get_operand(&mut self, opcode: &Opcode) -> Operand { 226 | match opcode.mode { 227 | AddressingMode::Relative => Operand::Address(self.reg.pc), 228 | AddressingMode::Immediate => Operand::Address(self.reg.pc), 229 | AddressingMode::Accumulator => Operand::Accumulator, 230 | 231 | AddressingMode::ZeroPage => Operand::Address(self.bus.read(self.reg.pc) as u16), 232 | AddressingMode::ZeroPageX => { 233 | Operand::Address(self.bus.read(self.reg.pc).wrapping_add(self.reg.x) as u16) 234 | } 235 | AddressingMode::ZeroPageY => { 236 | Operand::Address(self.bus.read(self.reg.pc).wrapping_add(self.reg.y) as u16) 237 | } 238 | 239 | AddressingMode::Absolute => Operand::Address(self.bus.read_u16(self.reg.pc)), 240 | AddressingMode::AbsoluteX => { 241 | if let Some(modifier) = opcode.tick_modifier { 242 | let lo = self.bus.read(self.reg.pc); 243 | let hi = self.bus.read(self.reg.pc + 1); 244 | 245 | return self.on_tick_modifier(lo, hi, self.reg.x, modifier); 246 | } 247 | 248 | Operand::Address( 249 | self.bus 250 | .read_u16(self.reg.pc) 251 | .wrapping_add(self.reg.x as u16), 252 | ) 253 | } 254 | AddressingMode::AbsoluteY => { 255 | if let Some(modifier) = opcode.tick_modifier { 256 | let lo = self.bus.read(self.reg.pc); 257 | let hi = self.bus.read(self.reg.pc + 1); 258 | 259 | return self.on_tick_modifier(lo, hi, self.reg.y, modifier); 260 | } 261 | 262 | Operand::Address( 263 | self.bus 264 | .read_u16(self.reg.pc) 265 | .wrapping_add(self.reg.y as u16), 266 | ) 267 | } 268 | 269 | AddressingMode::Indirect => { 270 | Operand::Address(self.bus.read_u16(self.bus.read_u16(self.reg.pc))) 271 | } 272 | AddressingMode::IndirectX => Operand::Address( 273 | self.bus 274 | .read_u16(self.bus.read(self.reg.pc).wrapping_add(self.reg.x) as u16), 275 | ), 276 | AddressingMode::IndirectY => { 277 | if let Some(modifier) = opcode.tick_modifier { 278 | let lo = self.bus.read(self.bus.read(self.reg.pc) as u16); 279 | let hi = self.bus.read(self.bus.read(self.reg.pc) as u16 + 1); 280 | 281 | return self.on_tick_modifier(lo, hi, self.reg.y, modifier); 282 | } 283 | 284 | Operand::Address( 285 | self.bus 286 | .read_u16(self.bus.read(self.reg.pc) as u16) 287 | .wrapping_add(self.reg.y as u16), 288 | ) 289 | } 290 | 291 | _ => unreachable!(), 292 | } 293 | } 294 | } 295 | 296 | // Opcode implementations 297 | 298 | impl CPU { 299 | #[inline] 300 | fn _asl(&mut self, value: u8) -> u8 { 301 | self.set_flag(Flags::CARRY, value >> 7 == 1); 302 | let result = value.wrapping_shl(1); 303 | self.update_nz_flags(result); 304 | result 305 | } 306 | 307 | #[inline] 308 | fn _lsr(&mut self, value: u8) -> u8 { 309 | self.set_flag(Flags::CARRY, value & 0x1 != 0); 310 | let result = value.wrapping_shr(1); 311 | self.set_flag(Flags::ZERO, value == 0); 312 | self.set_flag(Flags::NEGATIVE, false); 313 | result 314 | } 315 | 316 | #[inline] 317 | fn _rol(&mut self, value: u8) -> u8 { 318 | let result = value.rotate_left(1) & 0xfe | self.reg.p.contains(Flags::CARRY) as u8; 319 | 320 | self.set_flag(Flags::CARRY, value & 0x80 > 0); 321 | self.update_nz_flags(result); 322 | 323 | result 324 | } 325 | 326 | #[rustfmt::skip] 327 | #[inline] 328 | fn _ror(&mut self, value: u8) -> u8 { 329 | let result = value.rotate_right(1) 330 | & 0x7f 331 | | ((self.reg.p.contains(Flags::CARRY) as u8) << 7); 332 | 333 | self.set_flag(Flags::CARRY, value & 0x1 > 0); 334 | self.update_nz_flags(result); 335 | 336 | result 337 | } 338 | 339 | #[rustfmt::skip] 340 | fn adc(&mut self, opcode: &Opcode) { 341 | let m = self.reg.a as u16; 342 | let c = self.reg.p.contains(Flags::CARRY) as u16; 343 | 344 | if let Operand::Address(addr) = self.get_operand(opcode) { 345 | let n = self.bus.read(addr) as u16; 346 | 347 | if self.reg.p.contains(Flags::DECIMAL) { 348 | let mut l = (m & 0x0f) + (n & 0x0f) + c; 349 | let mut h = (m & 0xf0) + (n & 0xf0); 350 | 351 | if l > 0x09 { 352 | l = (l + 0x06) & 0x0f; h += 0x10; 353 | }; 354 | self.set_flag(Flags::OVERFLOW, !(m ^ n) & (m ^ h) & 0x80 != 0); 355 | if h > 0x90 { 356 | h += 0x60; 357 | }; 358 | self.set_flag(Flags::CARRY, h >> 8 > 0); 359 | 360 | self.reg.a = (h | l) as u8 361 | } else { 362 | let s = m + n + c; 363 | 364 | self.set_flag(Flags::CARRY, s > 0xff); 365 | self.set_flag(Flags::OVERFLOW, !(m ^ n) & (m ^ s) & 0x80 != 0); 366 | 367 | self.reg.a = s as u8; 368 | } 369 | 370 | self.update_nz_flags(self.reg.a); 371 | } 372 | } 373 | 374 | fn and(&mut self, opcode: &Opcode) { 375 | if let Operand::Address(addr) = self.get_operand(opcode) { 376 | self.reg.a &= self.bus.read(addr); 377 | self.update_nz_flags(self.reg.a); 378 | } 379 | } 380 | 381 | fn asl(&mut self, opcode: &Opcode) { 382 | match self.get_operand(opcode) { 383 | Operand::Accumulator => { 384 | self.reg.a = self._asl(self.reg.a); 385 | } 386 | Operand::Address(addr) => { 387 | let byte = self._asl(self.bus.read(addr)); 388 | self.bus.write(addr, byte); 389 | } 390 | } 391 | } 392 | 393 | fn bit(&mut self, opcode: &Opcode) { 394 | if let Operand::Address(addr) = self.get_operand(opcode) { 395 | let operand = self.bus.read(addr); 396 | let result = self.reg.a & operand; 397 | 398 | self.update_nz_flags(result); 399 | self.set_flag(Flags::NEGATIVE, operand & 0x80 > 0); 400 | self.set_flag(Flags::OVERFLOW, operand & 0x40 > 0); 401 | } 402 | } 403 | 404 | fn branch(&mut self, opcode: &Opcode, condition: bool) { 405 | if !condition { 406 | return; 407 | } 408 | 409 | self.cycle += 1; 410 | 411 | if let Operand::Address(addr) = self.get_operand(opcode) { 412 | let page = self.reg.pc >> 8; 413 | 414 | self.reg.pc = self 415 | .reg 416 | .pc 417 | .wrapping_add(1) 418 | .wrapping_add(i8::from_le_bytes(self.bus.read(addr).to_le_bytes()) as u16); 419 | 420 | if page != self.reg.pc >> 8 { 421 | self.cycle += 1; 422 | } 423 | } 424 | } 425 | 426 | fn cmp(&mut self, opcode: &Opcode, reg: u8) { 427 | if let Operand::Address(addr) = self.get_operand(opcode) { 428 | let operand = self.bus.read(addr); 429 | 430 | self.set_flag(Flags::ZERO, reg == operand); 431 | self.set_flag(Flags::CARRY, reg >= operand); 432 | self.set_flag(Flags::NEGATIVE, reg.wrapping_sub(operand) & 0x80 > 0); 433 | } 434 | } 435 | 436 | fn dec(&mut self, opcode: &Opcode) { 437 | if let Operand::Address(addr) = self.get_operand(opcode) { 438 | let value = self.bus.read(addr).wrapping_sub(1); 439 | self.bus.write(addr, value); 440 | self.update_nz_flags(value); 441 | } 442 | } 443 | 444 | fn dex(&mut self, _opcode: &Opcode) { 445 | self.reg.x = self.reg.x.wrapping_sub(1); 446 | self.update_nz_flags(self.reg.x); 447 | } 448 | 449 | fn dey(&mut self, _opcode: &Opcode) { 450 | self.reg.y = self.reg.y.wrapping_sub(1); 451 | self.update_nz_flags(self.reg.y); 452 | } 453 | 454 | fn eor(&mut self, opcode: &Opcode) { 455 | if let Operand::Address(addr) = self.get_operand(opcode) { 456 | self.reg.a ^= self.bus.read(addr); 457 | self.update_nz_flags(self.reg.a); 458 | } 459 | } 460 | 461 | fn inc(&mut self, opcode: &Opcode) { 462 | if let Operand::Address(addr) = self.get_operand(opcode) { 463 | let value = self.bus.read(addr).wrapping_add(1); 464 | self.bus.write(addr, value); 465 | self.update_nz_flags(value); 466 | } 467 | } 468 | 469 | fn inx(&mut self, _opcode: &Opcode) { 470 | self.reg.x = self.reg.x.wrapping_add(1); 471 | self.update_nz_flags(self.reg.x); 472 | } 473 | 474 | fn iny(&mut self, _opcode: &Opcode) { 475 | self.reg.y = self.reg.y.wrapping_add(1); 476 | self.update_nz_flags(self.reg.y); 477 | } 478 | 479 | fn jmp(&mut self, opcode: &Opcode) { 480 | let operand = self.bus.read_u16(self.reg.pc); 481 | 482 | if opcode.code == 0x6c { 483 | if operand & 0xff != 0xff { 484 | return self.reg.pc = self.bus.read_u16(operand); 485 | } 486 | 487 | // 6502 indirect jump bug 488 | let lo = self.bus.read(operand) as u16; 489 | let hi = self.bus.read(operand & 0xff00) as u16; 490 | 491 | return self.reg.pc = (hi << 8) | lo; 492 | } 493 | 494 | self.reg.pc = operand; 495 | } 496 | 497 | fn jsr(&mut self, opcode: &Opcode) { 498 | if let Operand::Address(addr) = self.get_operand(opcode) { 499 | self.stack_push_u16(self.reg.pc.wrapping_add(1)); 500 | self.reg.pc = addr; 501 | } 502 | } 503 | 504 | fn lda(&mut self, opcode: &Opcode) { 505 | if let Operand::Address(addr) = self.get_operand(opcode) { 506 | self.reg.a = self.bus.read(addr); 507 | self.update_nz_flags(self.reg.a); 508 | } 509 | } 510 | 511 | fn ldx(&mut self, opcode: &Opcode) { 512 | if let Operand::Address(addr) = self.get_operand(opcode) { 513 | self.reg.x = self.bus.read(addr); 514 | self.update_nz_flags(self.reg.x); 515 | } 516 | } 517 | 518 | fn ldy(&mut self, opcode: &Opcode) { 519 | if let Operand::Address(addr) = self.get_operand(opcode) { 520 | self.reg.y = self.bus.read(addr); 521 | self.update_nz_flags(self.reg.y); 522 | } 523 | } 524 | 525 | fn lsr(&mut self, opcode: &Opcode) { 526 | match self.get_operand(opcode) { 527 | Operand::Accumulator => { 528 | self.reg.a = self._lsr(self.reg.a); 529 | } 530 | Operand::Address(addr) => { 531 | let byte = self._lsr(self.bus.read(addr)); 532 | self.bus.write(addr, byte); 533 | } 534 | } 535 | } 536 | 537 | fn ora(&mut self, opcode: &Opcode) { 538 | if let Operand::Address(addr) = self.get_operand(opcode) { 539 | self.reg.a |= self.bus.read(addr); 540 | self.update_nz_flags(self.reg.a); 541 | } 542 | } 543 | 544 | fn pha(&mut self, _opcode: &Opcode) { 545 | self.stack_push(self.reg.a); 546 | } 547 | 548 | fn php(&mut self, _opcode: &Opcode) { 549 | self.stack_push(self.reg.p.bits() | 0x30); 550 | } 551 | 552 | fn pla(&mut self, _opcode: &Opcode) { 553 | self.reg.a = self.stack_pull(); 554 | self.update_nz_flags(self.reg.a); 555 | } 556 | 557 | fn plp(&mut self, _opcode: &Opcode) { 558 | self.reg.p.bits = self.stack_pull() | 0x30; 559 | } 560 | 561 | fn rol(&mut self, opcode: &Opcode) { 562 | match self.get_operand(opcode) { 563 | Operand::Accumulator => { 564 | self.reg.a = self._rol(self.reg.a); 565 | } 566 | Operand::Address(addr) => { 567 | let byte = self._rol(self.bus.read(addr)); 568 | self.bus.write(addr, byte); 569 | } 570 | } 571 | } 572 | 573 | fn ror(&mut self, opcode: &Opcode) { 574 | match self.get_operand(opcode) { 575 | Operand::Accumulator => { 576 | self.reg.a = self._ror(self.reg.a); 577 | } 578 | Operand::Address(addr) => { 579 | let byte = self._ror(self.bus.read(addr)); 580 | self.bus.write(addr, byte); 581 | } 582 | } 583 | } 584 | 585 | fn rti(&mut self, _opcode: &Opcode) { 586 | self.reg.p.bits = self.stack_pull() | 0x30; 587 | self.reg.pc = self.stack_pull_u16(); 588 | } 589 | 590 | fn rts(&mut self, _opcode: &Opcode) { 591 | self.reg.pc = self.stack_pull_u16() + 1; 592 | } 593 | 594 | #[rustfmt::skip] 595 | fn sbc(&mut self, opcode: &Opcode) { 596 | let m = self.reg.a; 597 | let c = self.reg.p.contains(Flags::CARRY) as u8; 598 | 599 | if let Operand::Address(addr) = self.get_operand(opcode) { 600 | let n = self.bus.read(addr); 601 | let mut s = m as u16 602 | + !n as u16 603 | + c as u16; 604 | 605 | self.update_nz_flags(s as u8); 606 | self.set_flag(Flags::CARRY, s > 0xff); 607 | self.set_flag(Flags::OVERFLOW, (m ^ n) & (m ^ s as u8) & 0x80 > 0); 608 | 609 | if self.reg.p.contains(Flags::DECIMAL) { 610 | let mut l = (m & 0x0f) as i16 - (n & 0x0f) as i16 + (c as i16) - 1; 611 | let mut h = (m & 0xf0) as i16 - (n & 0xf0) as i16; 612 | 613 | if l < 0x00 { l = (l - 0x06) & 0x0f; h -= 0x10; } 614 | if h < 0x00 { h = (h - 0x60) & 0xf0; } 615 | 616 | s = (h | l) as u16; 617 | } 618 | 619 | self.reg.a = s as u8; 620 | self.update_nz_flags(self.reg.a); 621 | } 622 | } 623 | 624 | fn str(&mut self, opcode: &Opcode, reg: u8) { 625 | if let Operand::Address(addr) = self.get_operand(opcode) { 626 | self.bus.write(addr, reg); 627 | } 628 | } 629 | 630 | fn tax(&mut self, _opcode: &Opcode) { 631 | self.reg.x = self.reg.a; 632 | self.update_nz_flags(self.reg.x); 633 | } 634 | 635 | fn tay(&mut self, _opcode: &Opcode) { 636 | self.reg.y = self.reg.a; 637 | self.update_nz_flags(self.reg.y); 638 | } 639 | 640 | fn tsx(&mut self, _opcode: &Opcode) { 641 | self.reg.x = self.reg.sp; 642 | self.update_nz_flags(self.reg.x); 643 | } 644 | 645 | fn txa(&mut self, _opcode: &Opcode) { 646 | self.reg.a = self.reg.x; 647 | self.update_nz_flags(self.reg.a); 648 | } 649 | 650 | fn txs(&mut self, _opcode: &Opcode) { 651 | self.reg.sp = self.reg.x; 652 | } 653 | 654 | fn tya(&mut self, _opcode: &Opcode) { 655 | self.reg.a = self.reg.y; 656 | self.update_nz_flags(self.reg.a); 657 | } 658 | } 659 | -------------------------------------------------------------------------------- /byte_common/misc/instructions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "code": 105, 4 | "mode": "Immediate", 5 | "name": "ADC", 6 | "size": 2, 7 | "tick": 2, 8 | "tick_modifier": null 9 | }, 10 | { 11 | "code": 101, 12 | "mode": "ZeroPage", 13 | "name": "ADC", 14 | "size": 2, 15 | "tick": 3, 16 | "tick_modifier": null 17 | }, 18 | { 19 | "code": 117, 20 | "mode": "ZeroPageX", 21 | "name": "ADC", 22 | "size": 2, 23 | "tick": 4, 24 | "tick_modifier": null 25 | }, 26 | { 27 | "code": 109, 28 | "mode": "Absolute", 29 | "name": "ADC", 30 | "size": 3, 31 | "tick": 4, 32 | "tick_modifier": null 33 | }, 34 | { 35 | "code": 125, 36 | "mode": "AbsoluteX", 37 | "name": "ADC", 38 | "size": 3, 39 | "tick": 4, 40 | "tick_modifier": "PageCrossed" 41 | }, 42 | { 43 | "code": 121, 44 | "mode": "AbsoluteY", 45 | "name": "ADC", 46 | "size": 3, 47 | "tick": 4, 48 | "tick_modifier": "PageCrossed" 49 | }, 50 | { 51 | "code": 97, 52 | "mode": "IndirectX", 53 | "name": "ADC", 54 | "size": 2, 55 | "tick": 6, 56 | "tick_modifier": null 57 | }, 58 | { 59 | "code": 113, 60 | "mode": "IndirectY", 61 | "name": "ADC", 62 | "size": 2, 63 | "tick": 5, 64 | "tick_modifier": "PageCrossed" 65 | }, 66 | { 67 | "code": 41, 68 | "mode": "Immediate", 69 | "name": "AND", 70 | "size": 2, 71 | "tick": 2, 72 | "tick_modifier": null 73 | }, 74 | { 75 | "code": 37, 76 | "mode": "ZeroPage", 77 | "name": "AND", 78 | "size": 2, 79 | "tick": 3, 80 | "tick_modifier": null 81 | }, 82 | { 83 | "code": 53, 84 | "mode": "ZeroPageX", 85 | "name": "AND", 86 | "size": 2, 87 | "tick": 4, 88 | "tick_modifier": null 89 | }, 90 | { 91 | "code": 45, 92 | "mode": "Absolute", 93 | "name": "AND", 94 | "size": 3, 95 | "tick": 4, 96 | "tick_modifier": null 97 | }, 98 | { 99 | "code": 61, 100 | "mode": "AbsoluteX", 101 | "name": "AND", 102 | "size": 3, 103 | "tick": 4, 104 | "tick_modifier": "PageCrossed" 105 | }, 106 | { 107 | "code": 57, 108 | "mode": "AbsoluteY", 109 | "name": "AND", 110 | "size": 3, 111 | "tick": 4, 112 | "tick_modifier": "PageCrossed" 113 | }, 114 | { 115 | "code": 33, 116 | "mode": "IndirectX", 117 | "name": "AND", 118 | "size": 2, 119 | "tick": 6, 120 | "tick_modifier": null 121 | }, 122 | { 123 | "code": 49, 124 | "mode": "IndirectY", 125 | "name": "AND", 126 | "size": 2, 127 | "tick": 5, 128 | "tick_modifier": "PageCrossed" 129 | }, 130 | { 131 | "code": 10, 132 | "mode": "Accumulator", 133 | "name": "ASL", 134 | "size": 1, 135 | "tick": 2, 136 | "tick_modifier": null 137 | }, 138 | { 139 | "code": 6, 140 | "mode": "ZeroPage", 141 | "name": "ASL", 142 | "size": 2, 143 | "tick": 5, 144 | "tick_modifier": null 145 | }, 146 | { 147 | "code": 22, 148 | "mode": "ZeroPageX", 149 | "name": "ASL", 150 | "size": 2, 151 | "tick": 6, 152 | "tick_modifier": null 153 | }, 154 | { 155 | "code": 14, 156 | "mode": "Absolute", 157 | "name": "ASL", 158 | "size": 3, 159 | "tick": 6, 160 | "tick_modifier": null 161 | }, 162 | { 163 | "code": 30, 164 | "mode": "AbsoluteX", 165 | "name": "ASL", 166 | "size": 3, 167 | "tick": 7, 168 | "tick_modifier": null 169 | }, 170 | { 171 | "code": 144, 172 | "mode": "Relative", 173 | "name": "BCC", 174 | "size": 2, 175 | "tick": 2, 176 | "tick_modifier": "Branch" 177 | }, 178 | { 179 | "code": 176, 180 | "mode": "Relative", 181 | "name": "BCS", 182 | "size": 2, 183 | "tick": 2, 184 | "tick_modifier": "Branch" 185 | }, 186 | { 187 | "code": 240, 188 | "mode": "Relative", 189 | "name": "BEQ", 190 | "size": 2, 191 | "tick": 2, 192 | "tick_modifier": "Branch" 193 | }, 194 | { 195 | "code": 36, 196 | "mode": "ZeroPage", 197 | "name": "BIT", 198 | "size": 2, 199 | "tick": 3, 200 | "tick_modifier": null 201 | }, 202 | { 203 | "code": 44, 204 | "mode": "Absolute", 205 | "name": "BIT", 206 | "size": 3, 207 | "tick": 4, 208 | "tick_modifier": null 209 | }, 210 | { 211 | "code": 48, 212 | "mode": "Relative", 213 | "name": "BMI", 214 | "size": 2, 215 | "tick": 2, 216 | "tick_modifier": "Branch" 217 | }, 218 | { 219 | "code": 208, 220 | "mode": "Relative", 221 | "name": "BNE", 222 | "size": 2, 223 | "tick": 2, 224 | "tick_modifier": "Branch" 225 | }, 226 | { 227 | "code": 16, 228 | "mode": "Relative", 229 | "name": "BPL", 230 | "size": 2, 231 | "tick": 2, 232 | "tick_modifier": "Branch" 233 | }, 234 | { 235 | "code": 0, 236 | "mode": "Implied", 237 | "name": "BRK", 238 | "size": 1, 239 | "tick": 7, 240 | "tick_modifier": null 241 | }, 242 | { 243 | "code": 80, 244 | "mode": "Relative", 245 | "name": "BVC", 246 | "size": 2, 247 | "tick": 2, 248 | "tick_modifier": "Branch" 249 | }, 250 | { 251 | "code": 112, 252 | "mode": "Relative", 253 | "name": "BVS", 254 | "size": 2, 255 | "tick": 2, 256 | "tick_modifier": "Branch" 257 | }, 258 | { 259 | "code": 24, 260 | "mode": "Implied", 261 | "name": "CLC", 262 | "size": 1, 263 | "tick": 2, 264 | "tick_modifier": null 265 | }, 266 | { 267 | "code": 216, 268 | "mode": "Implied", 269 | "name": "CLD", 270 | "size": 1, 271 | "tick": 2, 272 | "tick_modifier": null 273 | }, 274 | { 275 | "code": 88, 276 | "mode": "Implied", 277 | "name": "CLI", 278 | "size": 1, 279 | "tick": 2, 280 | "tick_modifier": null 281 | }, 282 | { 283 | "code": 184, 284 | "mode": "Implied", 285 | "name": "CLV", 286 | "size": 1, 287 | "tick": 2, 288 | "tick_modifier": null 289 | }, 290 | { 291 | "code": 201, 292 | "mode": "Immediate", 293 | "name": "CMP", 294 | "size": 2, 295 | "tick": 2, 296 | "tick_modifier": null 297 | }, 298 | { 299 | "code": 197, 300 | "mode": "ZeroPage", 301 | "name": "CMP", 302 | "size": 2, 303 | "tick": 3, 304 | "tick_modifier": null 305 | }, 306 | { 307 | "code": 213, 308 | "mode": "ZeroPageX", 309 | "name": "CMP", 310 | "size": 2, 311 | "tick": 4, 312 | "tick_modifier": null 313 | }, 314 | { 315 | "code": 205, 316 | "mode": "Absolute", 317 | "name": "CMP", 318 | "size": 3, 319 | "tick": 4, 320 | "tick_modifier": null 321 | }, 322 | { 323 | "code": 221, 324 | "mode": "AbsoluteX", 325 | "name": "CMP", 326 | "size": 3, 327 | "tick": 4, 328 | "tick_modifier": "PageCrossed" 329 | }, 330 | { 331 | "code": 217, 332 | "mode": "AbsoluteY", 333 | "name": "CMP", 334 | "size": 3, 335 | "tick": 4, 336 | "tick_modifier": "PageCrossed" 337 | }, 338 | { 339 | "code": 193, 340 | "mode": "IndirectX", 341 | "name": "CMP", 342 | "size": 2, 343 | "tick": 6, 344 | "tick_modifier": null 345 | }, 346 | { 347 | "code": 209, 348 | "mode": "IndirectY", 349 | "name": "CMP", 350 | "size": 2, 351 | "tick": 5, 352 | "tick_modifier": "PageCrossed" 353 | }, 354 | { 355 | "code": 224, 356 | "mode": "Immediate", 357 | "name": "CPX", 358 | "size": 2, 359 | "tick": 2, 360 | "tick_modifier": null 361 | }, 362 | { 363 | "code": 228, 364 | "mode": "ZeroPage", 365 | "name": "CPX", 366 | "size": 2, 367 | "tick": 3, 368 | "tick_modifier": null 369 | }, 370 | { 371 | "code": 236, 372 | "mode": "Absolute", 373 | "name": "CPX", 374 | "size": 3, 375 | "tick": 4, 376 | "tick_modifier": null 377 | }, 378 | { 379 | "code": 192, 380 | "mode": "Immediate", 381 | "name": "CPY", 382 | "size": 2, 383 | "tick": 2, 384 | "tick_modifier": null 385 | }, 386 | { 387 | "code": 196, 388 | "mode": "ZeroPage", 389 | "name": "CPY", 390 | "size": 2, 391 | "tick": 3, 392 | "tick_modifier": null 393 | }, 394 | { 395 | "code": 204, 396 | "mode": "Absolute", 397 | "name": "CPY", 398 | "size": 3, 399 | "tick": 4, 400 | "tick_modifier": null 401 | }, 402 | { 403 | "code": 198, 404 | "mode": "ZeroPage", 405 | "name": "DEC", 406 | "size": 2, 407 | "tick": 5, 408 | "tick_modifier": null 409 | }, 410 | { 411 | "code": 214, 412 | "mode": "ZeroPageX", 413 | "name": "DEC", 414 | "size": 2, 415 | "tick": 6, 416 | "tick_modifier": null 417 | }, 418 | { 419 | "code": 206, 420 | "mode": "Absolute", 421 | "name": "DEC", 422 | "size": 3, 423 | "tick": 6, 424 | "tick_modifier": null 425 | }, 426 | { 427 | "code": 222, 428 | "mode": "AbsoluteX", 429 | "name": "DEC", 430 | "size": 3, 431 | "tick": 7, 432 | "tick_modifier": null 433 | }, 434 | { 435 | "code": 202, 436 | "mode": "Implied", 437 | "name": "DEX", 438 | "size": 1, 439 | "tick": 2, 440 | "tick_modifier": null 441 | }, 442 | { 443 | "code": 136, 444 | "mode": "Implied", 445 | "name": "DEY", 446 | "size": 1, 447 | "tick": 2, 448 | "tick_modifier": null 449 | }, 450 | { 451 | "code": 73, 452 | "mode": "Immediate", 453 | "name": "EOR", 454 | "size": 2, 455 | "tick": 2, 456 | "tick_modifier": null 457 | }, 458 | { 459 | "code": 69, 460 | "mode": "ZeroPage", 461 | "name": "EOR", 462 | "size": 2, 463 | "tick": 3, 464 | "tick_modifier": null 465 | }, 466 | { 467 | "code": 85, 468 | "mode": "ZeroPageX", 469 | "name": "EOR", 470 | "size": 2, 471 | "tick": 4, 472 | "tick_modifier": null 473 | }, 474 | { 475 | "code": 77, 476 | "mode": "Absolute", 477 | "name": "EOR", 478 | "size": 3, 479 | "tick": 4, 480 | "tick_modifier": null 481 | }, 482 | { 483 | "code": 93, 484 | "mode": "AbsoluteX", 485 | "name": "EOR", 486 | "size": 3, 487 | "tick": 4, 488 | "tick_modifier": "PageCrossed" 489 | }, 490 | { 491 | "code": 89, 492 | "mode": "AbsoluteY", 493 | "name": "EOR", 494 | "size": 3, 495 | "tick": 4, 496 | "tick_modifier": "PageCrossed" 497 | }, 498 | { 499 | "code": 65, 500 | "mode": "IndirectX", 501 | "name": "EOR", 502 | "size": 2, 503 | "tick": 6, 504 | "tick_modifier": null 505 | }, 506 | { 507 | "code": 81, 508 | "mode": "IndirectY", 509 | "name": "EOR", 510 | "size": 2, 511 | "tick": 5, 512 | "tick_modifier": "PageCrossed" 513 | }, 514 | { 515 | "code": 230, 516 | "mode": "ZeroPage", 517 | "name": "INC", 518 | "size": 2, 519 | "tick": 5, 520 | "tick_modifier": null 521 | }, 522 | { 523 | "code": 246, 524 | "mode": "ZeroPageX", 525 | "name": "INC", 526 | "size": 2, 527 | "tick": 6, 528 | "tick_modifier": null 529 | }, 530 | { 531 | "code": 238, 532 | "mode": "Absolute", 533 | "name": "INC", 534 | "size": 3, 535 | "tick": 6, 536 | "tick_modifier": null 537 | }, 538 | { 539 | "code": 254, 540 | "mode": "AbsoluteX", 541 | "name": "INC", 542 | "size": 3, 543 | "tick": 7, 544 | "tick_modifier": null 545 | }, 546 | { 547 | "code": 232, 548 | "mode": "Implied", 549 | "name": "INX", 550 | "size": 1, 551 | "tick": 2, 552 | "tick_modifier": null 553 | }, 554 | { 555 | "code": 200, 556 | "mode": "Implied", 557 | "name": "INY", 558 | "size": 1, 559 | "tick": 2, 560 | "tick_modifier": null 561 | }, 562 | { 563 | "code": 76, 564 | "mode": "Absolute", 565 | "name": "JMP", 566 | "size": 3, 567 | "tick": 3, 568 | "tick_modifier": null 569 | }, 570 | { 571 | "code": 108, 572 | "mode": "Indirect", 573 | "name": "JMP", 574 | "size": 3, 575 | "tick": 5, 576 | "tick_modifier": null 577 | }, 578 | { 579 | "code": 32, 580 | "mode": "Absolute", 581 | "name": "JSR", 582 | "size": 3, 583 | "tick": 6, 584 | "tick_modifier": null 585 | }, 586 | { 587 | "code": 169, 588 | "mode": "Immediate", 589 | "name": "LDA", 590 | "size": 2, 591 | "tick": 2, 592 | "tick_modifier": null 593 | }, 594 | { 595 | "code": 165, 596 | "mode": "ZeroPage", 597 | "name": "LDA", 598 | "size": 2, 599 | "tick": 3, 600 | "tick_modifier": null 601 | }, 602 | { 603 | "code": 181, 604 | "mode": "ZeroPageX", 605 | "name": "LDA", 606 | "size": 2, 607 | "tick": 4, 608 | "tick_modifier": null 609 | }, 610 | { 611 | "code": 173, 612 | "mode": "Absolute", 613 | "name": "LDA", 614 | "size": 3, 615 | "tick": 4, 616 | "tick_modifier": null 617 | }, 618 | { 619 | "code": 189, 620 | "mode": "AbsoluteX", 621 | "name": "LDA", 622 | "size": 3, 623 | "tick": 4, 624 | "tick_modifier": "PageCrossed" 625 | }, 626 | { 627 | "code": 185, 628 | "mode": "AbsoluteY", 629 | "name": "LDA", 630 | "size": 3, 631 | "tick": 4, 632 | "tick_modifier": "PageCrossed" 633 | }, 634 | { 635 | "code": 161, 636 | "mode": "IndirectX", 637 | "name": "LDA", 638 | "size": 2, 639 | "tick": 6, 640 | "tick_modifier": null 641 | }, 642 | { 643 | "code": 177, 644 | "mode": "IndirectY", 645 | "name": "LDA", 646 | "size": 2, 647 | "tick": 5, 648 | "tick_modifier": "PageCrossed" 649 | }, 650 | { 651 | "code": 162, 652 | "mode": "Immediate", 653 | "name": "LDX", 654 | "size": 2, 655 | "tick": 2, 656 | "tick_modifier": null 657 | }, 658 | { 659 | "code": 166, 660 | "mode": "ZeroPage", 661 | "name": "LDX", 662 | "size": 2, 663 | "tick": 3, 664 | "tick_modifier": null 665 | }, 666 | { 667 | "code": 182, 668 | "mode": "ZeroPageY", 669 | "name": "LDX", 670 | "size": 2, 671 | "tick": 4, 672 | "tick_modifier": null 673 | }, 674 | { 675 | "code": 174, 676 | "mode": "Absolute", 677 | "name": "LDX", 678 | "size": 3, 679 | "tick": 4, 680 | "tick_modifier": null 681 | }, 682 | { 683 | "code": 190, 684 | "mode": "AbsoluteY", 685 | "name": "LDX", 686 | "size": 3, 687 | "tick": 4, 688 | "tick_modifier": "PageCrossed" 689 | }, 690 | { 691 | "code": 160, 692 | "mode": "Immediate", 693 | "name": "LDY", 694 | "size": 2, 695 | "tick": 2, 696 | "tick_modifier": null 697 | }, 698 | { 699 | "code": 164, 700 | "mode": "ZeroPage", 701 | "name": "LDY", 702 | "size": 2, 703 | "tick": 3, 704 | "tick_modifier": null 705 | }, 706 | { 707 | "code": 180, 708 | "mode": "ZeroPageX", 709 | "name": "LDY", 710 | "size": 2, 711 | "tick": 4, 712 | "tick_modifier": null 713 | }, 714 | { 715 | "code": 172, 716 | "mode": "Absolute", 717 | "name": "LDY", 718 | "size": 3, 719 | "tick": 4, 720 | "tick_modifier": null 721 | }, 722 | { 723 | "code": 188, 724 | "mode": "AbsoluteX", 725 | "name": "LDY", 726 | "size": 3, 727 | "tick": 4, 728 | "tick_modifier": "PageCrossed" 729 | }, 730 | { 731 | "code": 74, 732 | "mode": "Accumulator", 733 | "name": "LSR", 734 | "size": 1, 735 | "tick": 2, 736 | "tick_modifier": null 737 | }, 738 | { 739 | "code": 70, 740 | "mode": "ZeroPage", 741 | "name": "LSR", 742 | "size": 2, 743 | "tick": 5, 744 | "tick_modifier": null 745 | }, 746 | { 747 | "code": 86, 748 | "mode": "ZeroPageX", 749 | "name": "LSR", 750 | "size": 2, 751 | "tick": 6, 752 | "tick_modifier": null 753 | }, 754 | { 755 | "code": 78, 756 | "mode": "Absolute", 757 | "name": "LSR", 758 | "size": 3, 759 | "tick": 6, 760 | "tick_modifier": null 761 | }, 762 | { 763 | "code": 94, 764 | "mode": "AbsoluteX", 765 | "name": "LSR", 766 | "size": 3, 767 | "tick": 7, 768 | "tick_modifier": null 769 | }, 770 | { 771 | "code": 234, 772 | "mode": "Implied", 773 | "name": "NOP", 774 | "size": 1, 775 | "tick": 2, 776 | "tick_modifier": null 777 | }, 778 | { 779 | "code": 9, 780 | "mode": "Immediate", 781 | "name": "ORA", 782 | "size": 2, 783 | "tick": 2, 784 | "tick_modifier": null 785 | }, 786 | { 787 | "code": 5, 788 | "mode": "ZeroPage", 789 | "name": "ORA", 790 | "size": 2, 791 | "tick": 3, 792 | "tick_modifier": null 793 | }, 794 | { 795 | "code": 21, 796 | "mode": "ZeroPageX", 797 | "name": "ORA", 798 | "size": 2, 799 | "tick": 4, 800 | "tick_modifier": null 801 | }, 802 | { 803 | "code": 13, 804 | "mode": "Absolute", 805 | "name": "ORA", 806 | "size": 3, 807 | "tick": 4, 808 | "tick_modifier": null 809 | }, 810 | { 811 | "code": 29, 812 | "mode": "AbsoluteX", 813 | "name": "ORA", 814 | "size": 3, 815 | "tick": 4, 816 | "tick_modifier": "PageCrossed" 817 | }, 818 | { 819 | "code": 25, 820 | "mode": "AbsoluteY", 821 | "name": "ORA", 822 | "size": 3, 823 | "tick": 4, 824 | "tick_modifier": "PageCrossed" 825 | }, 826 | { 827 | "code": 1, 828 | "mode": "IndirectX", 829 | "name": "ORA", 830 | "size": 2, 831 | "tick": 6, 832 | "tick_modifier": null 833 | }, 834 | { 835 | "code": 17, 836 | "mode": "IndirectY", 837 | "name": "ORA", 838 | "size": 2, 839 | "tick": 5, 840 | "tick_modifier": "PageCrossed" 841 | }, 842 | { 843 | "code": 72, 844 | "mode": "Implied", 845 | "name": "PHA", 846 | "size": 1, 847 | "tick": 3, 848 | "tick_modifier": null 849 | }, 850 | { 851 | "code": 8, 852 | "mode": "Implied", 853 | "name": "PHP", 854 | "size": 1, 855 | "tick": 3, 856 | "tick_modifier": null 857 | }, 858 | { 859 | "code": 104, 860 | "mode": "Implied", 861 | "name": "PLA", 862 | "size": 1, 863 | "tick": 4, 864 | "tick_modifier": null 865 | }, 866 | { 867 | "code": 40, 868 | "mode": "Implied", 869 | "name": "PLP", 870 | "size": 1, 871 | "tick": 4, 872 | "tick_modifier": null 873 | }, 874 | { 875 | "code": 42, 876 | "mode": "Accumulator", 877 | "name": "ROL", 878 | "size": 1, 879 | "tick": 2, 880 | "tick_modifier": null 881 | }, 882 | { 883 | "code": 38, 884 | "mode": "ZeroPage", 885 | "name": "ROL", 886 | "size": 2, 887 | "tick": 5, 888 | "tick_modifier": null 889 | }, 890 | { 891 | "code": 54, 892 | "mode": "ZeroPageX", 893 | "name": "ROL", 894 | "size": 2, 895 | "tick": 6, 896 | "tick_modifier": null 897 | }, 898 | { 899 | "code": 46, 900 | "mode": "Absolute", 901 | "name": "ROL", 902 | "size": 3, 903 | "tick": 6, 904 | "tick_modifier": null 905 | }, 906 | { 907 | "code": 62, 908 | "mode": "AbsoluteX", 909 | "name": "ROL", 910 | "size": 3, 911 | "tick": 7, 912 | "tick_modifier": null 913 | }, 914 | { 915 | "code": 106, 916 | "mode": "Accumulator", 917 | "name": "ROR", 918 | "size": 1, 919 | "tick": 2, 920 | "tick_modifier": null 921 | }, 922 | { 923 | "code": 102, 924 | "mode": "ZeroPage", 925 | "name": "ROR", 926 | "size": 2, 927 | "tick": 5, 928 | "tick_modifier": null 929 | }, 930 | { 931 | "code": 118, 932 | "mode": "ZeroPageX", 933 | "name": "ROR", 934 | "size": 2, 935 | "tick": 6, 936 | "tick_modifier": null 937 | }, 938 | { 939 | "code": 110, 940 | "mode": "Absolute", 941 | "name": "ROR", 942 | "size": 3, 943 | "tick": 6, 944 | "tick_modifier": null 945 | }, 946 | { 947 | "code": 126, 948 | "mode": "AbsoluteX", 949 | "name": "ROR", 950 | "size": 3, 951 | "tick": 7, 952 | "tick_modifier": null 953 | }, 954 | { 955 | "code": 64, 956 | "mode": "Implied", 957 | "name": "RTI", 958 | "size": 1, 959 | "tick": 6, 960 | "tick_modifier": null 961 | }, 962 | { 963 | "code": 96, 964 | "mode": "Implied", 965 | "name": "RTS", 966 | "size": 1, 967 | "tick": 6, 968 | "tick_modifier": null 969 | }, 970 | { 971 | "code": 233, 972 | "mode": "Immediate", 973 | "name": "SBC", 974 | "size": 2, 975 | "tick": 2, 976 | "tick_modifier": null 977 | }, 978 | { 979 | "code": 229, 980 | "mode": "ZeroPage", 981 | "name": "SBC", 982 | "size": 2, 983 | "tick": 3, 984 | "tick_modifier": null 985 | }, 986 | { 987 | "code": 245, 988 | "mode": "ZeroPageX", 989 | "name": "SBC", 990 | "size": 2, 991 | "tick": 4, 992 | "tick_modifier": null 993 | }, 994 | { 995 | "code": 237, 996 | "mode": "Absolute", 997 | "name": "SBC", 998 | "size": 3, 999 | "tick": 4, 1000 | "tick_modifier": null 1001 | }, 1002 | { 1003 | "code": 253, 1004 | "mode": "AbsoluteX", 1005 | "name": "SBC", 1006 | "size": 3, 1007 | "tick": 4, 1008 | "tick_modifier": "PageCrossed" 1009 | }, 1010 | { 1011 | "code": 249, 1012 | "mode": "AbsoluteY", 1013 | "name": "SBC", 1014 | "size": 3, 1015 | "tick": 4, 1016 | "tick_modifier": "PageCrossed" 1017 | }, 1018 | { 1019 | "code": 225, 1020 | "mode": "IndirectX", 1021 | "name": "SBC", 1022 | "size": 2, 1023 | "tick": 6, 1024 | "tick_modifier": null 1025 | }, 1026 | { 1027 | "code": 241, 1028 | "mode": "IndirectY", 1029 | "name": "SBC", 1030 | "size": 2, 1031 | "tick": 5, 1032 | "tick_modifier": "PageCrossed" 1033 | }, 1034 | { 1035 | "code": 56, 1036 | "mode": "Implied", 1037 | "name": "SEC", 1038 | "size": 1, 1039 | "tick": 2, 1040 | "tick_modifier": null 1041 | }, 1042 | { 1043 | "code": 248, 1044 | "mode": "Implied", 1045 | "name": "SED", 1046 | "size": 1, 1047 | "tick": 2, 1048 | "tick_modifier": null 1049 | }, 1050 | { 1051 | "code": 120, 1052 | "mode": "Implied", 1053 | "name": "SEI", 1054 | "size": 1, 1055 | "tick": 2, 1056 | "tick_modifier": null 1057 | }, 1058 | { 1059 | "code": 133, 1060 | "mode": "ZeroPage", 1061 | "name": "STA", 1062 | "size": 2, 1063 | "tick": 3, 1064 | "tick_modifier": null 1065 | }, 1066 | { 1067 | "code": 149, 1068 | "mode": "ZeroPageX", 1069 | "name": "STA", 1070 | "size": 2, 1071 | "tick": 4, 1072 | "tick_modifier": null 1073 | }, 1074 | { 1075 | "code": 141, 1076 | "mode": "Absolute", 1077 | "name": "STA", 1078 | "size": 3, 1079 | "tick": 4, 1080 | "tick_modifier": null 1081 | }, 1082 | { 1083 | "code": 157, 1084 | "mode": "AbsoluteX", 1085 | "name": "STA", 1086 | "size": 3, 1087 | "tick": 5, 1088 | "tick_modifier": null 1089 | }, 1090 | { 1091 | "code": 153, 1092 | "mode": "AbsoluteY", 1093 | "name": "STA", 1094 | "size": 3, 1095 | "tick": 5, 1096 | "tick_modifier": null 1097 | }, 1098 | { 1099 | "code": 129, 1100 | "mode": "IndirectX", 1101 | "name": "STA", 1102 | "size": 2, 1103 | "tick": 6, 1104 | "tick_modifier": null 1105 | }, 1106 | { 1107 | "code": 145, 1108 | "mode": "IndirectY", 1109 | "name": "STA", 1110 | "size": 2, 1111 | "tick": 6, 1112 | "tick_modifier": null 1113 | }, 1114 | { 1115 | "code": 134, 1116 | "mode": "ZeroPage", 1117 | "name": "STX", 1118 | "size": 2, 1119 | "tick": 3, 1120 | "tick_modifier": null 1121 | }, 1122 | { 1123 | "code": 150, 1124 | "mode": "ZeroPageY", 1125 | "name": "STX", 1126 | "size": 2, 1127 | "tick": 4, 1128 | "tick_modifier": null 1129 | }, 1130 | { 1131 | "code": 142, 1132 | "mode": "Absolute", 1133 | "name": "STX", 1134 | "size": 3, 1135 | "tick": 4, 1136 | "tick_modifier": null 1137 | }, 1138 | { 1139 | "code": 132, 1140 | "mode": "ZeroPage", 1141 | "name": "STY", 1142 | "size": 2, 1143 | "tick": 3, 1144 | "tick_modifier": null 1145 | }, 1146 | { 1147 | "code": 148, 1148 | "mode": "ZeroPageX", 1149 | "name": "STY", 1150 | "size": 2, 1151 | "tick": 4, 1152 | "tick_modifier": null 1153 | }, 1154 | { 1155 | "code": 140, 1156 | "mode": "Absolute", 1157 | "name": "STY", 1158 | "size": 3, 1159 | "tick": 4, 1160 | "tick_modifier": null 1161 | }, 1162 | { 1163 | "code": 170, 1164 | "mode": "Implied", 1165 | "name": "TAX", 1166 | "size": 1, 1167 | "tick": 2, 1168 | "tick_modifier": null 1169 | }, 1170 | { 1171 | "code": 168, 1172 | "mode": "Implied", 1173 | "name": "TAY", 1174 | "size": 1, 1175 | "tick": 2, 1176 | "tick_modifier": null 1177 | }, 1178 | { 1179 | "code": 186, 1180 | "mode": "Implied", 1181 | "name": "TSX", 1182 | "size": 1, 1183 | "tick": 2, 1184 | "tick_modifier": null 1185 | }, 1186 | { 1187 | "code": 138, 1188 | "mode": "Implied", 1189 | "name": "TXA", 1190 | "size": 1, 1191 | "tick": 2, 1192 | "tick_modifier": null 1193 | }, 1194 | { 1195 | "code": 154, 1196 | "mode": "Implied", 1197 | "name": "TXS", 1198 | "size": 1, 1199 | "tick": 2, 1200 | "tick_modifier": null 1201 | }, 1202 | { 1203 | "code": 152, 1204 | "mode": "Implied", 1205 | "name": "TYA", 1206 | "size": 1, 1207 | "tick": 2, 1208 | "tick_modifier": null 1209 | } 1210 | ] 1211 | --------------------------------------------------------------------------------