diff --git a/.gitignore b/.gitignore index a849207..32a613b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ dist/ .vscode/* !.vscode/extensions.json .claude/ -nul \ No newline at end of file +nul + +reference/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index b18383e..df698b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,9 +303,23 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitfield" -version = "0.14.0" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" +checksum = "6bf79f42d21f18b5926a959280215903e659760da994835d27c3a0c5ff4f898f" +dependencies = [ + "bitfield-macros", +] + +[[package]] +name = "bitfield-macros" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6115af052c7914c0cbb97195e5c72cb61c511527250074f5c041d1048b0d8b16" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] [[package]] name = "bitflags" @@ -690,6 +704,7 @@ dependencies = [ "arm7di-core", "bitfield", "cc", + "goblin", "once_cell", "refsw2-cpp", "refsw2-rust", @@ -1151,6 +1166,17 @@ dependencies = [ "system-deps", ] +[[package]] +name = "goblin" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51876e3748c4a347fe65b906f2b1ae46a1e55a497b22c94c1f4f2c469ff7673a" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "gpu-alloc" version = "0.6.0" @@ -2326,6 +2352,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "png" version = "0.17.16" @@ -2669,6 +2701,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scroll" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "sctk-adwaita" version = "0.10.1" diff --git a/crates/arm7di-core/src/lib.rs b/crates/arm7di-core/src/lib.rs index 4dbe5da..783a739 100644 --- a/crates/arm7di-core/src/lib.rs +++ b/crates/arm7di-core/src/lib.rs @@ -1316,7 +1316,8 @@ impl<'a> Arm7Di<'a> { fn exec_branch(&mut self, opcode: u32) -> u32 { let link = opcode & (1 << 24) != 0; - let offset = opcode & 0x00FF_FFFF; + // Sign-extend the 24-bit offset: shift left 10 to move bit 23 to bit 31, + // then arithmetic shift right 8 to sign-extend and multiply by 4 let offset = ((opcode << 10) as i32) >> 8; let next = self.ctx.regs[R15_ARM_NEXT].get(); if link { diff --git a/crates/dreamcast/Cargo.toml b/crates/dreamcast/Cargo.toml index 53663ee..c3b62a6 100644 --- a/crates/dreamcast/Cargo.toml +++ b/crates/dreamcast/Cargo.toml @@ -11,8 +11,9 @@ refsw2-cpp = ["dep:refsw2-cpp"] refsw2-native = ["refsw2-cpp"] [dependencies] -bitfield = "0.14" +bitfield = "0.19.3" once_cell = "1.19" +goblin = "0.10.3" sh4-core = { path = "../sh4-core" } arm7di-core = { path = "../arm7di-core" } refsw2-rust = { path = "../refsw2-rust", optional = true } diff --git a/crates/dreamcast/src/lib.rs b/crates/dreamcast/src/lib.rs index cd29a03..3943432 100644 --- a/crates/dreamcast/src/lib.rs +++ b/crates/dreamcast/src/lib.rs @@ -17,6 +17,7 @@ use std::path::Path; use std::ptr::{self, NonNull}; use std::sync::atomic::{AtomicPtr, Ordering}; use std::sync::Mutex; +use goblin::elf::Elf; mod area0; pub use area0::AREA0_HANDLERS; @@ -230,24 +231,10 @@ fn load_file_into_slice>(path: P, buf: &mut [u8]) -> io::Result<( // pub static HELLO_BIN: &[u8] = include_bytes!("../../../data/hello.elf.bin"); // pub static ARM7W_BIN: &[u8] = include_bytes!("../../../data/arm7wrestler.bin"); -pub fn init_dreamcast(dc_: *mut Dreamcast, bios_rom: &[u8], bios_flash: &[u8]) { - let dc: &mut Dreamcast; - unsafe { - dc = &mut *dc_; - } - - DREAMCAST_PTR.store(dc as *mut Dreamcast, Ordering::SeqCst); - +fn reset_dreamcast_to_defaults(dc: &mut Dreamcast) { // Zero entire struct (like memset). In Rust, usually you'd implement Default. *dc = Dreamcast::default(); - // Copy BIOS ROM and Flash from provided slices - assert_eq!(bios_rom.len(), BIOS_ROM_SIZE as usize, "BIOS ROM must be exactly 2MB"); - assert_eq!(bios_flash.len(), BIOS_FLASH_SIZE as usize, "BIOS Flash must be exactly 128KB"); - - dc.bios_rom[..].copy_from_slice(bios_rom); - dc.bios_flash[..].copy_from_slice(bios_flash); - sh4_init_ctx(&mut dc.ctx); refsw2::refsw2_init(); @@ -409,6 +396,25 @@ pub fn init_dreamcast(dc_: *mut Dreamcast, bios_rom: &[u8], bios_flash: &[u8]) { dc.ctx.sr_t = 1; dc.ctx.fpscr.0 = 0x00040001; +} + +pub fn init_dreamcast(dc_: *mut Dreamcast, bios_rom: &[u8], bios_flash: &[u8]) { + let dc: &mut Dreamcast; + unsafe { + dc = &mut *dc_; + } + + DREAMCAST_PTR.store(dc as *mut Dreamcast, Ordering::SeqCst); + + reset_dreamcast_to_defaults(dc); + + + // Copy BIOS ROM and Flash from provided slices + assert_eq!(bios_rom.len(), BIOS_ROM_SIZE as usize, "BIOS ROM must be exactly 2MB"); + assert_eq!(bios_flash.len(), BIOS_FLASH_SIZE as usize, "BIOS Flash must be exactly 128KB"); + + dc.bios_rom[..].copy_from_slice(bios_rom); + dc.bios_flash[..].copy_from_slice(bios_flash); // ROTO test program at 0x8C010000 // dc.ctx.pc0 = 0x8C01_0000; @@ -670,3 +676,114 @@ pub fn get_arm_register(dc: *mut Dreamcast, register_name: &str) -> Option } } } + +pub fn init_dreamcast_with_elf(dc: *mut Dreamcast, elf_bytes: &[u8]) -> Result<(), String> { + // Parse the ELF file + let elf = Elf::parse(elf_bytes) + .map_err(|e| format!("Failed to parse ELF file: {}", e))?; + + unsafe { + let dc_ref = &mut *dc; + + reset_dreamcast_to_defaults(dc_ref); + + // Iterate through program headers and load PT_LOAD segments + for ph in &elf.program_headers { + // Only load PT_LOAD segments + if ph.p_type != goblin::elf::program_header::PT_LOAD { + continue; + } + + let vaddr = ph.p_vaddr as u32; + let memsz = ph.p_memsz as usize; + let filesz = ph.p_filesz as usize; + let offset = ph.p_offset as usize; + + println!("Loading ELF segment: vaddr=0x{:08X}, memsz=0x{:X}, filesz=0x{:X}", + vaddr, memsz, filesz); + + // Determine which memory region this address belongs to + let (dest_ptr, mask) = match vaddr { + // System RAM (mirrored across different regions) + 0x0C00_0000..=0x0FFF_FFFF | + 0x8C00_0000..=0x8FFF_FFFF | + 0xAC00_0000..=0xAFFF_FFFF => { + (dc_ref.sys_ram.as_mut_ptr(), SYSRAM_MASK) + } + // Video RAM + 0x0400_0000..=0x04FF_FFFF | + 0xA400_0000..=0xA4FF_FFFF | + 0x0500_0000..=0x05FF_FFFF | + 0xA500_0000..=0xA5FF_FFFF | + 0x0600_0000..=0x06FF_FFFF | + 0xA600_0000..=0xA6FF_FFFF | + 0x0700_0000..=0x07FF_FFFF | + 0xA700_0000..=0xA7FF_FFFF => { + (dc_ref.video_ram.as_mut_ptr(), VIDEORAM_MASK) + } + // Audio RAM + 0x0080_0000..=0x009F_FFFF | + 0x8080_0000..=0x809F_FFFF | + 0xA080_0000..=0xA09F_FFFF => { + (dc_ref.audio_ram.as_mut_ptr(), AUDIORAM_MASK) + } + // On-chip RAM + 0x7C00_0000..=0x7FFF_FFFF => { + (dc_ref.oc_ram.as_mut_ptr(), OCRAM_MASK) + } + _ => { + return Err(format!("ELF segment virtual address 0x{:08X} is not in a valid memory region", vaddr)); + } + }; + + // Calculate the offset into the destination memory + let dest_offset = (vaddr & mask) as usize; + + // Check if the segment fits in the memory region + if dest_offset + memsz > (mask as usize + 1) { + return Err(format!("ELF segment at 0x{:08X} with size 0x{:X} exceeds memory bounds", + vaddr, memsz)); + } + + // Copy the file data + if filesz > 0 { + // Validate that we have enough data in the ELF file + if offset + filesz > elf_bytes.len() { + return Err(format!("ELF segment data at offset 0x{:X} with size 0x{:X} exceeds file bounds", + offset, filesz)); + } + + let src_slice = &elf_bytes[offset..offset + filesz]; + + ptr::copy_nonoverlapping( + src_slice.as_ptr(), + dest_ptr.add(dest_offset), + filesz + ); + } + + // Zero out remaining bytes (BSS sections) + if memsz > filesz { + let bss_size = memsz - filesz; + ptr::write_bytes( + dest_ptr.add(dest_offset + filesz), + 0, + bss_size + ); + } + } + + DREAMCAST_PTR.store(dc, Ordering::SeqCst); + + + // Set PC to entry point if valid + if elf.entry > 0 { + let entry = elf.entry as u32; + println!("Setting entry point to 0x{:08X}", entry); + dc_ref.ctx.pc0 = entry; + dc_ref.ctx.pc1 = entry + 2; + dc_ref.ctx.pc2 = entry + 4; + } + Ok(()) + } +} diff --git a/crates/nulldc-minimal/src/main.rs b/crates/nulldc-minimal/src/main.rs index 4ad84c2..49c1676 100644 --- a/crates/nulldc-minimal/src/main.rs +++ b/crates/nulldc-minimal/src/main.rs @@ -25,6 +25,8 @@ fn load_bios_files() -> (Vec, Vec) { } fn main() { + let args: Vec = std::env::args().collect(); + let (bios_rom, bios_flash) = load_bios_files(); // Create window @@ -45,6 +47,20 @@ fn main() { let dc = Box::into_raw(Box::new(dreamcast::Dreamcast::default())); dreamcast::init_dreamcast(dc, &bios_rom, &bios_flash); + // Load ELF if provided as command line argument + if args.len() > 1 { + let elf_path = &args[1]; + println!("Loading ELF file: {}", elf_path); + + let elf_data = fs::read(elf_path) + .unwrap_or_else(|e| panic!("Failed to load ELF file from {}: {}", elf_path, e)); + + dreamcast::init_dreamcast_with_elf(dc, &elf_data) + .unwrap_or_else(|e| panic!("Failed to load ELF: {}", e)); + + println!("ELF file loaded successfully"); + } + // Framebuffer for minifb (ARGB format) let mut buffer: Vec = vec![0; WIDTH * HEIGHT]; diff --git a/crates/refsw2-rust/Cargo.toml b/crates/refsw2-rust/Cargo.toml index 1a87f82..5ebb6f0 100644 --- a/crates/refsw2-rust/Cargo.toml +++ b/crates/refsw2-rust/Cargo.toml @@ -4,6 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] -bitfield = "0.14" +bitfield = "0.19.3" [build-dependencies] diff --git a/crates/sh4-core/Cargo.toml b/crates/sh4-core/Cargo.toml index eb995dc..dde60d3 100644 --- a/crates/sh4-core/Cargo.toml +++ b/crates/sh4-core/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -bitfield = "0.14" +bitfield = "0.19.3" paste = "1.0" seq-macro = "0.3" diff --git a/crates/sh4-core/src/sh4p4.rs b/crates/sh4-core/src/sh4p4.rs index 9fb4639..746744c 100644 --- a/crates/sh4-core/src/sh4p4.rs +++ b/crates/sh4-core/src/sh4p4.rs @@ -1734,14 +1734,14 @@ pub fn p4_read(_ctx: *mut u8, addr: u32) -> T { 0xFF => { let handler = area7_router(addr); if handler.size as usize != std::mem::size_of::() { - panic!( + println!( "p4_read:: {:x} size mismatch, handler size = {}", std::mem::size_of::(), addr, handler.size ); } - let raw_value = (handler.read)(handler.ctx, addr); + let raw_value = (handler.read)(handler.ctx, addr & !(handler.size as u32 -1)); T::from_u32(raw_value) } diff --git a/index.html b/index.html index 04b1197..beac9fe 100644 --- a/index.html +++ b/index.html @@ -93,12 +93,15 @@

WASM embed

-

Drop Dreamcast BIOS Files Here

+

Drop Files to Start Emulator

Click to browse or drag and drop files

-

Required: dc_boot.bin (2MB) and dc_flash.bin (128KB)

+

Choose ONE option:

+

Option 1: BIOS Files - dc_boot.bin (2MB) + dc_flash.bin (128KB)

+

Option 2: ELF File - Any .elf executable file

BIOS ROM (2MB): Pending
BIOS Flash (128KB): Pending
- +
ELF File: Pending
+
@@ -107,6 +110,7 @@

Drop Dreamcast BIOS Files Here

// Store references to be used when WASM loads let biosRom = null; let biosFlash = null; + let elfData = null; let wasmReady = false; let wasmModule = null; @@ -115,6 +119,7 @@

Drop Dreamcast BIOS Files Here

const canvas = document.getElementById('egui_canvas'); const biosRomStatus = document.getElementById('bios-rom-status'); const biosFlashStatus = document.getElementById('bios-flash-status'); + const elfStatus = document.getElementById('elf-status'); // Listen for WASM module to be ready (Trunk will inject it) window.addEventListener('load', async () => { @@ -136,17 +141,52 @@

Drop Dreamcast BIOS Files Here

const data = new Uint8Array(e.target.result); const size = data.length; - if (size === 2 * 1024 * 1024) { + if (file.name.endsWith('.elf')) { + // ELF file + elfData = data; + const sizeKB = (size / 1024).toFixed(2); + updateStatus(elfStatus, 'loaded', `ELF File: ${file.name} (${sizeKB}KB) ✓`); + + // Clear BIOS status and start with ELF + updateStatus(biosRomStatus, 'pending', 'BIOS ROM: Not needed (using ELF)'); + updateStatus(biosFlashStatus, 'pending', 'BIOS Flash: Not needed (using ELF)'); + biosRom = null; + biosFlash = null; + + startEmulatorWithElf(); + } else if (size === 2 * 1024 * 1024) { // 2MB - BIOS ROM biosRom = data; updateStatus(biosRomStatus, 'loaded', `BIOS ROM: ${file.name} (2MB) ✓`); + + // Clear ELF if switching to BIOS mode + if (elfData) { + elfData = null; + updateStatus(elfStatus, 'pending', 'ELF File: Pending'); + } + + // If both BIOS files are loaded, start the emulator + if (biosRom && biosFlash) { + startEmulatorWithBios(); + } } else if (size === 128 * 1024) { // 128KB - BIOS Flash biosFlash = data; updateStatus(biosFlashStatus, 'loaded', `BIOS Flash: ${file.name} (128KB) ✓`); + + // Clear ELF if switching to BIOS mode + if (elfData) { + elfData = null; + updateStatus(elfStatus, 'pending', 'ELF File: Pending'); + } + + // If both BIOS files are loaded, start the emulator + if (biosRom && biosFlash) { + startEmulatorWithBios(); + } } else { const sizeKB = (size / 1024).toFixed(2); - alert(`Invalid file size: ${file.name} (${sizeKB}KB)\nExpected: 2MB or 128KB`); + alert(`Invalid file size: ${file.name} (${sizeKB}KB)\nExpected: 2MB (BIOS ROM), 128KB (BIOS Flash), or .elf file`); updateStatus( size > 512 * 1024 ? biosRomStatus : biosFlashStatus, 'error', @@ -154,11 +194,6 @@

Drop Dreamcast BIOS Files Here

); return; } - - // If both files are loaded, start the emulator - if (biosRom && biosFlash) { - startEmulator(); - } }; reader.onerror = function() { @@ -170,10 +205,10 @@

Drop Dreamcast BIOS Files Here

function handleFiles(files) { for (const file of files) { - if (file.name.endsWith('.bin')) { + if (file.name.endsWith('.bin') || file.name.endsWith('.elf')) { validateAndProcessFile(file); } else { - alert(`Invalid file type: ${file.name}\nOnly .bin files are accepted`); + alert(`Invalid file type: ${file.name}\nOnly .bin and .elf files are accepted`); } } } @@ -201,7 +236,7 @@

Drop Dreamcast BIOS Files Here

handleFiles(e.dataTransfer.files); }); - async function startEmulator() { + async function startEmulatorWithBios() { console.log('Starting emulator with BIOS files...'); if (!wasmReady) { @@ -212,6 +247,7 @@

Drop Dreamcast BIOS Files Here

// Store BIOS data globally so Trunk-injected script can access it window.__nulldc_bios_rom = biosRom; window.__nulldc_bios_flash = biosFlash; + window.__nulldc_boot_mode = 'bios'; // Hide drop zone, show canvas dropZone.classList.add('hidden'); @@ -222,8 +258,30 @@

Drop Dreamcast BIOS Files Here

window.__nulldc_start_requested = true; } - // Make startEmulator available globally for debugging - window.startEmulatorDebug = startEmulator; + async function startEmulatorWithElf() { + console.log('Starting emulator with ELF file...'); + + if (!wasmReady) { + alert('WASM module not ready yet. Please wait a moment and try again.'); + return; + } + + // Store ELF data globally so Trunk-injected script can access it + window.__nulldc_elf_data = elfData; + window.__nulldc_boot_mode = 'elf'; + + // Hide drop zone, show canvas + dropZone.classList.add('hidden'); + canvas.classList.remove('hidden'); + + // Signal that ELF file is ready + console.log('ELF file ready, waiting for WASM module...'); + window.__nulldc_start_requested = true; + } + + // Make start functions available globally for debugging + window.startEmulatorWithBiosDebug = startEmulatorWithBios; + window.startEmulatorWithElfDebug = startEmulatorWithElf; @@ -231,9 +289,9 @@

Drop Dreamcast BIOS Files Here

Updated 2025.09.29 - ZeZu is no longer a member of the nullDC team

diff --git a/src/lib.rs b/src/lib.rs index 578dbc3..3e4c234 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -737,6 +737,51 @@ pub async fn wasm_main_with_bios(bios_rom: Vec, bios_flash: Vec) { run(Some(dc)).await; } +// WASM entry point to boot with an ELF file (no BIOS required) +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +pub async fn wasm_main_with_elf(elf_data: Vec) { + console_error_panic_hook::set_once(); + wasm_logger::init(wasm_logger::Config::default()); + + log::info!("Received ELF: {} bytes", elf_data.len()); + + // Create and initialize Dreamcast + let dc = Box::into_raw(Box::new(Dreamcast::default())); + + // Load ELF + log::info!("Loading ELF file: {} bytes", elf_data.len()); + match dreamcast::init_dreamcast_with_elf(dc, &elf_data) { + Ok(()) => { + log::info!("ELF file loaded successfully"); + } + Err(e) => { + log::error!("Failed to load ELF: {}", e); + return; + } + } + + let debug_server = BroadcastDebugServer::new(dc); + + // Start broadcast debug server for WASM + match debug_server { + Ok(mut server) => { + if let Err(e) = server.start() { + log::error!("Failed to start broadcast debug server: {:?}", e); + } else { + log::info!("Broadcast debug server started successfully"); + // Keep the server alive by forgetting it (leak it intentionally) + std::mem::forget(server); + } + } + Err(e) => { + log::error!("Failed to create broadcast debug server: {:?}", e); + } + } + + run(Some(dc)).await; +} + // Main run function that works for both native and WASM pub async fn run(dreamcast: Option<*mut Dreamcast>) { // Setup logging and panic hooks for wasm diff --git a/src/main.rs b/src/main.rs index 2a32e07..ba3fc50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use nulldc::dreamcast::{Dreamcast, init_dreamcast}; +use nulldc::dreamcast::{Dreamcast, init_dreamcast, init_dreamcast_with_elf}; #[cfg(not(target_arch = "wasm32"))] use nulldc::start_debugger_server; @@ -24,11 +24,29 @@ fn load_bios_files() -> (Vec, Vec) { } fn main() { - let (bios_rom, bios_flash) = load_bios_files(); + let args: Vec = std::env::args().collect(); let dreamcast = Box::into_raw(Box::new(Dreamcast::default())); - init_dreamcast(dreamcast, &bios_rom, &bios_flash); + // Load ELF if provided as command line argument + if args.len() > 1 { + let elf_path = &args[1]; + println!("Loading ELF file: {}", elf_path); + + let elf_data = fs::read(elf_path) + .unwrap_or_else(|e| panic!("Failed to load ELF file from {}: {}", elf_path, e)); + + init_dreamcast_with_elf(dreamcast, &elf_data) + .unwrap_or_else(|e| panic!("Failed to load ELF: {}", e)); + + println!("ELF file loaded successfully"); + } else { + let (bios_rom, bios_flash) = load_bios_files(); + init_dreamcast(dreamcast, &bios_rom, &bios_flash); + + println!("BIOS loaded successfully"); + } + #[cfg(not(target_arch = "wasm32"))] start_debugger_server(dreamcast);