summaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/app')
-rw-r--r--src/app/check_events.rs52
-rw-r--r--src/app/draw_video.rs71
-rw-r--r--src/app/init.rs88
-rw-r--r--src/app/load.rs96
-rw-r--r--src/app/main.rs69
-rw-r--r--src/app/run.rs58
-rw-r--r--src/app/sync_video.rs44
7 files changed, 478 insertions, 0 deletions
diff --git a/src/app/check_events.rs b/src/app/check_events.rs
new file mode 100644
index 0000000..1dd9551
--- /dev/null
+++ b/src/app/check_events.rs
@@ -0,0 +1,52 @@
+/*
+ Copyright 2021-2023 Gabriel Jensen.
+
+ This file is part of Luma.
+
+ Luma is free software: you can redistribute it
+ and/or modify it under the terms of the GNU
+ Affero General Public License as published by
+ the Free Software Foundation, either version 3
+ of the License, or (at your option) any later
+ version.
+
+ Luma is distributed in the hope that it will be
+ useful, but WITHOUT ANY WARRANTY; without even
+ the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Affero General Public License for more details.
+
+ You should have received a copy of the GNU
+ Affero General Public License along with Luma.
+ If not, see <https://www.gnu.org/licenses/>.
+*/
+
+use crate::app::App;
+
+use sdl2::event::Event;
+use std::sync::atomic::Ordering;
+
+impl App {
+ pub fn check_events(&mut self) -> Result<bool, String> {
+ // Return true if we should quit.
+
+ let mut event_pump = match self.sdl.event_pump() {
+ Ok(pump) => pump,
+ _ => return Err("unable to get event pump".to_string()),
+ };
+
+ if self.got_terminate.load(Ordering::Relaxed) {
+ eprintln!("got terminate");
+ return Ok(true)
+ };
+
+ for event in event_pump.poll_iter() {
+ match event {
+ Event::Quit {..} => return Ok(true),
+ _ => {},
+ };
+ }
+
+ return Ok(false);
+ }
+}
diff --git a/src/app/draw_video.rs b/src/app/draw_video.rs
new file mode 100644
index 0000000..a0d2016
--- /dev/null
+++ b/src/app/draw_video.rs
@@ -0,0 +1,71 @@
+/*
+ Copyright 2021-2023 Gabriel Jensen.
+
+ This file is part of Luma.
+
+ Luma is free software: you can redistribute it
+ and/or modify it under the terms of the GNU
+ Affero General Public License as published by
+ the Free Software Foundation, either version 3
+ of the License, or (at your option) any later
+ version.
+
+ Luma is distributed in the hope that it will be
+ useful, but WITHOUT ANY WARRANTY; without even
+ the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Affero General Public License for more details.
+
+ You should have received a copy of the GNU
+ Affero General Public License along with Luma.
+ If not, see <https://www.gnu.org/licenses/>.
+*/
+
+use crate::SCREEN_SIZE;
+use crate::app::App;
+
+use sdl2::pixels::Color;
+use sdl2::rect::Rect;
+
+impl App {
+ pub fn draw_video(&mut self, video: &[u8], agb_palette: &[u16]) {
+ // TO-DO: Honour video mode.
+
+ let mut palette: [Color; 0x100] = [Color::RGB(0x00, 0x00, 0x00); 0x100];
+
+ for (index, element) in palette.iter_mut().enumerate() {
+ let value = unsafe { *agb_palette.get_unchecked(index) };
+
+ let colour = decode_colour(value);
+ *element = colour;
+ }
+
+ for pixel_y in 0x0..SCREEN_SIZE.1 {
+ for pixel_x in 0x0..SCREEN_SIZE.0 {
+ let pixel = pixel_y as usize * SCREEN_SIZE.0 as usize + pixel_x as usize;
+
+ let value = video[pixel];
+ let colour = palette[value as usize];
+ self.canvas.set_draw_color(colour);
+
+ let square = Rect::new(
+ (pixel_x as u32 * self.scale) as i32,
+ (pixel_y as u32 * self.scale) as i32,
+ self.scale,
+ self.scale,
+ );
+ self.canvas.fill_rect(square).unwrap();
+ }
+ }
+
+ self.canvas.present();
+ }
+}
+
+fn decode_colour(colour: u16) -> Color {
+ let red = ((colour & 0b0000000000011111) as f32 / 31.0 * 255.0) as u8;
+ let green = ((colour & 0b0000001111100000) as f32 / 992.0 * 255.0) as u8;
+ let blue = ((colour & 0b0111110000000000) as f32 / 31744.0 * 255.0) as u8;
+
+ return Color::RGB(red, green, blue);
+}
diff --git a/src/app/init.rs b/src/app/init.rs
new file mode 100644
index 0000000..c7a8653
--- /dev/null
+++ b/src/app/init.rs
@@ -0,0 +1,88 @@
+/*
+ Copyright 2021-2023 Gabriel Jensen.
+
+This file is part of Luma.
+Luma is free software: you can redistribute it
+and/or modify it under the terms of the GNU
+ Affero General Public License as published by
+ the Free Software Foundation, either version 3
+ of the License, or (at your option) any later
+ version.
+
+ Luma is distributed in the hope that it will be
+ useful, but WITHOUT ANY WARRANTY; without even
+ the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Affero General Public License for more details.
+
+ You should have received a copy of the GNU
+ Affero General Public License along with Luma.
+ If not, see <https://www.gnu.org/licenses/>.
+*/
+
+use crate::{SCREEN_SIZE, VERSION};
+use crate::app::App;
+use crate::configuration::Configuration;
+
+use sdl2::pixels::Color;
+use sdl2::render::BlendMode;
+use std::sync::Arc;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+impl App {
+ pub fn init(configuration: Configuration) -> Result<Self, String> {
+ let got_terminate = Arc::new(AtomicBool::new(false));
+
+ match ctrlc::set_handler({
+ let got_terminate = got_terminate.clone();
+ move || got_terminate.store(true, Ordering::Relaxed)
+ }) {
+ Err(..) => return Err("unable to set signal handler".to_string()),
+ _ => {},
+ };
+
+ let sdl = match sdl2::init() {
+ Ok( sdl) => sdl,
+ Err(..) => return Err("unable to initialise sdl2".to_string()),
+ };
+
+ let sdl_video = match sdl.video() {
+ Ok( video) => video,
+ Err(..) => return Err("unable to initialise video".to_string()),
+ };
+
+ let window_title = format!("Luma {:X}.{:X}", VERSION.0, VERSION.1);
+
+ let mut window_builder = sdl_video.window(&window_title, SCREEN_SIZE.0 as u32 * configuration.scale, SCREEN_SIZE.1 as u32 * configuration.scale);
+ window_builder.position_centered();
+
+ let window = match window_builder.build() {
+ Ok( window) => window,
+ Err(..) => return Err("unable to open window".to_string()),
+ };
+
+ let mut canvas = match window.into_canvas().build() {
+ Ok( canvas) => canvas,
+ Err(..) => return Err("unable to build canvas".to_string()),
+ };
+
+ canvas.set_blend_mode(BlendMode::Blend);
+
+ let clear_colour = Color::RGB(0x00, 0x00, 0x00);
+ canvas.set_draw_color(clear_colour);
+ canvas.clear();
+ canvas.present();
+
+ return Ok(App {
+ bootloader: configuration.bootloader,
+ image: configuration.image,
+
+ scale: configuration.scale,
+
+ got_terminate: got_terminate,
+
+ sdl: sdl,
+ canvas: canvas,
+ });
+ }
+}
diff --git a/src/app/load.rs b/src/app/load.rs
new file mode 100644
index 0000000..24a2385
--- /dev/null
+++ b/src/app/load.rs
@@ -0,0 +1,96 @@
+/*
+ Copyright 2021-2023 Gabriel Jensen.
+
+ This file is part of Luma.
+
+ Luma is free software: you can redistribute it
+ and/or modify it under the terms of the GNU
+ Affero General Public License as published by
+ the Free Software Foundation, either version 3
+ of the License, or (at your option) any later
+ version.
+
+ Luma is distributed in the hope that it will be
+ useful, but WITHOUT ANY WARRANTY; without even
+ the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Affero General Public License for more details.
+
+ You should have received a copy of the GNU
+ Affero General Public License along with Luma.
+ If not, see <https://www.gnu.org/licenses/>.
+*/
+
+use crate::VERSION;
+use crate::app::App;
+use crate::state::State;
+
+use std::fs::File;
+use std::io::Read;
+
+impl App {
+ pub fn load(&mut self, state: &mut State) -> Result<(), String> {
+ eprintln!("loading booatloader \"{}\"", self.bootloader);
+
+ let mut bootloader = match File::open(&self.bootloader) {
+ Ok(file) => file,
+ _ => return Err("unable to open bootloader".to_string()),
+ };
+
+ if let Err(..) = bootloader.read(state.bootloader_buffer()) { return Err("unable to read bootloader".to_string()) };
+
+ eprintln!("loading image \"{}\"", self.image);
+
+ let mut image = match File::open(&self.image) {
+ Ok(file) => file,
+ _ => return Err("unable to open image".to_string()),
+ };
+
+ match image.read(state.image_buffer()) {
+ Err(..) => return Err("unable to read image".to_string()),
+ _ => {},
+ };
+
+ let title = get_title(&state.image_buffer()[0xA0..0xAC]);
+ let id = get_id(&state.image_buffer()[0xAC..0xB0]);
+ let version = state.image_buffer()[0xBC];
+
+ eprintln!("loaded image \"{title}\" ({id}) v.{version}");
+
+ self.canvas.window_mut().set_title(&format!("Luma {:X}.{:X} - {title}", VERSION.0, VERSION.1)).unwrap();
+
+ return Ok(());
+ }
+}
+
+fn get_title(data: &[u8]) -> String {
+ let mut title = String::with_capacity(0xC);
+
+ for raw in data {
+ let character = match char::from_u32(*raw as u32) {
+ Some('\u{0000}') => break,
+ Some(character) => character,
+ None => '?',
+ };
+
+ title.push(character);
+ }
+
+ return title;
+}
+
+fn get_id(data: &[u8]) -> String {
+ let mut id = String::with_capacity(0xC);
+
+ for raw in data {
+ let character = match char::from_u32(*raw as u32) {
+ Some('\u{0000}') => break,
+ Some(character) => character,
+ None => '?',
+ };
+
+ id.push(character);
+ }
+
+ return id;
+}
diff --git a/src/app/main.rs b/src/app/main.rs
new file mode 100644
index 0000000..6391716
--- /dev/null
+++ b/src/app/main.rs
@@ -0,0 +1,69 @@
+/*
+ Copyright 2021-2023 Gabriel Jensen.
+
+ This file is part of Luma.
+
+ Luma is free software: you can redistribute it
+ and/or modify it under the terms of the GNU
+ Affero General Public License as published by
+ the Free Software Foundation, either version 3
+ of the License, or (at your option) any later
+ version.
+
+ Luma is distributed in the hope that it will be
+ useful, but WITHOUT ANY WARRANTY; without even
+ the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Affero General Public License for more details.
+
+ You should have received a copy of the GNU
+ Affero General Public License along with Luma.
+ If not, see <https://www.gnu.org/licenses/>.
+*/
+
+use crate::VERSION;
+use crate::app::App;
+use crate::configuration::Configuration;
+
+use std::env::{args, var};
+use std::process::exit;
+
+impl App {
+ pub fn main() {
+ println!("\u{1B}[1mluma\u{1B}[0m {:X}.{:X}", VERSION.0, VERSION.1);
+ println!("Copyright 2021-2023 \u{1B}[1mGabriel Bj\u{F8}rnager Jensen\u{1B}[0m.");
+ println!();
+
+ let path = if let Some(path) = args().nth(0x1) { path }
+ else { default_configuration_path() };
+
+ let configuration = match Configuration::load(&path) {
+ Ok( configuration) => configuration,
+ Err(message) => panic!("unable to load configuration: {message}"),
+ };
+
+ let app = match App::init(configuration) {
+ Ok( app) => app,
+ Err(message) => panic!("unable to initialise application: {message}"),
+ };
+
+ let result = app.run();
+
+ if let Err(ref message) = result { eprintln!("\u{1B}[1m\u{1B}[91merror\u{1B}[0m: {message}") };
+
+ exit(match result {
+ Ok( ..) => 0x0,
+ Err(..) => 0x1,
+ });
+ }
+}
+
+fn default_configuration_path() -> String {
+ let home = match var("HOME") {
+ Ok( path) => path,
+ Err(..) => "/".to_string(),
+ };
+
+ let path = home + "/.luma.toml";
+ return path;
+}
diff --git a/src/app/run.rs b/src/app/run.rs
new file mode 100644
index 0000000..5bc8dc7
--- /dev/null
+++ b/src/app/run.rs
@@ -0,0 +1,58 @@
+/*
+ Copyright 2021-2023 Gabriel Jensen.
+
+ This file is part of Luma.
+
+ Luma is free software: you can redistribute it
+ and/or modify it under the terms of the GNU
+ Affero General Public License as published by
+ the Free Software Foundation, either version 3
+ of the License, or (at your option) any later
+ version.
+
+ Luma is distributed in the hope that it will be
+ useful, but WITHOUT ANY WARRANTY; without even
+ the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Affero General Public License for more details.
+
+ You should have received a copy of the GNU
+ Affero General Public License along with Luma.
+ If not, see <https://www.gnu.org/licenses/>.
+*/
+
+use crate::{PALETTE_LENGTH, VIDEO_LENGTH};
+use crate::app::App;
+use crate::cpu::Cpu;
+use crate::state::State;
+
+use std::time::Instant;
+
+impl App {
+ pub fn run(mut self) -> Result<(), String> {
+ let mut state = State::new();
+
+ self.load(&mut state)?;
+
+ let cpu = Cpu::new(state);
+
+ let mut cpu = cpu.boot();
+
+ let mut video_buffer: Vec::<u8> = vec![0x0; VIDEO_LENGTH as usize];
+ let mut palette_buffer: Vec::<u16> = vec![0x0; (PALETTE_LENGTH / 0x2) as usize];
+
+ 'main_loop: loop {
+ let frame_start = Instant::now();
+
+ if self.check_events()? { break 'main_loop };
+
+ cpu.dump_video( &mut video_buffer[..]);
+ cpu.dump_palette(&mut palette_buffer[..]);
+ self.draw_video(&video_buffer[..], &palette_buffer[..]);
+
+ self.sync_video(frame_start);
+ }
+
+ return Ok(());
+ }
+}
diff --git a/src/app/sync_video.rs b/src/app/sync_video.rs
new file mode 100644
index 0000000..bb863d4
--- /dev/null
+++ b/src/app/sync_video.rs
@@ -0,0 +1,44 @@
+/*
+ Copyright 2021-2023 Gabriel Jensen.
+
+ This file is part of Luma.
+
+ Luma is free software: you can redistribute it
+ and/or modify it under the terms of the GNU
+ Affero General Public License as published by
+ the Free Software Foundation, either version 3
+ of the License, or (at your option) any later
+ version.
+
+ Luma is distributed in the hope that it will be
+ useful, but WITHOUT ANY WARRANTY; without even
+ the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Affero General Public License for more details.
+
+ You should have received a copy of the GNU
+ Affero General Public License along with Luma.
+ If not, see <https://www.gnu.org/licenses/>.
+*/
+
+use crate::app::App;
+
+use std::thread::sleep;
+use std::time::{Duration, Instant};
+
+impl App {
+ pub fn sync_video(&self, frame_start: Instant) {
+ // Courtesy of TASVideos: <https://tasvideos.org/PlatformFramerates>
+ // 59.7275005696058 Hz
+
+ const FRAME_DURATION: u64 = 0xFF7932;
+ let frame_duration = Duration::from_nanos(FRAME_DURATION);
+
+ let remaining = match frame_duration.checked_sub(frame_start.elapsed()) {
+ Some(value) => value,
+ None => Duration::from_secs(0x0),
+ };
+
+ sleep(remaining);
+ }
+}