diff --git a/.gitignore b/.gitignore index 69c93e4..f8a5197 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Cargo.lock elf2le a.exe new.elf +Makefile diff --git a/Cargo.toml b/Cargo.toml index f260c4b..9a4c13a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,12 +9,8 @@ edition = "2021" [dependencies] heapless = { version = "0.7.16", features = ["ufmt-impl"] } spin = "0.9.4" - - -[profile.dev] -panic = "abort" -lto = "fat" -codegen-units = 1 +bitvec = { version = "1.0.1", default-features = false, features = ["atomic", "alloc"] } +itertools = { version = "0.11", default-features = false, features = ["use_alloc"] } [profile.release] panic = "abort" diff --git a/dos.json b/dos.json index ded751b..989e5dd 100644 --- a/dos.json +++ b/dos.json @@ -7,7 +7,7 @@ "exe-suffix": ".elf", "linker-flavor": "gcc", "linker-is-gnu": true, - "llvm-target": "i586-unknown-none-none", + "llvm-target": "i386-unknown-none-none", "max-atomic-width": 64, "position-independent-executables": false, "relocation-model": "static", diff --git a/src/bmp.rs b/src/bmp.rs new file mode 100644 index 0000000..e010cad --- /dev/null +++ b/src/bmp.rs @@ -0,0 +1,131 @@ +use core::mem::size_of; + +use itertools::Itertools; + +use alloc::boxed::Box; +use bitvec::{view::BitView, field::BitField}; + +use crate::println; +use crate::vga::Vga18; + +#[repr(packed)] +struct PackedBmpHeader { + bmp_type: u16, + size: u32, + reserved: u32, + offset: u32, + header_size: u32, + width: u32, + height: u32, + planes: u16, + bpp: u16, + compression: u32, + size_image: u32, + xppm: u32, + yppm: u32, + colors_used: u32, + colors_important: u32, +} +pub struct BmpHeader { + pub bmp_type: u16, + pub size: u32, + pub reserved: u32, + pub offset: u32, + pub header_size: u32, + pub width: u32, + pub height: u32, + pub planes: u16, + pub bpp: u16, + pub compression: u32, + pub size_image: u32, + pub xppm: u32, + pub yppm: u32, + pub colors_used: u32, + pub colors_important: u32, +} +impl From:: for BmpHeader { + fn from(value: PackedBmpHeader) -> Self { + Self { bmp_type: value.bmp_type, size: value.size, reserved: value.reserved, offset: value.offset, header_size: value.header_size, width: value.width, height: value.height, planes: value.planes, bpp: value.bpp, compression: value.compression, size_image: value.size_image, xppm: value.xppm, yppm: value.yppm, colors_used: value.colors_used, colors_important: value.colors_important } + } +} + +pub struct Bmp { + pub header: BmpHeader, + pub palette_table: alloc::vec::Vec, + pub data: Box<[u8]> +} + +fn load_data(header: &PackedBmpHeader, raw: &[u8]) -> Option> { + match header.bpp { + 8 => { + let row_size = (((8 * header.width + 31) >> 5) << 2) as usize; + let chunks = raw[..row_size * header.height as usize] + .chunks_exact(row_size).rev() + .flat_map(|f| f[..header.width as usize].iter().cloned()); + Some(chunks.collect()) + }, + bpp @ (4|2|1) => { + let row_size = ((bpp as usize * header.width as usize + 31) >> 5) << 2; + let chunks = raw[..row_size * header.height as usize] + .chunks_exact(row_size).rev() + .map(|row| row.view_bits::() + .chunks(bpp as usize).take(header.width as usize) + .map(|b| b.load_le::())); + Some(chunks.flatten().collect()) + } + bpp => { + println!("Unsupported BPP {}", bpp); + return None + } + } +} + +pub fn load_bmp(source: &[u8]) -> Option { + // i'm sorry but i love loading file headers like this + let header: PackedBmpHeader = unsafe { + let mut copy: [u8; size_of::()] = [0; size_of::()]; + copy.copy_from_slice(&source[..size_of::()]); + core::mem::transmute(copy) + }; + + // 'BM' + if header.bmp_type != 0x4D42 { + println!("Invalid BMP type"); + return None + } + if header.size as usize != source.len() { + println!("BMP size does not match file length!"); + return None + } + if header.offset as usize > source.len() { + println!("BMP bitmap offset greater than file length!"); + return None + } + if header.compression != 0 { + println!("Compressed BMPs are not supported"); + return None + } + if { + let row_size = ((header.bpp as usize * header.width as usize + 31) >> 5) << 2; + let (calc_size, overflow) = row_size.overflowing_mul(header.height as usize); + (calc_size > (source.len() - header.offset as usize)) | overflow + } { + println!("File cannot possibly contain full BMP data!"); + return None + } + + let color_table = &source[(header.offset - header.colors_used * 4) as usize..]; + let palette: alloc::vec::Vec<_> = color_table.iter() + .tuples().take(header.colors_used as usize) + .map(|(b,g,r,_)| Vga18 { red:r>>2,green:g>>2,blue:b>>2 }) + .collect(); + + let raw_data = &source[header.offset as usize..]; + let data = load_data(&header, raw_data); + + Some(Bmp { + header: BmpHeader::from(header), + palette_table: palette, + data: data? + }) +} diff --git a/src/chicken.bmp b/src/chicken.bmp new file mode 100644 index 0000000..226c82e Binary files /dev/null and b/src/chicken.bmp differ diff --git a/src/dpmi.rs b/src/dpmi.rs index 58eda8c..50bc5bf 100644 --- a/src/dpmi.rs +++ b/src/dpmi.rs @@ -1,9 +1,11 @@ -use core::{arch::asm, fmt::{Arguments, Write}}; +#![allow(dead_code)] +use core::{arch::asm, fmt::{Arguments, Write}, ffi::CStr, str::FromStr}; extern crate alloc; #[allow(dead_code)] #[repr(packed)] +#[derive(Default)] pub struct DpmiRegs { pub edi: u32, pub esi: u32, @@ -29,19 +31,21 @@ impl DpmiRegs { } } -#[inline] +// BIOS function INT 10,0 Set video mode pub fn set_video_mode(mode: u8) { let mut regs = DpmiRegs::zero(); regs.eax = mode as u32; real_int(0x10, &mut regs); } +// BIOS function INT 16,0 Wait for keystroke and read pub fn getchar() -> u16 { let mut regs = DpmiRegs::zero(); real_int(0x16, &mut regs); regs.eax as u16 } +// BIOS function INT 16,1 Get keyboard status pub fn kb_status() -> Option { let mut regs = DpmiRegs::zero(); regs.eax = 0x100; @@ -55,17 +59,167 @@ pub fn kb_status() -> Option { } } -pub fn get_psp() -> *const u8 { - let psp: u32; +pub fn get_psp(buff: &mut [u8; 256]) { + // DOS DPMI function 21h,AH 62h - Get PSP Selector + // Out: EBX = PSP selector + // this really was hurting me so i just did it in assembly unsafe { asm!( "int 0x21", - in("ax") 0x5100_u16, - out("ebx") psp + "push esi", + "push edi", + "mov ds, bx", + "xor esi, esi", + "mov ecx, 256", + "rep movsb", + "pop edi", + "pop esi", + "push es", + "pop ds", + in("ax") 0x6200_u16, + out("ebx") _, + in("edi") buff.as_mut_ptr(), ); } - (psp << 4) as *const u8 +} + +pub fn get_args() -> alloc::vec::Vec { + let mut buff = [0_u8; 256]; + get_psp(&mut buff); + let cmd_byte_cnt = buff[0x80]; + let cmd_line = &buff[0x81..0x100.min(0x81+cmd_byte_cnt as usize)]; + // this is a horrible way of doing this, but i'm lazy + core::str::from_utf8(cmd_line).unwrap() + .split_whitespace() + .map(|s| alloc::string::String::from_str(s).unwrap()) + .collect() +} + +pub struct File { + handle: u32, + size: u32 +} + +impl File { + pub fn open(string: &CStr) -> Option { + // DOS DPMI function 21h, AH 3Dh - Open File + // In: + // AL = access mode + // DS:EDX = pointer to ASCIIZ file name + // Out: + // if successful: + // CF clear + // EAX = file handle + // + // if failed: + // CF set + // EAX = DOS error code + let err: u8; + let eax: u32; + unsafe { asm!( + "int 0x21", + "setc bl", + inout("eax") 0x00003D00_u32 => eax, + in("edx") string.as_ptr(), + inout("bl") 0_u8 => err + );} + + // Error TODO return error code instead of printing + if err == 1 { + crate::println!("Could not open file \"{}\" ({}).", string.to_str().unwrap(), eax); + return None; + } + + let mut file = Self { + handle: eax, + size: 0 + }; + let size = file.find_size()?; + file.size = size; + + return Some(file); + } + + pub fn get_size(&self) -> u32 { self.size } + + fn seek(&mut self, origin: u8) -> Option { + // DOS DPMI function 21h, AH 42h - Set Current File Position + // In: + // AH = 42h + // AL = origin of move: + // + // 00h = start + // 01h = current + // 02h = end + // + // ECX:EDX = file position + // Out: + // if successful: + // CF clear + // EDX:EAX = current file position + // + // if failed: + // CF set + // EAX = DOS error code + let err: u32; + let eax: u32; + let edx: u32; + unsafe { asm!( + "int 0x21", + "setc bl", + inout("eax") 0x00004200_u32 | origin as u32 => eax, + in("ecx") 0, + inout("edx") 0 => edx, + inout("ebx") self.handle => err + );} + + // Error TODO return error code + if err == 1 { return None } + + return Some(eax | (edx << 16)); + } + + fn find_size(&mut self) -> Option { + let size = self.seek(2); + self.seek(0)?; + return size; + } + + pub fn read(&mut self, buffer: &mut [u8]) -> Option { + // DOS DPMI function 21h, AH 3Fh - Read File + // In: + // AH = 3Fh + // EBX = file handle + // ECX = number of bytes to read (size) + // DS:EDX = pointer to buffer to read to (addr) + // Out: + // if successful: + // CF clear + // EAX = number of bytes read + // + // if failed: + // CF set + // EAX = DOS error code + let err: u32; + let eax: u32; + unsafe { asm!( + "int 0x21", + "mov ebx, 0", + "setc bl", + inout("eax") 0x00003F00_u32 => eax, + inout("ebx") self.handle => err, + in("ecx") buffer.len(), + in("edx") buffer.as_mut_ptr(), + );} + + // Error TODO return error code + if err == 1 { return None } + + return Some(eax); + } } pub fn real_int(int: u8, regs: &mut DpmiRegs) { + // DPMI function 0300h - Simulate Real Mode Interrupt + // TODO get error codes from AX/CF unsafe { asm!( "int 0x31", in("bx") 0x0000_u16 | int as u16, @@ -75,6 +229,52 @@ pub fn real_int(int: u8, regs: &mut DpmiRegs) { );} } +pub struct IntHandler { + interrupt: u8, + selector: u16, + offset: u32 +} +impl IntHandler { + pub fn new(interrupt: u8) -> Self { + // get old handler + // DPMI function 0204h - Get Protected Mode Interrupt Vector + let selector: u16; + let offset: u32; + unsafe { asm!( + "int 0x31", + in("ax") 0x0204, + in("bl") interrupt, + out("edx") offset, + out("cx") selector + ); } + Self { interrupt, offset, selector } + } + pub fn set_handler(&mut self, f: extern "x86-interrupt" fn()) { + // install new handler + // DPMI function 0205h - Set Protected Mode Interrupt Vector + let func_ptr = f as *const () as u32; + unsafe { asm!( + "mov cx, cs", + "int 0x31", + inout("ax") 0x0205 => _, + in("bl") self.interrupt, + in("edx") func_ptr, + out("cx") _ + ); } + } + pub fn restore_handler(&mut self) { + // restore original handler + // DPMI function 0205h - Set Protected Mode Interrupt Vector + unsafe { asm!( + "int 0x31", + inout("ax") 0x0205 => _, + in("bl") self.interrupt, + in("cx") self.selector, + in("edx") self.offset, + ); } + } +} + pub fn dpmi_print(string: &str) { // DOS DPMI function 21h, AH 9h // Prints string pointed to @@ -106,14 +306,13 @@ macro_rules! print { } #[macro_export] macro_rules! println { - () => (_raw_print!("\r\n$")); + () => ($crate::_raw_print!("\r\n$")); ($($arg:tt)*) => ($crate::_raw_print!("{}\r\n$", format_args!($($arg)*))); } #[doc(hidden)] pub fn _print(args: Arguments) { let mut s: alloc::string::String = alloc::string::String::new(); - //let mut s: heapless::String<20> = heapless::String::new(); s.write_fmt(args).unwrap(); dpmi_print(s.as_str()); } diff --git a/src/dpmi_alloc.rs b/src/dpmi_alloc.rs index 313af81..3101f33 100644 --- a/src/dpmi_alloc.rs +++ b/src/dpmi_alloc.rs @@ -2,8 +2,10 @@ use core::{ptr::null_mut, arch::asm, alloc::Layout}; use core::fmt::Write; use heapless::String; -pub struct DpmiAlloc {} -impl DpmiAlloc { const fn new() -> Self { DpmiAlloc { } } } +pub struct DpmiAlloc { } +impl DpmiAlloc { + const fn new() -> Self { DpmiAlloc {} } +} unsafe impl core::alloc::GlobalAlloc for DpmiAlloc { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { @@ -24,6 +26,7 @@ unsafe impl core::alloc::GlobalAlloc for DpmiAlloc { "int 0x21", "mov eax, esi", "mov esi, edx", + // We can calculate the handle ourself inout("eax") 0x0FF91 => _handle, inout("ebx") layout.size() as u32 => ptr, out("edx") _ @@ -47,7 +50,7 @@ unsafe impl core::alloc::GlobalAlloc for DpmiAlloc { "int 0x21", "mov esi, edx", in("eax") 0x0FF92, - in("ebx") ptr as u32 - 0x10, // handle should be just 0x10 less than the pointer + in("ebx") ptr as u32 - 0x10, // in dos32a the handle should be just 0x10 less than the pointer out("edx") _ ); } diff --git a/src/main.rs b/src/main.rs index 5f7f648..037a058 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,25 +10,154 @@ extern crate alloc; mod dpmi; mod dpmi_alloc; mod panic; +mod vga; +mod bmp; -fn main() { - println!("Hello, world!"); - debug_trap!(); - println!("Done."); -} +use alloc::{vec, ffi::CString}; +use bmp::Bmp; +use vga::Mode13hDisplay; -#[allow(unused_macros)] -#[macro_export] -macro_rules! debug_trap { - () => { unsafe { asm!("int 0x1"); } }; -} +const TEST_BMP: &[u8; 5318] = include_bytes!("chicken.bmp"); #[no_mangle] pub extern "C" fn start() { unsafe { asm!( + "mov ax, es", "push ds", "pop es", ); } main(); dpmi::dpmi_exit(); } + +fn main() { + let args = dpmi::get_args(); + let filename = { + if args.is_empty() || args[0].is_empty() { + println!("Filename required."); + println!("Press Q to exit, or any key to continue with the default image."); + match dpmi::getchar() as u8 { + b'q' | b'Q' => return, + _ => None + } + } else { + Some(&*args[0]) + } + }; + + // Try to load BMP file from filename, or else use the included test image + let bmp = { + let mut bmp_buff; + let src = if let Some(filename) = filename { + println!("Loading BMP from {}...", filename); + let mut file = match dpmi::File::open(&CString::new(filename).unwrap()) { + Some(f) => f, + None => { + println!("Could not open file."); + return; + } + }; + println!("File size: {} bytes", file.get_size()); + + bmp_buff = vec![0; file.get_size() as usize]; + file.read(&mut bmp_buff); + + &bmp_buff[..] + } else { + TEST_BMP + }; + if let Some(bmp) = bmp::load_bmp(src) { + bmp + } else { + println!("Could not open BMP. Exiting"); + return; + } + }; + + println!("Width x Height x BPP: {}x{}x{}", bmp.header.width, bmp.header.height, bmp.header.bpp); + println!("Colors Used, Important: {},{}", bmp.header.colors_used, bmp.header.colors_important); + println!("Arrow keys to move, 1-9 to change speed, Q to exit."); + println!("Press any key to continue."); + dpmi::getchar(); + + // mode 13h, 320x200 256 color graphics + dpmi::set_video_mode(0x13); + // Get screen buffer + let mut vga = Mode13hDisplay::default(); + + // sets the VGA screen palette to the BMP color palette + vga::set_vga_dac_colors(0, &bmp.palette_table); + + // set up new keyboard handler + // could do getchar, but this is more fun + let mut kb_handler = dpmi::IntHandler::new(9); + kb_handler.set_handler(keyboard_int_handler); + + let mut last_scancode = 0xFF; + let mut delta = 1; + let mut pos = Position { x: 0, y: 0 }; + loop { + let scancode = { *SCANCODE.read() }; + if scancode != last_scancode { + match scancode { + 0x48 => { pos.y += delta; }, // up + 0x4B => { pos.x -= delta; }, // left + 0x4D => { pos.x += delta; }, // right + 0x50 => { pos.y -= delta; }, // down + s @ 0x02..=0x0A => { delta = s as isize - 1; }, // 1-9 + 0x10 => break, // q + _ => {} + } + last_scancode = scancode; + draw_loop(&mut vga, &bmp, &pos); + } + // halt processor so we don't burn the CPU + unsafe { asm!("hlt"); } + } + + // restore old keyboard handler + kb_handler.restore_handler(); + + // mode 3h, text mode graphics, DOS default + dpmi::set_video_mode(0x3); +} + +fn draw_loop(vga: &mut Mode13hDisplay, bmp: &Bmp, pos: &Position) { + vga.clear(); + vga.copy_to_screen(pos.x, pos.y, bmp.header.width as usize, bmp.header.height as usize, &bmp.data); + // TODO wait for blanking interval + vga.flush(); +} + +struct Position { + x: isize, + y: isize, +} + +static SCANCODE: spin::RwLock = spin::RwLock::new(0); +pub extern "x86-interrupt" fn keyboard_int_handler() { + let old_ds: u16; + unsafe { asm!( + "mov bx, ds", + "mov ax, es", + "mov ds, ax", + out("bx") old_ds + ); } + + let code: u8; + unsafe { asm!( + "in al, 0x60", + out("al") code + ); } + + let mut scan = SCANCODE.write(); + *scan = code; + drop(scan); + + unsafe { asm!( + "mov ds, bx", + "out 0x20, al", + in("al") 0x20_u8, + in("bx") old_ds + ); } +} diff --git a/src/panic.rs b/src/panic.rs index f2d5a66..f1fca6d 100644 --- a/src/panic.rs +++ b/src/panic.rs @@ -1,8 +1,8 @@ use crate::dpmi::dpmi_exit; #[panic_handler] -fn panic(_info: &core::panic::PanicInfo) -> ! { - //crate::println!("Panic! {}", info); +fn panic(info: &core::panic::PanicInfo) -> ! { + crate::println!("Panic! {}", info); dpmi_exit(); } diff --git a/src/vga.rs b/src/vga.rs new file mode 100644 index 0000000..71d9350 --- /dev/null +++ b/src/vga.rs @@ -0,0 +1,87 @@ +use core::arch::asm; + +#[derive(Copy,Clone,Default,PartialEq)] +pub struct Vga18 { + pub red: u8, + pub green: u8, + pub blue: u8 +} + +#[inline] +fn mode13h_vga_arr() -> &'static mut [[u8; 320]; 200] { + unsafe { &mut *(0xa0000 as *mut [[u8; 320]; 200]) } +} + +unsafe fn outb(port: u16, data: u8) { + asm! { + "out dx, al", + in("dx") port, + in("al") data, + } +} + +pub fn set_vga_dac_colors(start_index: u8, colors: &[Vga18]) { + if colors.is_empty() { return } + unsafe { outb(0x3c8, start_index); } + for (i, &Vga18 { red, green, blue }) in colors.iter().enumerate() { + if i + start_index as usize >= 256 { + break + } + unsafe { + outb(0x3c9, red); + outb(0x3c9, green); + outb(0x3c9, blue); + } + } +} + +pub struct Mode13hDisplay { + buffer: [[u8; 320]; 200] +} + +impl Default for Mode13hDisplay { + fn default() -> Self { + Self { buffer: [[0; 320]; 200] } + } +} + +impl Mode13hDisplay { + #[allow(unused)] + pub fn flush(&self) { + let vga = mode13h_vga_arr(); + *vga = self.buffer; + } + + pub fn clear(&mut self) { + self.buffer = [[0; 320]; 200]; + } + + pub fn copy_to_screen(&mut self, screen_col: isize, screen_line: isize, src_width: usize, src_height: usize, bytes: &[u8]) { + let x = screen_col; + let y = screen_line; + + for l in 0..src_height { + let l_y = l as isize + y; + // past screen + if l_y >= 200 { break; } + // before screen + else if l_y < 0 { continue; } + // past screen + if -x >= src_width as isize { break; } + + let line_len = if x >= 0 { + (320 - x).min(src_width as isize) + } else { + 320.min(src_width as isize + x) + }; + + let src_off = if x >= 0 { + l * src_width + } else { ((l * src_width) as isize - x) as usize }; + + let x_adj = x.max(0); + + self.buffer[l_y as usize][x_adj as usize..x_adj as usize+line_len as usize].copy_from_slice(&bytes[src_off..src_off+line_len as usize]); + } + } +}