diff --git a/.github/workflows/sirc-vm.yml b/.github/workflows/sirc-vm.yml index 196bdea1..487e5bc7 100644 --- a/.github/workflows/sirc-vm.yml +++ b/.github/workflows/sirc-vm.yml @@ -33,6 +33,12 @@ jobs: - name: Clippy run: cargo clippy --all-targets --all-features -- -D warnings + # Currently separate until llvm-cov supports them + - name: Run doctests + run: cargo test --doc --verbose --all-features --workspace + + # TODO: Include doc tests in coverage once it is stabilised + # https://github.com/taiki-e/cargo-llvm-cov/issues/2 - name: Run tests run: cargo llvm-cov --verbose --all-targets --all-features --workspace --codecov --output-path codecov-main.json @@ -95,4 +101,4 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos files: codecov-*.json - fail_ci_if_error: true \ No newline at end of file + fail_ci_if_error: true diff --git a/sirc-vm/device-debug/src/lib.rs b/sirc-vm/device-debug/src/lib.rs index e1adf107..deae6186 100644 --- a/sirc-vm/device-debug/src/lib.rs +++ b/sirc-vm/device-debug/src/lib.rs @@ -1,6 +1,7 @@ use std::any::Any; use log::debug; +use peripheral_bus::memory_mapped_device::MemoryMapped; use peripheral_bus::{ device::BusAssertions, device::Device, memory_mapped_device::MemoryMappedDevice, }; @@ -53,7 +54,7 @@ impl Device for DebugDevice { } } -impl MemoryMappedDevice for DebugDevice { +impl MemoryMapped for DebugDevice { fn read_address(&self, address: u32) -> u16 { debug!("Reading from address 0x{address:X}"); match address { @@ -78,3 +79,5 @@ impl MemoryMappedDevice for DebugDevice { } } } + +impl MemoryMappedDevice for DebugDevice {} diff --git a/sirc-vm/device-ram/src/lib.rs b/sirc-vm/device-ram/src/lib.rs index 03107598..48ec2f60 100644 --- a/sirc-vm/device-ram/src/lib.rs +++ b/sirc-vm/device-ram/src/lib.rs @@ -6,6 +6,7 @@ use std::{ }; use memmap::{MmapMut, MmapOptions}; +use peripheral_bus::memory_mapped_device::MemoryMapped; use peripheral_bus::{ device::BusAssertions, device::Device, memory_mapped_device::MemoryMappedDevice, }; @@ -61,7 +62,7 @@ impl Device for RamDevice { } } -impl MemoryMappedDevice for RamDevice { +impl MemoryMapped for RamDevice { fn read_address(&self, address: u32) -> u16 { let address_pointer = address as usize * 2; @@ -91,7 +92,9 @@ impl MemoryMappedDevice for RamDevice { raw_memory[address_pointer] = byte_pair[0]; raw_memory[address_pointer + 1] = byte_pair[1]; } +} +impl MemoryMappedDevice for RamDevice { fn read_raw_bytes(&self, limit: u32) -> Vec { let cell = self.mem_cell.borrow(); diff --git a/sirc-vm/device-terminal/src/lib.rs b/sirc-vm/device-terminal/src/lib.rs index 80b02dee..f01ced87 100644 --- a/sirc-vm/device-terminal/src/lib.rs +++ b/sirc-vm/device-terminal/src/lib.rs @@ -12,7 +12,7 @@ use log::error; use peripheral_bus::device::BusAssertions; use peripheral_bus::device::Device; -use peripheral_bus::memory_mapped_device::MemoryMappedDevice; +use peripheral_bus::memory_mapped_device::{MemoryMapped, MemoryMappedDevice}; use std::any::Any; use std::collections::VecDeque; @@ -157,8 +157,7 @@ impl Device for TerminalDevice { } } -#[allow(clippy::cast_possible_truncation)] -impl MemoryMappedDevice for TerminalDevice { +impl MemoryMapped for TerminalDevice { fn read_address(&self, address: u32) -> u16 { match address { 0x0 => self.control_registers.baud, @@ -186,3 +185,5 @@ impl MemoryMappedDevice for TerminalDevice { } } } + +impl MemoryMappedDevice for TerminalDevice {} diff --git a/sirc-vm/device-video/Cargo.toml b/sirc-vm/device-video/Cargo.toml index 8d2a61b8..d17c5be9 100644 --- a/sirc-vm/device-video/Cargo.toml +++ b/sirc-vm/device-video/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" [dependencies] peripheral_bus = { path = "../peripheral-bus" } log = "0.4.21" -minifb = "0.27" \ No newline at end of file +minifb = "0.27" +modular-bitfield = "0.11.2" \ No newline at end of file diff --git a/sirc-vm/device-video/src/lib.rs b/sirc-vm/device-video/src/lib.rs index 2f80645b..b15e14da 100644 --- a/sirc-vm/device-video/src/lib.rs +++ b/sirc-vm/device-video/src/lib.rs @@ -1,11 +1,14 @@ +mod types; + use std::{any::Any, time::Duration}; +use crate::types::PpuRegisters; use log::{debug, info}; use minifb::{Window, WindowOptions}; +use peripheral_bus::memory_mapped_device::MemoryMapped; use peripheral_bus::{ device::BusAssertions, device::Device, memory_mapped_device::MemoryMappedDevice, }; - // Some reference: // https://www.raphnet.net/divers/retro_challenge_2019_03/qsnesdoc.html#Reg2115 // https://martin.hinner.info/vga/pal.html#:~:text=PAL%20details&text=CCIR%2FPAL%20standard%20video%20signal,Thus%20field%20rate%20is%2050. @@ -55,6 +58,7 @@ pub struct VideoDevice { // Native vram: Vec, + ppu_registers: PpuRegisters, // Other frame_count: usize, @@ -124,6 +128,7 @@ pub fn new_video_device(master_clock_freq: usize) -> VideoDevice { buffer: vec![0; total_pixels], window, vram: vec![0; VRAM_SIZE], + ppu_registers: PpuRegisters::default(), frame_count: 0, frame_clock: 0, clocks_per_line, @@ -182,14 +187,12 @@ impl Device for VideoDevice { } } -impl MemoryMappedDevice for VideoDevice { +impl MemoryMapped for VideoDevice { fn read_address(&self, address: u32) -> u16 { debug!("Reading from address 0x{address:X}"); match address { // First FF addresses are control registers - // TODO: Design and implement PPU control registers - // category=Features - 0x0000..=0x00FF => 0x0, + 0x0000..=0x00FF => self.ppu_registers.read_address(address), // After that range _ => self.vram[(address as usize) - 0x00FF], } @@ -201,10 +204,12 @@ impl MemoryMappedDevice for VideoDevice { match address { // First FF addresses are control registers 0x0000..=0x00FF => { - // TODO + self.ppu_registers.write_address(address, value); } // After that range _ => self.vram[(address as usize) - 0x00FF] = value, } } } + +impl MemoryMappedDevice for VideoDevice {} diff --git a/sirc-vm/device-video/src/types.rs b/sirc-vm/device-video/src/types.rs new file mode 100644 index 00000000..119fcea6 --- /dev/null +++ b/sirc-vm/device-video/src/types.rs @@ -0,0 +1,200 @@ +// Suppress warnings caused by macro code +#![allow(redundant_semicolons, dead_code)] + +use modular_bitfield::prelude::*; +use peripheral_bus::memory_mapped_device::MemoryMapped; + +const READONLY_STATUS_REGISTER_ADDR: u32 = 0x0013; +// TODO: Use exclusive range in match when rust 1.80 is released +// category=Refactoring +// Then we can get rid of this. See also: https://github.com/rust-lang/rust/issues/37854 +const READONLY_STATUS_REGISTER_ADDR_MINUS_ONE: u32 = READONLY_STATUS_REGISTER_ADDR - 1; +#[bitfield(bytes = 2)] +#[derive(Debug, Default)] +pub struct BaseConfigRegister { + pub graphics_disable: B1, + pub b1_disable: B1, + pub b2_disable: B1, + pub b3_disable: B1, + pub reserved_1: B1, + pub s1_disable: B1, + pub s2_disable: B1, + pub s3_disable: B1, + pub reserved_2: B1, + pub screen_brightness: B4, + pub reserved_3: B3, +} + +#[derive(BitfieldSpecifier, Debug)] +pub enum TileSize { + EightByEight, + SixteenBySixteen, +} + +#[derive(BitfieldSpecifier, Debug)] +pub enum TilemapSize { + Single, + Double, +} + +#[bitfield(bytes = 2)] +#[derive(Debug, Default)] +pub struct TileSizeRegister { + #[bits = 1] + pub b1_size: TileSize, + #[bits = 1] + pub b2_size: TileSize, + #[bits = 1] + pub b3_size: TileSize, + pub reserved_1: B1, + // + #[bits = 1] + pub s1_size: TileSize, + #[bits = 1] + pub s2_size: TileSize, + #[bits = 1] + pub s3_size: TileSize, + pub reserved_2: B1, + // + #[bits = 1] + pub b1_tilemap_size_x: TilemapSize, + #[bits = 1] + pub b2_tilemap_size_x: TilemapSize, + #[bits = 1] + pub b3_tilemap_size_x: TilemapSize, + pub reserved_3: B1, + // + #[bits = 1] + pub b1_tilemap_size_y: TilemapSize, + #[bits = 1] + pub b2_tilemap_size_y: TilemapSize, + #[bits = 1] + pub b3_tilemap_size_y: TilemapSize, + pub reserved_4: B1, +} + +#[bitfield(bytes = 2)] +#[derive(Debug, Default)] +pub struct ScrollRegister { + pub scroll_amount: B10, + pub reserved: B6, +} + +#[derive(BitfieldSpecifier, Debug)] +pub enum OutputMode { + Ntsc, + Pal, +} + +#[bitfield(bytes = 2)] +#[derive(Debug, Default)] +pub struct StatusRegister { + pub ppu_version: B4, + #[bits = 1] + pub output_mode: OutputMode, + pub reserved: B11, +} + +#[derive(Debug, Default)] +pub struct PpuRegisters { + pub base_config: BaseConfigRegister, + pub tile_size: TileSizeRegister, + pub b1_tilemap_addr: u16, + pub b2_tilemap_addr: u16, + pub b3_tilemap_addr: u16, + pub reserved: u16, + pub b1_tile_addr: u16, + pub b2_tile_addr: u16, + pub b3_tile_addr: u16, + pub reserved2: u16, + pub b1_scroll_x: ScrollRegister, + pub b2_scroll_x: ScrollRegister, + pub b3_scroll_x: ScrollRegister, + pub reserved3: u16, + pub b1_scroll_y: ScrollRegister, + pub b2_scroll_y: ScrollRegister, + pub b3_scroll_y: ScrollRegister, + pub reserved4: u16, + pub s_tile_addr: u16, + pub status: StatusRegister, +} + +impl MemoryMapped for PpuRegisters { + fn read_address(&self, address: u32) -> u16 { + // The modular_bitfield crate only provides bytes for raw output, hopefully the compiler can + // optimise it so it doesn't matter + match address { + 0x0000 => u16::from_be_bytes(self.base_config.bytes), + 0x0001 => u16::from_be_bytes(self.tile_size.bytes), + 0x0002 => self.b1_tilemap_addr, + 0x0003 => self.b2_tilemap_addr, + 0x0004 => self.b3_tilemap_addr, + 0x0005 => self.reserved, + 0x0006 => self.b1_tile_addr, + 0x0007 => self.b2_tile_addr, + 0x0008 => self.b3_tile_addr, + 0x0009 => self.reserved2, + 0x000A => u16::from_be_bytes(self.b1_scroll_x.bytes), + 0x000B => u16::from_be_bytes(self.b2_scroll_x.bytes), + 0x000C => u16::from_be_bytes(self.b3_scroll_x.bytes), + 0x000D => self.reserved3, + 0x000E => u16::from_be_bytes(self.b1_scroll_y.bytes), + 0x000F => u16::from_be_bytes(self.b2_scroll_y.bytes), + 0x0010 => u16::from_be_bytes(self.b3_scroll_y.bytes), + 0x0011 => self.reserved4, + 0x0012 => self.s_tile_addr, + READONLY_STATUS_REGISTER_ADDR => u16::from_be_bytes(self.status.bytes), + _ => 0x0, // Open bus + } + } + + fn write_address(&mut self, address: u32, value: u16) { + // The modular_bitfield crate only accepts bytes for raw input, hopefully the compiler can + // optimise it so it doesn't matter + let bytes = u16::to_be_bytes(value); + match address { + 0x0000 => self.base_config.bytes = bytes, + 0x0001 => self.tile_size.bytes = bytes, + 0x0002 => self.b1_tilemap_addr = value, + 0x0003 => self.b2_tilemap_addr = value, + 0x0004 => self.b3_tilemap_addr = value, + 0x0005 => self.reserved = value, + 0x0006 => self.b1_tile_addr = value, + 0x0007 => self.b2_tile_addr = value, + 0x0008 => self.b3_tile_addr = value, + 0x0009 => self.reserved2 = value, + 0x000A => self.b1_scroll_x.bytes = bytes, + 0x000B => self.b2_scroll_x.bytes = bytes, + 0x000C => self.b3_scroll_x.bytes = bytes, + 0x000D => self.reserved3 = value, + 0x000E => self.b1_scroll_y.bytes = bytes, + 0x000F => self.b2_scroll_y.bytes = bytes, + 0x0010 => self.b3_scroll_y.bytes = bytes, + 0x0011 => self.reserved4 = value, + 0x0012 => self.s_tile_addr = value, + // 0x0013 Read Only (status) + _ => {} // Open bus + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_address_round_trip() { + let mut ppu_registers = PpuRegisters::default(); + for addr in 0..=0xFF { + let sentinel_value = 0xFF - addr as u16; + ppu_registers.write_address(addr, sentinel_value); + let stored_value = ppu_registers.read_address(addr); + match addr { + 0x0..=READONLY_STATUS_REGISTER_ADDR_MINUS_ONE => { + assert_eq!(sentinel_value, stored_value) + } + _ => assert_eq!(0x0, stored_value), + } + } + } +} diff --git a/sirc-vm/peripheral-bus/src/memory_mapped_device.rs b/sirc-vm/peripheral-bus/src/memory_mapped_device.rs index 7275b1c3..cab7efa9 100644 --- a/sirc-vm/peripheral-bus/src/memory_mapped_device.rs +++ b/sirc-vm/peripheral-bus/src/memory_mapped_device.rs @@ -10,11 +10,13 @@ use crate::{ // The first word is generally used as a "chip select" and the second word is the address used by the device pub const ADDRESS_MASK: u32 = 0x0000_FFFF; -#[allow(clippy::cast_possible_truncation)] -pub trait MemoryMappedDevice: Device { +pub trait MemoryMapped { fn read_address(&self, address: u32) -> u16; fn write_address(&mut self, address: u32, value: u16); +} +#[allow(clippy::cast_possible_truncation)] +pub trait MemoryMappedDevice: MemoryMapped + Device { /// Shortcut to get data out of the device fast for testing/debugging /// Does not represent anything in hardware fn read_raw_bytes(&self, limit: u32) -> Vec { @@ -74,7 +76,7 @@ impl Device for StubMemoryMappedDevice { } } -impl MemoryMappedDevice for StubMemoryMappedDevice { +impl MemoryMapped for StubMemoryMappedDevice { fn read_address(&self, address: u32) -> u16 { self.data[address as usize] } @@ -82,7 +84,9 @@ impl MemoryMappedDevice for StubMemoryMappedDevice { fn write_address(&mut self, address: u32, value: u16) { self.data[address as usize] = value; } +} +impl MemoryMappedDevice for StubMemoryMappedDevice { fn read_raw_bytes(&self, limit: u32) -> Vec { let words = &self.data[..limit as usize]; words_to_bytes(words) diff --git a/sirc-vm/toolchain/src/parsers/instruction.rs b/sirc-vm/toolchain/src/parsers/instruction.rs index 42dff1c6..fa6a1c85 100644 --- a/sirc-vm/toolchain/src/parsers/instruction.rs +++ b/sirc-vm/toolchain/src/parsers/instruction.rs @@ -236,7 +236,7 @@ fn parse_addressing_mode(i: &str) -> AsmResult { // Immediate can have absolute label references (default lower 16 bit) (@label) or (@label.l explicit lower or @label.h higher (segment)) // PC/SP relative can have relative label references (16 bit signed) (@label) - let mut addressing_mode_parser = alt(( + let addressing_mode_parser = alt(( map(parse_immediate_addressing, AddressingMode::Immediate) .context("immediate value (e.g. #3)"), map(parse_direct_register, AddressingMode::DirectRegister).context("register (e.g. x1)"), @@ -275,7 +275,7 @@ fn parse_addressing_mode(i: &str) -> AsmResult { .context("shift definition (e.g. ASR #4)"), )); - addressing_mode_parser(i) + lexeme(addressing_mode_parser)(i) } pub fn parse_instruction_operands1(i: &str) -> AsmResult> { diff --git a/sirc-vm/toolchain/src/parsers/opcodes/arithmetic_immediate.rs b/sirc-vm/toolchain/src/parsers/opcodes/arithmetic_immediate.rs index 35c3e215..34e5ba1e 100644 --- a/sirc-vm/toolchain/src/parsers/opcodes/arithmetic_immediate.rs +++ b/sirc-vm/toolchain/src/parsers/opcodes/arithmetic_immediate.rs @@ -56,7 +56,7 @@ use super::super::shared::AsmResult; /// /// ``` /// use toolchain::parsers::opcodes::arithmetic_immediate::arithmetic_immediate; -/// use toolchain::parsers::instruction::InstructionToken; +/// use toolchain::types::instruction::InstructionToken; /// use peripheral_cpu::coprocessors::processing_unit::definitions::{ConditionFlags, Instruction, InstructionData, ImmediateInstructionData, ShiftType}; /// /// diff --git a/sirc-vm/toolchain/src/parsers/opcodes/arithmetic_register.rs b/sirc-vm/toolchain/src/parsers/opcodes/arithmetic_register.rs index d58f41b2..5f92f7b6 100644 --- a/sirc-vm/toolchain/src/parsers/opcodes/arithmetic_register.rs +++ b/sirc-vm/toolchain/src/parsers/opcodes/arithmetic_register.rs @@ -36,7 +36,7 @@ use super::super::shared::AsmResult; /// /// ``` /// use toolchain::parsers::opcodes::arithmetic_register::arithmetic_register; -/// use toolchain::parsers::instruction::InstructionToken; +/// use toolchain::types::instruction::InstructionToken; /// use peripheral_cpu::coprocessors::processing_unit::definitions::{ConditionFlags, Instruction, InstructionData, RegisterInstructionData, ShiftType}; /// /// let (_, parsed_instruction) = arithmetic_register("ADDR|>= r1, r3, RTL #4\n").unwrap(); diff --git a/sirc-vm/toolchain/src/parsers/opcodes/branching.rs b/sirc-vm/toolchain/src/parsers/opcodes/branching.rs index 6d1bbade..6ae22c4d 100644 --- a/sirc-vm/toolchain/src/parsers/opcodes/branching.rs +++ b/sirc-vm/toolchain/src/parsers/opcodes/branching.rs @@ -43,7 +43,7 @@ use super::super::shared::AsmResult; /// /// ``` /// use toolchain::parsers::opcodes::branching::branching; -/// use toolchain::parsers::instruction::InstructionToken; +/// use toolchain::types::instruction::InstructionToken; /// use peripheral_cpu::coprocessors::processing_unit::definitions::{ConditionFlags, Instruction, InstructionData, RegisterInstructionData, ShiftType}; /// use nom_supreme::error::ErrorTree; /// use nom_supreme::final_parser::{final_parser, Location}; diff --git a/sirc-vm/toolchain/src/parsers/opcodes/implied.rs b/sirc-vm/toolchain/src/parsers/opcodes/implied.rs index 5b6762de..c57d287c 100644 --- a/sirc-vm/toolchain/src/parsers/opcodes/implied.rs +++ b/sirc-vm/toolchain/src/parsers/opcodes/implied.rs @@ -15,7 +15,7 @@ use peripheral_cpu::registers::AddressRegisterName; /// /// ``` /// use toolchain::parsers::opcodes::implied::implied; -/// use toolchain::parsers::instruction::InstructionToken; +/// use toolchain::types::instruction::InstructionToken; /// use peripheral_cpu::coprocessors::processing_unit::definitions::{ConditionFlags, Instruction, InstructionData, RegisterInstructionData}; /// use nom_supreme::error::ErrorTree; /// use nom_supreme::final_parser::{final_parser, Location}; diff --git a/sirc-vm/toolchain/src/parsers/opcodes/ljmp.rs b/sirc-vm/toolchain/src/parsers/opcodes/ljmp.rs index 6f90856a..85059cf7 100644 --- a/sirc-vm/toolchain/src/parsers/opcodes/ljmp.rs +++ b/sirc-vm/toolchain/src/parsers/opcodes/ljmp.rs @@ -20,7 +20,7 @@ use peripheral_cpu::registers::AddressRegisterName; /// /// ``` /// use toolchain::parsers::opcodes::ljmp::ljmp; -/// use toolchain::parsers::instruction::InstructionToken; +/// use toolchain::types::instruction::InstructionToken; /// use peripheral_cpu::coprocessors::processing_unit::definitions::{ConditionFlags, Instruction, InstructionData, RegisterInstructionData, ShiftType}; /// use nom_supreme::error::ErrorTree; /// use nom_supreme::final_parser::{final_parser, Location}; diff --git a/sirc-vm/toolchain/src/parsers/opcodes/ljsr.rs b/sirc-vm/toolchain/src/parsers/opcodes/ljsr.rs index aead78ad..9f539284 100644 --- a/sirc-vm/toolchain/src/parsers/opcodes/ljsr.rs +++ b/sirc-vm/toolchain/src/parsers/opcodes/ljsr.rs @@ -20,7 +20,7 @@ use peripheral_cpu::registers::AddressRegisterName; /// /// ``` /// use toolchain::parsers::opcodes::ljsr::ljsr; -/// use toolchain::parsers::instruction::InstructionToken; +/// use toolchain::types::instruction::InstructionToken; /// use peripheral_cpu::coprocessors::processing_unit::definitions::{ConditionFlags, Instruction, InstructionData, RegisterInstructionData}; /// use nom_supreme::error::ErrorTree; /// use nom_supreme::final_parser::{final_parser, Location};