Skip to content
Snippets Groups Projects
main.rs 7.28 KiB
Newer Older
  • Learn to ignore specific revisions
  • use chip_ahoyto::chip8::{Chip8, SCREEN_PIXEL_HEIGHT, SCREEN_PIXEL_WIDTH};
    
    use sdl2::{
        event::Event, image::LoadSurface, keyboard::Keycode, pixels::PixelFormatEnum, surface::Surface,
    };
    
    use std::{fs::File, io::Read};
    
    const COLORS: [[u8; 3]; 2] = [[255, 255, 255], [80, 203, 147]];
    
    
    const LOGIC_HZ: u32 = 240;
    
    const VISUAL_HZ: u32 = 60;
    
    const IDLE_HZ: u32 = 60;
    
    const LOGIC_DELTA: u32 = 60;
    
    
    const SCREEN_SCALE: f32 = 15.0;
    
    
    // The base title to be used in the window.
    const TITLE: &str = "CHIP-Ahoyto";
    
    // The title that is going to be presented initially to the user.
    const TITLE_INITIAL: &str = "CHIP-Ahoyto [Drag and drop the ROM file to play]";
    
    pub struct State {
        system: Chip8,
        rom_loaded: bool,
    
        logic_frequency: u32,
        visual_frequency: u32,
        idle_frequency: u32,
        next_tick_time: u32,
    
        pixel_color: [u8; 3],
    
        // builds the CHIP-8 machine, this is the instance that
        // is going to logically represent the CHIP-8
        let mut state = State {
            system: Chip8::new(),
            rom_loaded: false,
    
            logic_frequency: LOGIC_HZ,
            visual_frequency: VISUAL_HZ,
            idle_frequency: IDLE_HZ,
            next_tick_time: 0,
            pixel_color: COLORS[0],
    
        };
    
        // initializes the SDL sub-system
    
        let sdl = sdl2::init().unwrap();
        let video_subsystem = sdl.video().unwrap();
        let mut timer_subsystem = sdl.timer().unwrap();
    
    
    João Magalhães's avatar
    João Magalhães committed
        // creates the system window that is going to be used to
        // show the emulator and sets it to the central are o screen
    
        let mut window = video_subsystem
    
                TITLE_INITIAL,
    
                SCREEN_SCALE as u32 * SCREEN_PIXEL_WIDTH as u32,
                SCREEN_SCALE as u32 * SCREEN_PIXEL_HEIGHT as u32,
            )
            .resizable()
            .position_centered()
            .build()
            .unwrap();
    
    
        // updates the icon of the window to reflect the image
        // and style of the emulator
    
        let surface = Surface::from_file("./resources/icon.png").unwrap();
        window.set_icon(&surface);
    
        let mut canvas = window.into_canvas().build().unwrap();
        canvas.set_scale(SCREEN_SCALE, SCREEN_SCALE).unwrap();
        canvas.clear();
        canvas.present();
    
        let texture_creator = canvas.texture_creator();
        let mut texture = texture_creator
            .create_texture_streaming(
                PixelFormatEnum::RGB24,
                SCREEN_PIXEL_WIDTH as u32,
                SCREEN_PIXEL_HEIGHT as u32,
            )
            .unwrap();
    
    
        // creates a texture for the surface and presents it to
        // to the screen creating a call to action to drag and
        // drop the image into the screen
    
        let background = texture_creator
            .create_texture_from_surface(&surface)
            .unwrap();
    
        canvas.copy(&background, None, None).unwrap();
        canvas.present();
    
    
        let mut event_pump = sdl.event_pump().unwrap();
    
        'main: loop {
            while let Some(event) = event_pump.poll_event() {
                match event {
                    Event::Quit { .. } => break 'main,
    
                    Event::KeyDown {
                        keycode: Some(Keycode::Escape),
                        ..
                    } => break 'main,
    
    
                    Event::KeyDown {
                        keycode: Some(Keycode::Plus),
                        ..
                    } => {
                        state.logic_frequency += LOGIC_DELTA;
                        None
                    }
    
                    Event::KeyDown {
                        keycode: Some(Keycode::Minus),
                        ..
                    } => {
                        state.logic_frequency -= LOGIC_DELTA;
                        None
                    }
    
    
                    Event::DropFile { filename, .. } => {
                        let rom = read_file(&filename);
    
    
                        state.system.reset();
                        state.system.load_rom(&rom);
    
                        state.rom_loaded = true;
    
                        canvas
                            .window_mut()
                            .set_title(&format!("{} [Currently playing: {}]", TITLE, filename))
                            .unwrap();
                        None
                    }
    
                    Event::KeyDown {
                        keycode: Some(keycode),
                        ..
    
                    } if state.rom_loaded => key_to_btn(keycode).map(|btn| state.system.key_press(btn)),
    
    
                    Event::KeyUp {
                        keycode: Some(keycode),
                        ..
    
                    } if state.rom_loaded => key_to_btn(keycode).map(|btn| state.system.key_lift(btn)),
    
            // in case the ROM is not loaded we must delay next execution
    
            // a little bit to avoid extreme CPU usage
    
            if !state.rom_loaded {
    
                timer_subsystem.delay(1000 / state.idle_frequency);
    
            let current_time = timer_subsystem.ticks();
    
            if current_time >= state.next_tick_time {
    
                // calculates the ratio between the logic and the visual frequency
                // to make sure that the proper number of updates are performed
                let logic_visual_ratio = state.logic_frequency / state.visual_frequency;
                for _ in 0..logic_visual_ratio {
                    // runs the tick operation in the CHIP-8 system,
                    // effectively changing the logic state of the machine
                    state.system.clock();
                    state.system.clock_dt();
                    state.system.clock_st();
                }
    
                // re-creates a vector of pixels from the system pixels
                // buffer, this is considered a pretty expensive operation
    
                let mut rgb_pixels = vec![];
    
                for p in state.system.pixels() {
    
                    rgb_pixels.extend_from_slice(&[
    
                        p * state.pixel_color[0],
                        p * state.pixel_color[1],
                        p * state.pixel_color[2],
    
                // creates a texture based on the RGB pixel buffer
                // and copies that to the canvas for presentation
    
                texture
                    .update(None, &rgb_pixels, SCREEN_PIXEL_WIDTH as usize * 3)
                    .unwrap();
                canvas.copy(&texture, None, None).unwrap();
                canvas.present();
    
    
                // updates the next update time reference to the current
                // time so that it can be used from game loop control
    
                state.next_tick_time = current_time + (1000 / state.visual_frequency);
    
            let current_time = timer_subsystem.ticks();
    
            let pending_time = state.next_tick_time.saturating_sub(current_time);
            timer_subsystem.delay(pending_time);
    
        }
    }
    
    fn key_to_btn(keycode: Keycode) -> Option<u8> {
        match keycode {
            Keycode::Num1 => Some(0x01),
            Keycode::Num2 => Some(0x02),
            Keycode::Num3 => Some(0x03),
            Keycode::Num4 => Some(0x0C),
            Keycode::Q => Some(0x04),
            Keycode::W => Some(0x05),
            Keycode::E => Some(0x06),
            Keycode::R => Some(0x0D),
            Keycode::A => Some(0x07),
            Keycode::S => Some(0x08),
            Keycode::D => Some(0x09),
            Keycode::F => Some(0x0E),
            Keycode::Z => Some(0x0A),
            Keycode::X => Some(0x00),
            Keycode::C => Some(0x0B),
            Keycode::V => Some(0x0F),
            _ => None,
        }
    }
    
    fn read_file(path: &str) -> Vec<u8> {
        let mut file = File::open(path).unwrap();
        let mut rom = Vec::new();
        file.read_to_end(&mut rom).unwrap();
        rom
    }