diff --git a/examples/audio_playback/src/main.rs b/examples/audio_playback/src/main.rs index c2924e4..b6cd654 100644 --- a/examples/audio_playback/src/main.rs +++ b/examples/audio_playback/src/main.rs @@ -40,7 +40,7 @@ impl SineWaveGenerator { } impl AudioGenerator for SineWaveGenerator { - fn gen_sample(&mut self, position: usize) -> Option { + fn gen_sample(&mut self, _position: usize) -> Option { const MAX_TIME: usize = AUDIO_FREQUENCY_22KHZ as usize * 3; // 3 seconds if self.t < MAX_TIME { let sample = (self.t as f64 * 0.25).sin() * 80.0; @@ -53,103 +53,104 @@ impl AudioGenerator for SineWaveGenerator { } fn main() -> Result<()> { - let mut system = SystemBuilder::new().window_title("Audio Playback").vsync(true).build()?; + let config = DosLikeConfig::new().vsync(true); + let mut system = SystemBuilder::new().window_title("Audio Playback").build(config)?; let mut using_queue_commands = false; let mut volume = 1.0; let sounds = [ - load_and_convert_wav(Path::new("./assets/pickup-coin.wav"), system.audio.spec())?, - load_and_convert_wav(Path::new("./assets/powerup.wav"), system.audio.spec())?, - load_and_convert_wav(Path::new("./assets/explosion.wav"), system.audio.spec())?, - load_and_convert_wav(Path::new("./assets/jump.wav"), system.audio.spec())?, - load_and_convert_wav(Path::new("./assets/laser-shoot.wav"), system.audio.spec())?, + load_and_convert_wav(Path::new("./assets/pickup-coin.wav"), system.res.audio.spec())?, + load_and_convert_wav(Path::new("./assets/powerup.wav"), system.res.audio.spec())?, + load_and_convert_wav(Path::new("./assets/explosion.wav"), system.res.audio.spec())?, + load_and_convert_wav(Path::new("./assets/jump.wav"), system.res.audio.spec())?, + load_and_convert_wav(Path::new("./assets/laser-shoot.wav"), system.res.audio.spec())?, ]; let mut statuses = [AudioChannelStatus { size: 0, position: 0, playing: false }; NUM_CHANNELS]; - while !system.do_events() { - if system.input_devices.keyboard.is_key_pressed(Scancode::Escape) { + while !system.do_events()? { + if system.res.keyboard.is_key_pressed(Scancode::Escape) { break; } - let mut audio_device = system.audio.lock(); + let mut audio_device = system.res.audio.lock(); audio_device.volume = volume; - if system.input_devices.keyboard.is_key_pressed(Scancode::Num1) { + if system.res.keyboard.is_key_pressed(Scancode::Num1) { if using_queue_commands { - system.audio_queue.play_buffer(&sounds[0], false); + system.res.audio_queue.play_buffer(&sounds[0], false)?; } else { audio_device.play_buffer(&sounds[0], false)?; } } - if system.input_devices.keyboard.is_key_pressed(Scancode::Num2) { + if system.res.keyboard.is_key_pressed(Scancode::Num2) { if using_queue_commands { - system.audio_queue.play_buffer(&sounds[1], false); + system.res.audio_queue.play_buffer(&sounds[1], false)?; } else { audio_device.play_buffer(&sounds[1], false)?; } } - if system.input_devices.keyboard.is_key_pressed(Scancode::Num3) { + if system.res.keyboard.is_key_pressed(Scancode::Num3) { if using_queue_commands { - system.audio_queue.play_buffer(&sounds[2], false); + system.res.audio_queue.play_buffer(&sounds[2], false)?; } else { audio_device.play_buffer(&sounds[2], false)?; } } - if system.input_devices.keyboard.is_key_pressed(Scancode::Num4) { + if system.res.keyboard.is_key_pressed(Scancode::Num4) { if using_queue_commands { - system.audio_queue.play_buffer(&sounds[3], false); + system.res.audio_queue.play_buffer(&sounds[3], false)?; } else { audio_device.play_buffer(&sounds[3], false)?; } } - if system.input_devices.keyboard.is_key_pressed(Scancode::Num5) { + if system.res.keyboard.is_key_pressed(Scancode::Num5) { if using_queue_commands { - system.audio_queue.play_buffer(&sounds[4], false); + system.res.audio_queue.play_buffer(&sounds[4], false)?; } else { audio_device.play_buffer(&sounds[4], false)?; } } - if system.input_devices.keyboard.is_key_pressed(Scancode::Num6) { + if system.res.keyboard.is_key_pressed(Scancode::Num6) { if using_queue_commands { - system.audio_queue.play_generator(Box::new(SineWaveGenerator::new()), false); + system.res.audio_queue.play_generator(Box::new(SineWaveGenerator::new()), false)?; } else { - audio_device.play_generator(Box::new(SineWaveGenerator::new()), false); + audio_device.play_generator(Box::new(SineWaveGenerator::new()), false)?; } } - if system.input_devices.keyboard.is_key_pressed(Scancode::Num7) { + if system.res.keyboard.is_key_pressed(Scancode::Num7) { let index = rnd_value(0, sounds.len() - 1); if using_queue_commands { - system.audio_queue.play_buffer_on_channel(7, &sounds[index], false)?; + system.res.audio_queue.play_buffer_on_channel(7, &sounds[index], false)?; } else { audio_device.play_buffer_on_channel(7, &sounds[index], false)?; } } - if system.input_devices.keyboard.is_key_pressed(Scancode::S) { + if system.res.keyboard.is_key_pressed(Scancode::S) { if using_queue_commands { - system.audio_queue.stop_all(); + system.res.audio_queue.stop_all(); } else { audio_device.stop_all(); } } - system.audio_queue.apply_to_device(&mut audio_device)?; + system.res.audio_queue.apply_to_device(&mut audio_device)?; - if system.input_devices.keyboard.is_key_pressed(Scancode::KpMinus) { + if system.res.keyboard.is_key_pressed(Scancode::KpMinus) { volume -= 0.1; } - if system.input_devices.keyboard.is_key_pressed(Scancode::KpPlus) { + if system.res.keyboard.is_key_pressed(Scancode::KpPlus) { volume += 0.1; } - if system.input_devices.keyboard.is_key_pressed(Scancode::Q) { + if system.res.keyboard.is_key_pressed(Scancode::Q) { using_queue_commands = !using_queue_commands; } @@ -163,24 +164,31 @@ fn main() -> Result<()> { drop(audio_device); - system.video.clear(0); + system.update()?; + system.res.video.clear(0); - system.video.print_string(&format!("Volume: {:2.2}", volume), 16, 16, FontRenderOpts::Color(10), &system.font); - system.video.print_string( + system.res.video.print_string( + &format!("Volume: {:2.2}", volume), + 16, 16, FontRenderOpts::Color(10), &system.res.font + ); + system.res.video.print_string( if using_queue_commands { "Queueing Commands" } else { "Direct Commands" }, - 160, 16, FontRenderOpts::Color(9), &system.font, + 160, 16, FontRenderOpts::Color(9), &system.res.font, ); - system.video.print_string("Audio Channels", 16, 32, FontRenderOpts::Color(14), &system.font); + system.res.video.print_string( + "Audio Channels", + 16, 32, FontRenderOpts::Color(14), &system.res.font + ); let mut y = 48; for index in 0..NUM_CHANNELS { let status = &statuses[index]; - system.video.print_string( + system.res.video.print_string( &format!( "channel {} - {} {}", index, @@ -189,7 +197,7 @@ fn main() -> Result<()> { ), 16, y, FontRenderOpts::Color(15), - &system.font, + &system.res.font, ); y += 16; } diff --git a/examples/balls/src/main.rs b/examples/balls/src/main.rs index 0835e66..0628a1a 100644 --- a/examples/balls/src/main.rs +++ b/examples/balls/src/main.rs @@ -22,15 +22,15 @@ struct Ball { } fn main() -> Result<()> { + let config = DosLikeConfig::new().vsync(true); let mut system = SystemBuilder::new() .window_title("Flying Balls!") - .vsync(true) - .build()?; + .build(config)?; let font = BitmaskFont::new_vga_font()?; let (balls_bmp, balls_palette) = Bitmap::load_pcx_file(Path::new("./assets/balls.pcx"))?; - system.palette = balls_palette.clone(); + system.res.palette = balls_palette.clone(); let mut sprites = Vec::::new(); let mut balls = Vec::::new(); @@ -59,12 +59,12 @@ fn main() -> Result<()> { balls.push(ball); } - while !system.do_events() { - if system.input_devices.keyboard.is_key_pressed(Scancode::Escape) { + while !system.do_events()? { + if system.res.keyboard.is_key_pressed(Scancode::Escape) { break; } - if system.input_devices.keyboard.is_key_up(Scancode::S) { + if system.res.keyboard.is_key_up(Scancode::S) { for i in 0..NUM_BALLS { let ball = &mut balls[i]; ball.x += ball.dir_x; @@ -96,14 +96,13 @@ fn main() -> Result<()> { } } - system.video.clear(2); + system.update()?; + system.res.video.clear(2); - system - .video - .print_string("hello, world!", 10, 10, FontRenderOpts::Color(15), &font); + system.res.video.print_string("hello, world!", 10, 10, FontRenderOpts::Color(15), &font); for i in 0..NUM_BALLS { - system.video.blit( + system.res.video.blit( BlitMethod::Transparent(0), &sprites[balls[i].sprite], balls[i].x, diff --git a/examples/balls_v2/src/entities.rs b/examples/balls_v2/src/entities.rs index 87775d8..05232d3 100644 --- a/examples/balls_v2/src/entities.rs +++ b/examples/balls_v2/src/entities.rs @@ -165,7 +165,7 @@ fn render_system_sprites(context: &mut Context) { for (entity, sprite_index) in sprite_indices.iter() { let position = positions.get(&entity).unwrap(); - context.system.video.blit( + context.system.res.video.blit( BlitMethod::Transparent(0), &context.sprites[sprite_index.0], position.0.x as i32, @@ -206,7 +206,7 @@ fn render_system_particles(context: &mut Context) { } if let Some(color) = pixel_color { - context.system.video.set_pixel(position.0.x as i32, position.0.y as i32, color); + context.system.res.video.set_pixel(position.0.x as i32, position.0.y as i32, color); } } } diff --git a/examples/balls_v2/src/main.rs b/examples/balls_v2/src/main.rs index fbc80e1..2c28709 100644 --- a/examples/balls_v2/src/main.rs +++ b/examples/balls_v2/src/main.rs @@ -12,7 +12,8 @@ mod entities; mod states; fn main() -> Result<()> { - let system = SystemBuilder::new().window_title("Flying Balls").vsync(true).build()?; + let config = DosLikeConfig::new().vsync(true); + let system = SystemBuilder::new().window_title("Flying Balls").build(config)?; let mut game = Game::new(system)?; let mut states = States::new(); states.push(SimulationState)?; @@ -20,15 +21,16 @@ fn main() -> Result<()> { let tick_frequency = game.context.system.tick_frequency(); let mut last_ticks = game.context.system.ticks(); - while !game.context.system.do_events() && !states.is_empty() { + while !game.context.system.do_events()? && !states.is_empty() { let ticks = game.context.system.ticks(); let elapsed = ticks - last_ticks; last_ticks = ticks; game.context.delta = (elapsed as f64 / tick_frequency as f64) as f32; states.update(&mut game)?; + game.context.system.update()?; - game.context.system.video.clear(0); + game.context.system.res.video.clear(0); states.render(&mut game); game.context.system.display()?; diff --git a/examples/balls_v2/src/states.rs b/examples/balls_v2/src/states.rs index ac3ae49..f4ad943 100644 --- a/examples/balls_v2/src/states.rs +++ b/examples/balls_v2/src/states.rs @@ -13,7 +13,7 @@ pub const NUM_BALL_SPRITES: usize = 16; pub struct Context { pub delta: f32, - pub system: System, + pub system: System, pub font: BitmaskFont, pub sprites: Vec, pub entities: Entities, @@ -27,11 +27,11 @@ pub struct Game { } impl Game { - pub fn new(mut system: System) -> Result { + pub fn new(mut system: System) -> Result { let font = BitmaskFont::new_vga_font()?; let (balls_bmp, balls_palette) = Bitmap::load_pcx_file(Path::new("./assets/balls.pcx"))?; - system.palette = balls_palette.clone(); + system.res.palette = balls_palette.clone(); let mut sprites = Vec::new(); for i in 0..NUM_BALL_SPRITES { @@ -78,12 +78,12 @@ pub struct SimulationState; impl AppState for SimulationState { fn update(&mut self, _state: State, context: &mut Game) -> Option> { - if context.context.system.input_devices.keyboard.is_key_up(Scancode::S) { + if context.context.system.res.keyboard.is_key_up(Scancode::S) { context.do_events(); context.component_systems.update(&mut context.context); } - if context.context.system.input_devices.keyboard.is_key_pressed(Scancode::Escape) { + if context.context.system.res.keyboard.is_key_pressed(Scancode::Escape) { return Some(StateChange::Pop(1)); } @@ -91,9 +91,9 @@ impl AppState for SimulationState { } fn render(&mut self, _state: State, context: &mut Game) { - context.context.system.video.clear(2); + context.context.system.res.video.clear(2); context.component_systems.render(&mut context.context); - context.context.system.video.print_string("hello, world!", 10, 10, FontRenderOpts::Color(15), &context.context.font); + context.context.system.res.video.print_string("hello, world!", 10, 10, FontRenderOpts::Color(15), &context.context.font); } fn transition(&mut self, _state: State, _context: &mut Game) -> bool { diff --git a/examples/slimed/src/entities/systems.rs b/examples/slimed/src/entities/systems.rs index d32dd2c..0ec4782 100644 --- a/examples/slimed/src/entities/systems.rs +++ b/examples/slimed/src/entities/systems.rs @@ -692,7 +692,7 @@ fn render_system_sprites(context: &mut Core) { // now render them in the correct order ... for (entity, position, blit_method) in context.sprite_render_list.iter() { let sprite = sprites.get(entity).unwrap(); - context.system.video.blit_atlas( + context.system.res.video.blit_atlas( blit_method.clone(), &sprite.atlas, sprite.index, @@ -710,7 +710,7 @@ fn render_system_pixels(context: &mut Core) { if let Some((_, camera)) = context.entities.components::().single() { for (entity, pixel) in pixels.iter() { if let Some(position) = positions.get(entity) { - context.system.video.set_pixel( + context.system.res.video.set_pixel( position.0.x as i32 - camera.x, position.0.y as i32 - camera.y, pixel.0, diff --git a/examples/slimed/src/main.rs b/examples/slimed/src/main.rs index 1ce906a..d00e873 100644 --- a/examples/slimed/src/main.rs +++ b/examples/slimed/src/main.rs @@ -28,7 +28,7 @@ pub const TILE_HEIGHT: u32 = 16; pub struct Core { pub delta: f32, - pub system: System, + pub system: System, pub font: BitmaskFont, pub entities: Entities, pub event_publisher: EventPublisher, @@ -54,12 +54,12 @@ pub struct Core { pub sprite_render_list: Vec<(EntityId, Vector2, BlitMethod)>, } -impl CoreState for Core { - fn system(&self) -> &System { +impl CoreState for Core { + fn system(&self) -> &System { &self.system } - fn system_mut(&mut self) -> &mut System { + fn system_mut(&mut self) -> &mut System { &mut self.system } @@ -72,7 +72,7 @@ impl CoreState for Core { } } -impl CoreStateWithEvents for Core { +impl CoreStateWithEvents for Core { fn event_publisher(&mut self) -> &mut EventPublisher { &mut self.event_publisher } @@ -85,7 +85,7 @@ pub struct Support { impl SupportSystems for Support {} -impl SupportSystemsWithEvents for Support { +impl SupportSystemsWithEvents for Support { type ContextType = Core; fn event_listeners(&mut self) -> &mut EventListeners { @@ -98,7 +98,7 @@ pub struct Game { pub support: Support, } -impl AppContext for Game { +impl AppContext for Game { type CoreType = Core; type SupportType = Support; @@ -112,9 +112,9 @@ impl AppContext for Game { } impl Game { - pub fn new(mut system: System) -> Result { + pub fn new(mut system: System) -> Result { let palette = load_palette(Path::new("./assets/db16.pal"))?; - system.palette = palette.clone(); + system.res.palette = palette.clone(); let font = load_font(Path::new("./assets/dp.fnt"))?; @@ -196,7 +196,8 @@ impl Game { } fn main() -> Result<()> { - let system = SystemBuilder::new().window_title("Slime Stabbing Simulator").vsync(true).build()?; + let config = DosLikeConfig::new().vsync(true); + let system = SystemBuilder::new().window_title("Slime Stabbing Simulator").build(config)?; let game = Game::new(system)?; main_loop(game, MainMenuState::new()).context("Main loop error") } diff --git a/examples/slimed/src/states.rs b/examples/slimed/src/states.rs index f289ce9..ffc691c 100644 --- a/examples/slimed/src/states.rs +++ b/examples/slimed/src/states.rs @@ -27,17 +27,17 @@ impl MainMenuState { impl AppState for MainMenuState { fn update(&mut self, state: State, context: &mut Game) -> Option> { if state == State::Active { - if context.core.system.input_devices.keyboard.is_key_pressed(Scancode::Escape) { + if context.core.system.res.keyboard.is_key_pressed(Scancode::Escape) { return Some(StateChange::Pop(1)); } - if context.core.system.input_devices.keyboard.is_key_pressed(Scancode::Up) { + if context.core.system.res.keyboard.is_key_pressed(Scancode::Up) { self.selection = (self.selection - 1).clamp(0, 1); } - if context.core.system.input_devices.keyboard.is_key_pressed(Scancode::Down) { + if context.core.system.res.keyboard.is_key_pressed(Scancode::Down) { self.selection = (self.selection + 1).clamp(0, 1); } - if context.core.system.input_devices.keyboard.is_key_pressed(Scancode::Return) { + if context.core.system.res.keyboard.is_key_pressed(Scancode::Return) { match self.selection { 0 => return Some(StateChange::Push(Box::new(GamePlayState::new()))), 1 => return Some(StateChange::Pop(1)), @@ -53,7 +53,7 @@ impl AppState for MainMenuState { } fn render(&mut self, state: State, context: &mut Game) { - context.core.tilemap.draw(&mut context.core.system.video, &context.core.tiles, 0, 0); + context.core.tilemap.draw(&mut context.core.system.res.video, &context.core.tiles, 0, 0); context.support.component_systems.render(&mut context.core); let x = 32; @@ -61,13 +61,13 @@ impl AppState for MainMenuState { let width = 48; let height = 40; const SPACER: i32 = 8; - draw_window(&mut context.core.system.video, &context.core.ui, x, y, x + width, y + height); + draw_window(&mut context.core.system.res.video, &context.core.ui, x, y, x + width, y + height); let selection_y = y + SPACER + (self.selection as i32 * 16); - context.core.system.video.print_string(">", x + SPACER, selection_y, FontRenderOpts::Color(15), &context.core.font); + context.core.system.res.video.print_string(">", x + SPACER, selection_y, FontRenderOpts::Color(15), &context.core.font); - context.core.system.video.print_string("Play", x + SPACER + SPACER, y + SPACER, FontRenderOpts::Color(15), &context.core.font); - context.core.system.video.print_string("Quit", x + SPACER + SPACER, y + SPACER + 16, FontRenderOpts::Color(15), &context.core.font); + context.core.system.res.video.print_string("Play", x + SPACER + SPACER, y + SPACER, FontRenderOpts::Color(15), &context.core.font); + context.core.system.res.video.print_string("Quit", x + SPACER + SPACER, y + SPACER + 16, FontRenderOpts::Color(15), &context.core.font); } fn transition(&mut self, state: State, context: &mut Game) -> bool { @@ -86,7 +86,7 @@ impl AppState for MainMenuState { self.fade = 1.0; } State::Paused => { - context.core.system.palette = context.core.palette.clone(); + context.core.system.res.palette = context.core.palette.clone(); } _ => {} } @@ -113,17 +113,17 @@ impl AppState for GamePlayState { fn update(&mut self, state: State, context: &mut Game) -> Option> { if state == State::Active { if self.in_menu { - if context.core.system.input_devices.keyboard.is_key_pressed(Scancode::Escape) { + if context.core.system.res.keyboard.is_key_pressed(Scancode::Escape) { self.in_menu = false; } - if context.core.system.input_devices.keyboard.is_key_pressed(Scancode::Up) { + if context.core.system.res.keyboard.is_key_pressed(Scancode::Up) { self.selection = (self.selection - 1).clamp(0, 1); } - if context.core.system.input_devices.keyboard.is_key_pressed(Scancode::Down) { + if context.core.system.res.keyboard.is_key_pressed(Scancode::Down) { self.selection = (self.selection + 1).clamp(0, 1); } - if context.core.system.input_devices.keyboard.is_key_pressed(Scancode::Return) { + if context.core.system.res.keyboard.is_key_pressed(Scancode::Return) { match self.selection { 0 => self.in_menu = false, 1 => return Some(StateChange::Pop(1)), @@ -131,24 +131,24 @@ impl AppState for GamePlayState { } } } else { - if context.core.system.input_devices.keyboard.is_key_pressed(Scancode::Escape) { + if context.core.system.res.keyboard.is_key_pressed(Scancode::Escape) { self.in_menu = true; } if let Some((player_entity, _)) = context.core.entities.components::().single() { - if context.core.system.input_devices.keyboard.is_key_down(Scancode::Up) { + if context.core.system.res.keyboard.is_key_down(Scancode::Up) { context.core.event_publisher.queue(Event::TurnAndMove(*player_entity, Direction::North)); } - if context.core.system.input_devices.keyboard.is_key_down(Scancode::Down) { + if context.core.system.res.keyboard.is_key_down(Scancode::Down) { context.core.event_publisher.queue(Event::TurnAndMove(*player_entity, Direction::South)); } - if context.core.system.input_devices.keyboard.is_key_down(Scancode::Left) { + if context.core.system.res.keyboard.is_key_down(Scancode::Left) { context.core.event_publisher.queue(Event::TurnAndMove(*player_entity, Direction::West)); } - if context.core.system.input_devices.keyboard.is_key_down(Scancode::Right) { + if context.core.system.res.keyboard.is_key_down(Scancode::Right) { context.core.event_publisher.queue(Event::TurnAndMove(*player_entity, Direction::East)); } - if context.core.system.input_devices.keyboard.is_key_pressed(Scancode::Space) { + if context.core.system.res.keyboard.is_key_pressed(Scancode::Space) { context.core.event_publisher.queue(Event::Attack(*player_entity)); } } @@ -163,7 +163,7 @@ impl AppState for GamePlayState { fn render(&mut self, state: State, context: &mut Game) { if let Some((_, camera)) = context.core.entities.components::().single() { - context.core.tilemap.draw(&mut context.core.system.video, &context.core.tiles, camera.x, camera.y); + context.core.tilemap.draw(&mut context.core.system.res.video, &context.core.tiles, camera.x, camera.y); } context.support.component_systems.render(&mut context.core); @@ -173,13 +173,13 @@ impl AppState for GamePlayState { let width = 80; let height = 40; const SPACER: i32 = 8; - draw_window(&mut context.core.system.video, &context.core.ui, x, y, x + width, y + height); + draw_window(&mut context.core.system.res.video, &context.core.ui, x, y, x + width, y + height); let selection_y = y + SPACER + (self.selection as i32 * 16); - context.core.system.video.print_string(">", x + SPACER, selection_y, FontRenderOpts::Color(15), &context.core.font); + context.core.system.res.video.print_string(">", x + SPACER, selection_y, FontRenderOpts::Color(15), &context.core.font); - context.core.system.video.print_string("Continue", x + SPACER + SPACER, y + SPACER, FontRenderOpts::Color(15), &context.core.font); - context.core.system.video.print_string("Quit", x + SPACER + SPACER, y + SPACER + 16, FontRenderOpts::Color(15), &context.core.font); + context.core.system.res.video.print_string("Continue", x + SPACER + SPACER, y + SPACER, FontRenderOpts::Color(15), &context.core.font); + context.core.system.res.video.print_string("Quit", x + SPACER + SPACER, y + SPACER + 16, FontRenderOpts::Color(15), &context.core.font); } } diff --git a/examples/slimed/src/support.rs b/examples/slimed/src/support.rs index 5dae11d..dbdd0d0 100644 --- a/examples/slimed/src/support.rs +++ b/examples/slimed/src/support.rs @@ -58,10 +58,10 @@ pub fn update_fade_transition(state: State, fade: &mut f32, delta: f32, context: *fade += delta; if *fade >= 1.0 { *fade = 1.0; - context.core.system.palette = context.core.palette.clone(); + context.core.system.res.palette = context.core.palette.clone(); true } else { - context.core.system.palette.lerp(0..=255, &context.core.fade_out_palette, &context.core.palette, *fade); + context.core.system.res.palette.lerp(0..=255, &context.core.fade_out_palette, &context.core.palette, *fade); false } } @@ -69,10 +69,10 @@ pub fn update_fade_transition(state: State, fade: &mut f32, delta: f32, context: *fade -= delta; if *fade <= 0.0 { *fade = 0.0; - context.core.system.palette = context.core.fade_out_palette.clone(); + context.core.system.res.palette = context.core.fade_out_palette.clone(); true } else { - context.core.system.palette.lerp(0..=255, &context.core.fade_out_palette, &context.core.palette, *fade); + context.core.system.res.palette.lerp(0..=255, &context.core.fade_out_palette, &context.core.palette, *fade); false } } diff --git a/examples/template_complicated/src/main.rs b/examples/template_complicated/src/main.rs index de42baf..159934b 100644 --- a/examples/template_complicated/src/main.rs +++ b/examples/template_complicated/src/main.rs @@ -61,7 +61,7 @@ pub fn update_system_movement(context: &mut Core) { pub fn update_system_remove_offscreen(context: &mut Core) { let positions = context.entities.components::().unwrap(); for (entity, position) in positions.iter() { - if !context.system.video.is_xy_visible(position.0.x as i32, position.0.y as i32) { + if !context.system.res.video.is_xy_visible(position.0.x as i32, position.0.y as i32) { context.event_publisher.queue(Event::Remove(*entity)); } } @@ -73,7 +73,7 @@ pub fn render_system_pixels(context: &mut Core) { for (entity, position) in positions.iter() { let color = colors.get(entity).unwrap(); - context.system.video.set_pixel(position.0.x as i32, position.0.y as i32, color.0); + context.system.res.video.set_pixel(position.0.x as i32, position.0.y as i32, color.0); } } @@ -100,7 +100,7 @@ impl DemoState { impl AppState for DemoState { fn update(&mut self, state: State, context: &mut App) -> Option> { if state == State::Active { - if context.core.system.input_devices.keyboard.is_key_pressed(Scancode::Escape) { + if context.core.system.res.keyboard.is_key_pressed(Scancode::Escape) { return Some(StateChange::Pop(1)); } } @@ -116,7 +116,7 @@ impl AppState for DemoState { } fn render(&mut self, state: State, context: &mut App) { - context.core.system.video.clear(0); + context.core.system.res.video.clear(0); context.support.component_systems.render(&mut context.core); } @@ -138,17 +138,17 @@ impl AppState for DemoState { pub struct Core { pub delta: f32, - pub system: System, + pub system: System, pub entities: Entities, pub event_publisher: EventPublisher, } -impl CoreState for Core { - fn system(&self) -> &System { +impl CoreState for Core { + fn system(&self) -> &System { &self.system } - fn system_mut(&mut self) -> &mut System { + fn system_mut(&mut self) -> &mut System { &mut self.system } @@ -161,7 +161,7 @@ impl CoreState for Core { } } -impl CoreStateWithEvents for Core { +impl CoreStateWithEvents for Core { fn event_publisher(&mut self) -> &mut EventPublisher { &mut self.event_publisher } @@ -174,7 +174,7 @@ pub struct Support { impl SupportSystems for Support {} -impl SupportSystemsWithEvents for Support { +impl SupportSystemsWithEvents for Support { type ContextType = Core; fn event_listeners(&mut self) -> &mut EventListeners { @@ -187,7 +187,7 @@ pub struct App { pub support: Support, } -impl AppContext for App { +impl AppContext for App { type CoreType = Core; type SupportType = Support; @@ -201,7 +201,7 @@ impl AppContext for App { } impl App { - pub fn new(system: System) -> Result { + pub fn new(system: System) -> Result { let entities = Entities::new(); let component_systems = ComponentSystems::new(); let event_publisher = EventPublisher::new(); @@ -225,7 +225,8 @@ impl App { ////////////////////////////////////////////////////////////////////////////////////////////////// fn main() -> Result<()> { - let system = SystemBuilder::new().window_title("Complicated Template").vsync(true).build()?; + let config = DosLikeConfig::new().vsync(true); + let system = SystemBuilder::new().window_title("Complicated Template").build(config)?; let app = App::new(system)?; main_loop(app, DemoState).context("Main loop error") } diff --git a/examples/template_minimal/src/main.rs b/examples/template_minimal/src/main.rs index 4ffc9e7..11ce3c7 100644 --- a/examples/template_minimal/src/main.rs +++ b/examples/template_minimal/src/main.rs @@ -6,22 +6,23 @@ use ggdt::system::*; use ggdt::utils::rnd_value; fn main() -> Result<()> { - let mut system = SystemBuilder::new().window_title("Minimal Template").vsync(true).build()?; + let config = DosLikeConfig::new().vsync(true); + let mut system = SystemBuilder::new().window_title("Minimal Template").build(config)?; - let font = BitmaskFont::new_vga_font()?; + system.res.video.clear(0); + system.res.video.print_string("Hello, world!", 20, 20, FontRenderOpts::Color(15), &system.res.font); - system.video.clear(0); - system.video.print_string("Hello, world!", 20, 20, FontRenderOpts::Color(15), &font); - - while !system.do_events() { - if system.input_devices.keyboard.is_key_pressed(Scancode::Escape) { + while !system.do_events()? { + if system.res.keyboard.is_key_pressed(Scancode::Escape) { break; } + system.update()?; + let x = rnd_value(0, SCREEN_RIGHT) as i32; let y = rnd_value(0, SCREEN_BOTTOM) as i32; let color = rnd_value(0, 255); - system.video.set_pixel(x, y, color); + system.res.video.set_pixel(x, y, color); system.display()?; } diff --git a/ggdt/src/base/mod.rs b/ggdt/src/base/mod.rs index 815d1f0..42b567d 100644 --- a/ggdt/src/base/mod.rs +++ b/ggdt/src/base/mod.rs @@ -19,7 +19,7 @@ //! pub enum Event { /* .. various events here .. */ } //! struct App { //! pub delta: f32, -//! pub system: ggdt::system::System, +//! pub system: ggdt::system::System, //! pub entities: ggdt::entities::Entities, //! pub component_systems: ggdt::entities::ComponentSystems, // oh no! :'( //! pub event_publisher: ggdt::events::EventPublisher, @@ -42,7 +42,7 @@ //! // "core" because what the heck else do i call this? "InnerContext"? "InnerApp"? ... //! struct Core { //! pub delta: f32, -//! pub system: ggdt::system::System, +//! pub system: ggdt::system::System, //! pub entities: ggdt::entities::Entities, //! pub event_publisher: ggdt::events::EventPublisher, //! } @@ -81,7 +81,7 @@ //! // with. you'd probably want to put your game/app resources/assets on this struct too. //! struct Core { //! pub delta: f32, -//! pub system: ggdt::system::System, +//! pub system: ggdt::system::System, //! pub entities: ggdt::entities::Entities, //! pub event_publisher: ggdt::events::EventPublisher, //! } @@ -129,9 +129,10 @@ use crate::events::*; use crate::states::*; use crate::system::*; -pub trait CoreState { - fn system(&self) -> &System; - fn system_mut(&mut self) -> &mut System; +pub trait CoreState +where SystemResType: SystemResources { + fn system(&self) -> &System; + fn system_mut(&mut self) -> &mut System; fn delta(&self) -> f32; fn set_delta(&mut self, delta: f32); @@ -145,15 +146,17 @@ pub trait CoreState { } } -pub trait CoreStateWithEvents: CoreState { +pub trait CoreStateWithEvents: CoreState +where SystemResType: SystemResources { fn event_publisher(&mut self) -> &mut EventPublisher; } pub trait SupportSystems {} -pub trait SupportSystemsWithEvents: SupportSystems +pub trait SupportSystemsWithEvents: SupportSystems +where SystemResType: SystemResources { - type ContextType: CoreStateWithEvents; + type ContextType: CoreStateWithEvents; fn event_listeners(&mut self) -> &mut EventListeners; fn do_events(&mut self, context: &mut Self::ContextType) { @@ -162,8 +165,9 @@ pub trait SupportSystemsWithEvents: SupportSystems } } -pub trait AppContext { - type CoreType: CoreState; +pub trait AppContext +where SystemResType: SystemResources { + type CoreType: CoreState; type SupportType: SupportSystems; fn core(&mut self) -> &mut Self::CoreType; @@ -182,23 +186,24 @@ pub enum MainLoopError { AudioDeviceError(#[from] AudioDeviceError), } -pub fn main_loop( +pub fn main_loop( mut app: ContextType, initial_state: State, ) -> Result<(), MainLoopError> - where - ContextType: AppContext, - State: AppState + 'static, +where + SystemResType: SystemResources, + ContextType: AppContext, + State: AppState + 'static, { let mut states = States::new(); states.push(initial_state)?; let mut last_ticks = app.core().system().ticks(); - while !app.core().system_mut().do_events() && !states.is_empty() { + while !app.core().system_mut().do_events()? && !states.is_empty() { last_ticks = app.core().update_frame_delta(last_ticks); states.update(&mut app)?; - app.core().system_mut().apply_audio_queue()?; + app.core().system_mut().update()?; states.render(&mut app); app.core().system_mut().display()?; } diff --git a/ggdt/src/system/mod.rs b/ggdt/src/system/mod.rs index d1f4431..8a20e20 100644 --- a/ggdt/src/system/mod.rs +++ b/ggdt/src/system/mod.rs @@ -15,9 +15,12 @@ pub use self::event::*; pub use self::input_devices::*; pub use self::input_devices::keyboard::*; pub use self::input_devices::mouse::*; +pub use self::res::*; +pub use self::res::dos_like::*; pub mod event; pub mod input_devices; +pub mod res; fn is_x11_compositor_skipping_problematic() -> bool { /* @@ -58,19 +61,18 @@ pub enum SystemError { #[error("System audio error: {0}")] AudioError(#[from] AudioError), + + #[error("SystemResources error: {0}")] + SystemResourcesError(#[from] SystemResourcesError), } /// Builder for configuring and constructing an instance of [`System`]. #[derive(Debug)] pub struct SystemBuilder { window_title: String, - vsync: bool, - target_framerate: Option, - initial_scale_factor: u32, resizable: bool, show_mouse: bool, relative_mouse_scaling: bool, - integer_scaling: bool, skip_x11_compositor: bool, } @@ -79,13 +81,9 @@ impl SystemBuilder { pub fn new() -> SystemBuilder { SystemBuilder { window_title: String::new(), - vsync: false, - target_framerate: None, - initial_scale_factor: DEFAULT_SCALE_FACTOR, resizable: true, show_mouse: false, relative_mouse_scaling: true, - integer_scaling: false, skip_x11_compositor: !is_x11_compositor_skipping_problematic(), } } @@ -96,30 +94,6 @@ impl SystemBuilder { self } - /// Enables or disables V-Sync for the [`System`] to be built. Enabling V-sync automatically - /// disables `target_framerate`. - pub fn vsync(&mut self, enable: bool) -> &mut SystemBuilder { - self.vsync = enable; - self.target_framerate = None; - self - } - - /// Sets a target framerate for the [`System`] being built to run at. This is intended to be - /// used when V-sync is not desired, so setting a target framerate automatically disables - /// `vsync`. - pub fn target_framerate(&mut self, target_framerate: u32) -> &mut SystemBuilder { - self.target_framerate = Some(target_framerate); - self.vsync = false; - self - } - - /// Sets an integer scaling factor for the [`System`] being built to up-scale the virtual - /// framebuffer to when displaying it on screen. - pub fn scale_factor(&mut self, scale_factor: u32) -> &mut SystemBuilder { - self.initial_scale_factor = scale_factor; - self - } - /// Sets whether the window will be resizable by the user for the [`System`] being built. pub fn resizable(&mut self, enable: bool) -> &mut SystemBuilder { self.resizable = enable; @@ -141,13 +115,6 @@ impl SystemBuilder { self } - /// Enables or disables restricting the final rendered output to always be integer scaled, - /// even if that result will not fully fill the area of the window. - pub fn integer_scaling(&mut self, enable: bool) -> &mut SystemBuilder { - self.integer_scaling = enable; - self - } - /// Enables or disables skipping the X11 server compositor on Linux systems only. This can be /// set to manually control the underlying SDL hint that is used to control this setting. The /// default setting that [`SystemBuilder`] configures is to follow the SDL default, except where @@ -159,12 +126,10 @@ impl SystemBuilder { } /// Builds and returns a [`System`] from the current configuration. - pub fn build(&self) -> Result { - // todo: maybe let this be customized in the future, or at least halved so a 160x120 mode can be available ... ? - let screen_width = SCREEN_WIDTH; - let screen_height = SCREEN_HEIGHT; - let texture_pixel_size = 4; // 32-bit ARGB format - + pub fn build( + &self, + config: ConfigType, + ) -> Result, SystemError> { sdl2::hint::set( "SDL_MOUSE_RELATIVE_SCALING", if self.relative_mouse_scaling { @@ -210,14 +175,13 @@ impl SystemBuilder { Err(message) => return Err(SystemError::SDLError(message)), }; - // create the window + // create the window with an initial default size that will be overridden during + // SystemResources initialization - let window_width = screen_width * self.initial_scale_factor; - let window_height = screen_height * self.initial_scale_factor; let mut window_builder = &mut (sdl_video_subsystem.window( self.window_title.as_str(), - window_width, - window_height, + 640, + 480, )); if self.resizable { window_builder = window_builder.resizable(); @@ -229,113 +193,24 @@ impl SystemBuilder { sdl_context.mouse().show_cursor(self.show_mouse); - // turn the window into a canvas (under the hood, an SDL Renderer that owns the window) - - let mut canvas_builder = sdl_window.into_canvas(); - if self.vsync { - canvas_builder = canvas_builder.present_vsync(); - } - let mut sdl_canvas = match canvas_builder.build() { - Ok(canvas) => canvas, - Err(error) => return Err(SystemError::SDLError(error.to_string())), - }; - if let Err(error) = sdl_canvas.set_logical_size(screen_width, screen_height) { - return Err(SystemError::SDLError(error.to_string())); - }; - - // TODO: newer versions of rust-sdl2 support this directly off the WindowCanvas struct - unsafe { - sdl2::sys::SDL_RenderSetIntegerScale( - sdl_canvas.raw(), - if self.integer_scaling { - sdl2::sys::SDL_bool::SDL_TRUE - } else { - sdl2::sys::SDL_bool::SDL_FALSE - }, - ); - } - - // create an SDL texture which we will be uploading to every frame to display the - // application's framebuffer - - let sdl_texture = match sdl_canvas.create_texture_streaming( - Some(PixelFormatEnum::ARGB8888), - screen_width, - screen_height, + let system_resources = match config.build( + &sdl_video_subsystem, + &sdl_audio_subsystem, + sdl_window ) { - Ok(texture) => texture, - Err(error) => return Err(SystemError::SDLError(error.to_string())), + Ok(system_resources) => system_resources, + Err(error) => return Err(SystemError::SystemResourcesError(error)), }; - let sdl_texture_pitch = (sdl_texture.query().width * texture_pixel_size) as usize; - - // create a raw 32-bit RGBA buffer that will be used as the temporary source for - // SDL texture uploads each frame. necessary as applications are dealing with 8-bit indexed - // bitmaps, not 32-bit RGBA pixels, so this temporary buffer is where we convert the final - // application framebuffer to 32-bit RGBA pixels before it is uploaded to the SDL texture - let texture_pixels_size = (screen_width * screen_height * texture_pixel_size) as usize; - let texture_pixels = vec![0u32; texture_pixels_size].into_boxed_slice(); - - // create the Bitmap object that will be exposed to the application acting as the system - // backbuffer - - let framebuffer = match Bitmap::new(SCREEN_WIDTH, SCREEN_HEIGHT) { - Ok(bmp) => bmp, - Err(error) => return Err(SystemError::SDLError(error.to_string())), - }; - - // create the default palette, initialized to the VGA default palette. also exposed to the - // application for manipulation - - let palette = match Palette::new_vga_palette() { - Ok(palette) => palette, - Err(error) => return Err(SystemError::SDLError(error.to_string())), - }; - - // create the default font, initialized to the VGA BIOS default font. - - let font = match BitmaskFont::new_vga_font() { - Ok(font) => font, - Err(error) => return Err(SystemError::SDLError(error.to_string())), - }; - - let audio_spec = AudioSpecDesired { - freq: Some(TARGET_AUDIO_FREQUENCY as i32), - channels: Some(TARGET_AUDIO_CHANNELS), - samples: None, - }; - let mut audio = Audio::new(audio_spec, &sdl_audio_subsystem)?; - audio.resume(); - let audio_queue = AudioQueue::new(&audio); let event_pump = SystemEventPump::from(sdl_event_pump); - // create input device objects, exposed to the application - - let keyboard = Keyboard::new(); - let mouse = Mouse::new(); - - let input_devices = InputDevices { - keyboard, - mouse, - }; - Ok(System { sdl_context, sdl_audio_subsystem, sdl_video_subsystem, sdl_timer_subsystem, - sdl_canvas, - sdl_texture, - sdl_texture_pitch, - texture_pixels, - audio, - audio_queue, - video: framebuffer, - palette, - font, - input_devices, + res: system_resources, event_pump, - target_framerate: self.target_framerate, target_framerate_delta: None, next_tick: 0, }) @@ -346,101 +221,45 @@ impl SystemBuilder { /// applications to render to the display, react to input device events, etc. through the /// "virtual machine" exposed by this library. #[allow(dead_code)] -pub struct System { +pub struct System +where SystemResType: SystemResources { sdl_context: Sdl, sdl_audio_subsystem: AudioSubsystem, sdl_video_subsystem: VideoSubsystem, sdl_timer_subsystem: TimerSubsystem, - sdl_canvas: WindowCanvas, - sdl_texture: Texture, - sdl_texture_pitch: usize, - texture_pixels: Box<[u32]>, - - target_framerate: Option, target_framerate_delta: Option, next_tick: i64, - /// An [`Audio`] instance that allows interacting with the system's audio output device. - pub audio: Audio, - - /// An [`AudioQueue`] instance that can queue up playback/stop commands to be issued to the - /// system's [`Audio`] instance a bit more flexibly. If you use this, your application must - /// manually call [`AudioQueue::apply`] or [`AudioQueue::apply_to_device`] in your loop to - /// flush the queued commands, otherwise this queue will not do anything. - pub audio_queue: AudioQueue, - - /// The primary backbuffer [`Bitmap`] that will be rendered to the screen whenever - /// [`System::display`] is called. Regardless of the actual window size, this bitmap is always - /// [`SCREEN_WIDTH`]x[`SCREEN_HEIGHT`] pixels in size. - pub video: Bitmap, - - /// The [`Palette`] that will be used in conjunction with the `video` backbuffer to - /// render the final output to the screen whenever [`System::display`] is called. - pub palette: Palette, - - /// A pre-loaded [`Font`] that can be used for text rendering. - pub font: BitmaskFont, - - /// Contains instances representing the current state of the input devices available. - /// To ensure these are updated each frame, ensure that you are either calling - /// [`System::do_events`] or manually implementing an event polling loop which calls - /// [`InputDevices::update`] and [`InputDevices::handle_event`]. - pub input_devices: InputDevices, + pub res: SystemResType, pub event_pump: SystemEventPump, } -impl std::fmt::Debug for System { +impl std::fmt::Debug for System +where SystemResType: SystemResources { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("System") - .field("audio", &self.audio) - .field("audio_queue", &self.audio_queue) - .field("video", &self.video) - .field("palette", &self.palette) - .field("font", &self.font) - //.field("keyboard", &self.keyboard) - //.field("mouse", &self.mouse) - .field("target_framerate", &self.target_framerate) + .field("res", &self.res) .field("target_framerate_delta", &self.target_framerate_delta) .field("next_tick", &self.next_tick) .finish_non_exhaustive() } } -impl System { +impl System +where SystemResType: SystemResources { /// Takes the `video` backbuffer bitmap and `palette` and renders it to the window, up-scaled /// to fill the window (preserving aspect ratio of course). If V-sync is enabled, this method /// will block to wait for V-sync. Otherwise, if a target framerate was configured a delay /// might be used to try to meet that framerate. pub fn display(&mut self) -> Result<(), SystemError> { - self.input_devices.mouse.render_cursor(&mut self.video); - - // convert application framebuffer to 32-bit RGBA pixels, and then upload it to the SDL - // texture so it will be displayed on screen - - self.video - .copy_as_argb_to(&mut self.texture_pixels, &self.palette); - - let texture_pixels = self.texture_pixels.as_byte_slice(); - if let Err(error) = self - .sdl_texture - .update(None, texture_pixels, self.sdl_texture_pitch) - { - return Err(SystemError::SDLError(error.to_string())); - } - self.sdl_canvas.clear(); - if let Err(error) = self.sdl_canvas.copy(&self.sdl_texture, None, None) { - return Err(SystemError::SDLError(error)); - } - self.sdl_canvas.present(); - - self.input_devices.mouse.hide_cursor(&mut self.video); + self.res.display()?; // if a specific target framerate is desired, apply some loop timing/delay to achieve it // TODO: do this better. delaying when running faster like this is a poor way to do this.. - if let Some(target_framerate) = self.target_framerate { + if let Some(target_framerate) = self.res.target_framerate() { if self.target_framerate_delta.is_some() { // normal path for every other loop iteration except the first let delay = self.next_tick - self.ticks() as i64; @@ -457,8 +276,7 @@ impl System { // target_framerate_delta was not yet set to avoid doing any delay on the first // loop, just in case there was some other processing between the System struct // being created and the actual beginning of the first loop ... - self.target_framerate_delta = - Some((self.tick_frequency() / target_framerate as u64) as i64); + self.target_framerate_delta = Some((self.tick_frequency() / target_framerate as u64) as i64); } // expected time for the next display() call to happen by @@ -476,9 +294,10 @@ impl System { /// ```no_run /// use ggdt::system::*; /// - /// let mut system = SystemBuilder::new().window_title("Example").build().unwrap(); + /// let config = DosLikeConfig::new(); + /// let mut system = SystemBuilder::new().window_title("Example").build(config).unwrap(); /// - /// while !system.do_events() { + /// while !system.do_events().unwrap() { /// // ... the body of your main loop here ... /// } /// ``` @@ -490,12 +309,13 @@ impl System { /// ```no_run /// use ggdt::system::*; /// - /// let mut system = SystemBuilder::new().window_title("Example").build().unwrap(); + /// let config = DosLikeConfig::new(); + /// let mut system = SystemBuilder::new().window_title("Example").build(config).unwrap(); /// /// 'mainloop: loop { - /// system.input_devices.update(); + /// system.res.update_event_state().unwrap(); /// for event in system.event_pump.poll_iter() { - /// system.input_devices.handle_event(&event); + /// system.res.handle_event(&event).unwrap(); /// match event { /// SystemEvent::Quit => { /// break 'mainloop @@ -507,23 +327,23 @@ impl System { /// // ...the rest of the body of your main loop here ... /// } /// ``` - pub fn do_events(&mut self) -> bool { + pub fn do_events(&mut self) -> Result { let mut should_quit = false; - self.input_devices.update(); + self.res.update_event_state()?; for event in self.event_pump.poll_iter() { - self.input_devices.handle_event(&event); + self.res.handle_event(&event)?; if event == SystemEvent::Quit { should_quit = true; } } - should_quit + Ok(should_quit) } - /// Convenience method that applies any [`AudioBuffer`]s that may have been queued up on - /// [`System::audio_queue`] to the system audio device so that they will be played. Do not - /// call this when you already have an active lock on an [`AudioDevice`]. - pub fn apply_audio_queue(&mut self) -> Result<(), AudioDeviceError> { - self.audio_queue.apply(&mut self.audio) + pub fn update(&mut self) -> Result<(), SystemError> { + if let Err(error) = self.res.update() { + return Err(SystemError::SystemResourcesError(error)); + } + Ok(()) } pub fn ticks(&self) -> u64 { diff --git a/ggdt/src/system/res/dos_like.rs b/ggdt/src/system/res/dos_like.rs new file mode 100644 index 0000000..3e95c9d --- /dev/null +++ b/ggdt/src/system/res/dos_like.rs @@ -0,0 +1,298 @@ +use sdl2::video::Window; + +use crate::system::*; + +pub struct DosLikeConfig { + screen_width: u32, + screen_height: u32, + vsync: bool, + target_framerate: Option, + initial_scale_factor: u32, + integer_scaling: bool, +} + +impl DosLikeConfig { + pub fn new() -> Self { + DosLikeConfig { + screen_width: SCREEN_WIDTH, + screen_height: SCREEN_HEIGHT, + vsync: false, + target_framerate: None, + initial_scale_factor: DEFAULT_SCALE_FACTOR, + integer_scaling: false, + } + } + + /// Enables or disables V-Sync for the [`System`] to be built. Enabling V-sync automatically + /// disables `target_framerate`. + pub fn vsync(mut self, enable: bool) -> Self { + self.vsync = enable; + self.target_framerate = None; + self + } + + /// Sets a target framerate for the [`System`] being built to run at. This is intended to be + /// used when V-sync is not desired, so setting a target framerate automatically disables + /// `vsync`. + pub fn target_framerate(mut self, target_framerate: u32) -> Self { + self.target_framerate = Some(target_framerate); + self.vsync = false; + self + } + + /// Sets an integer scaling factor for the [`System`] being built to up-scale the virtual + /// framebuffer to when displaying it on screen. + pub fn scale_factor(mut self, scale_factor: u32) -> Self { + self.initial_scale_factor = scale_factor; + self + } + + /// Enables or disables restricting the final rendered output to always be integer scaled, + /// even if that result will not fully fill the area of the window. + pub fn integer_scaling(mut self, enable: bool) -> Self { + self.integer_scaling = enable; + self + } +} + +impl SystemResourcesConfig for DosLikeConfig { + type SystemResourcesType = DosLike; + + fn build( + self, + _video_subsystem: &VideoSubsystem, + audio_subsystem: &AudioSubsystem, + mut window: Window, + ) -> Result { + let texture_pixel_size = 4; // 32-bit ARGB format + + let window_width = self.screen_width * self.initial_scale_factor; + let window_height = self.screen_height * self.initial_scale_factor; + if let Err(error) = window.set_size(window_width, window_height) { + return Err(SystemResourcesError::SDLError(error.to_string())); + } + + // turn the window into a canvas (under the hood, an SDL Renderer that owns the window) + + let mut canvas_builder = window.into_canvas(); + if self.vsync { + canvas_builder = canvas_builder.present_vsync(); + } + let mut sdl_canvas = match canvas_builder.build() { + Ok(canvas) => canvas, + Err(error) => return Err(SystemResourcesError::SDLError(error.to_string())), + }; + if let Err(error) = sdl_canvas.set_logical_size(self.screen_width, self.screen_height) { + return Err(SystemResourcesError::SDLError(error.to_string())); + }; + + // TODO: newer versions of rust-sdl2 support this directly off the WindowCanvas struct + unsafe { + sdl2::sys::SDL_RenderSetIntegerScale( + sdl_canvas.raw(), + if self.integer_scaling { + sdl2::sys::SDL_bool::SDL_TRUE + } else { + sdl2::sys::SDL_bool::SDL_FALSE + }, + ); + } + + // create an SDL texture which we will be uploading to every frame to display the + // application's framebuffer + + let sdl_texture = match sdl_canvas.create_texture_streaming( + Some(PixelFormatEnum::ARGB8888), + self.screen_width, + self.screen_height, + ) { + Ok(texture) => texture, + Err(error) => return Err(SystemResourcesError::SDLError(error.to_string())), + }; + let sdl_texture_pitch = (sdl_texture.query().width * texture_pixel_size) as usize; + + // create a raw 32-bit RGBA buffer that will be used as the temporary source for + // SDL texture uploads each frame. necessary as applications are dealing with 8-bit indexed + // bitmaps, not 32-bit RGBA pixels, so this temporary buffer is where we convert the final + // application framebuffer to 32-bit RGBA pixels before it is uploaded to the SDL texture + let texture_pixels_size = (self.screen_width * self.screen_height * texture_pixel_size) as usize; + let texture_pixels = vec![0u32; texture_pixels_size].into_boxed_slice(); + + // create the Bitmap object that will be exposed to the application acting as the system + // backbuffer + + let framebuffer = match Bitmap::new(SCREEN_WIDTH, SCREEN_HEIGHT) { + Ok(bmp) => bmp, + Err(error) => return Err(SystemResourcesError::SDLError(error.to_string())), + }; + + // create the default palette, initialized to the VGA default palette. also exposed to the + // application for manipulation + + let palette = match Palette::new_vga_palette() { + Ok(palette) => palette, + Err(error) => return Err(SystemResourcesError::SDLError(error.to_string())), + }; + + // create the default font, initialized to the VGA BIOS default font. + + let font = match BitmaskFont::new_vga_font() { + Ok(font) => font, + Err(error) => return Err(SystemResourcesError::SDLError(error.to_string())), + }; + + // create audio device and queue + + let audio_spec = AudioSpecDesired { + freq: Some(TARGET_AUDIO_FREQUENCY as i32), + channels: Some(TARGET_AUDIO_CHANNELS), + samples: None, + }; + let mut audio = Audio::new(audio_spec, &audio_subsystem)?; + audio.resume(); + let audio_queue = AudioQueue::new(&audio); + + // create all of the input device objects + + let keyboard = Keyboard::new(); + let mouse = Mouse::new(); + + Ok(DosLike { + sdl_canvas, + sdl_texture, + sdl_texture_pitch, + texture_pixels, + vsync: self.vsync, + target_framerate: self.target_framerate, + audio, + audio_queue, + palette, + video: framebuffer, + font, + keyboard, + mouse, + }) + } +} + +pub struct DosLike { + sdl_canvas: WindowCanvas, + sdl_texture: Texture, + sdl_texture_pitch: usize, + texture_pixels: Box<[u32]>, + vsync: bool, + target_framerate: Option, + + /// An [`Audio`] instance that allows interacting with the system's audio output device. + pub audio: Audio, + + /// An [`AudioQueue`] instance that can queue up playback/stop commands to be issued to the + /// system's [`Audio`] instance a bit more flexibly. If you use this, your application must + /// manually call [`AudioQueue::apply`] or [`AudioQueue::apply_to_device`] in your loop to + /// flush the queued commands, otherwise this queue will not do anything. + pub audio_queue: AudioQueue, + + /// The [`Palette`] that will be used in conjunction with the `video` backbuffer to + /// render the final output to the screen whenever [`System::display`] is called. + pub palette: Palette, + + /// The primary backbuffer [`Bitmap`] that will be rendered to the screen whenever + /// [`System::display`] is called. Regardless of the actual window size, this bitmap is always + /// [`SCREEN_WIDTH`]x[`SCREEN_HEIGHT`] pixels in size. + pub video: Bitmap, + + /// A pre-loaded [`Font`] that can be used for text rendering. + pub font: BitmaskFont, + + /// The current keyboard state. To ensure it is updated each frame, you should call + /// [`System::do_events`] or [`System::do_events_with`] each frame. + pub keyboard: Keyboard, + + /// The current mouse state. To ensure it is updated each frame, you should call + /// [`System::do_events`] or [`System::do_events_with`] each frame. + pub mouse: Mouse, +} + +impl std::fmt::Debug for DosLike { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DosLike") + .field("audio", &self.audio) + .field("audio_queue", &self.audio_queue) + .field("palette", &self.palette) + .field("video", &self.video) + .field("font", &self.font) + .field("keyboard", &self.keyboard) + .field("mouse", &self.mouse) + .field("vsync", &self.vsync) + .field("target_framerate", &self.target_framerate) + .finish_non_exhaustive() + } +} + +impl SystemResources for DosLike { + fn update(&mut self) -> Result<(), SystemResourcesError> { + match self.audio_queue.apply(&mut self.audio) { + Ok(_) => Ok(()), + Err(error) => Err(SystemResourcesError::AudioDeviceError(error)) + } + } + + fn display(&mut self) -> Result<(), SystemResourcesError> { + self.mouse.render_cursor(&mut self.video); + + // convert application framebuffer to 32-bit RGBA pixels, and then upload it to the SDL + // texture so it will be displayed on screen + + self.video.copy_as_argb_to(&mut self.texture_pixels, &self.palette); + + let texture_pixels = self.texture_pixels.as_byte_slice(); + if let Err(error) = self.sdl_texture.update(None, texture_pixels, self.sdl_texture_pitch) { + return Err(SystemResourcesError::SDLError(error.to_string())); + } + self.sdl_canvas.clear(); + if let Err(error) = self.sdl_canvas.copy(&self.sdl_texture, None, None) { + return Err(SystemResourcesError::SDLError(error)); + } + self.sdl_canvas.present(); + + self.mouse.hide_cursor(&mut self.video); + + Ok(()) + } + + fn update_event_state(&mut self) -> Result<(), SystemResourcesError> { + self.keyboard.update(); + self.mouse.update(); + Ok(()) + } + + fn handle_event(&mut self, event: &SystemEvent) -> Result { + if self.keyboard.handle_event(event) { + return Ok(true); + } + if self.mouse.handle_event(event) { + return Ok(true); + } + Ok(false) + } + + #[inline] + fn width(&self) -> u32 { + self.video.width() + } + + #[inline] + fn height(&self) -> u32 { + self.video.height() + } + + #[inline] + fn vsync(&self) -> bool { + self.vsync + } + + #[inline] + fn target_framerate(&self) -> Option { + self.target_framerate + } +} diff --git a/ggdt/src/system/res/mod.rs b/ggdt/src/system/res/mod.rs new file mode 100644 index 0000000..ccf2ba9 --- /dev/null +++ b/ggdt/src/system/res/mod.rs @@ -0,0 +1,41 @@ +use thiserror::Error; + +use crate::audio::{AudioDeviceError, AudioError}; +use crate::system::SystemEvent; + +pub mod dos_like; + +#[derive(Error, Debug)] +pub enum SystemResourcesError { + #[error("SystemResources SDL error: {0}")] + SDLError(String), + + #[error("System audioerror: {0}")] + AudioError(#[from] AudioError), + + #[error("System audio device error: {0}")] + AudioDeviceError(#[from] AudioDeviceError), +} + +pub trait SystemResourcesConfig { + type SystemResourcesType: SystemResources; + + fn build( + self, + video_subsystem: &sdl2::VideoSubsystem, + audio_subsystem: &sdl2::AudioSubsystem, + window: sdl2::video::Window, + ) -> Result; +} + +pub trait SystemResources : std::fmt::Debug { + fn update(&mut self) -> Result<(), SystemResourcesError>; + fn display(&mut self) -> Result<(), SystemResourcesError>; + fn update_event_state(&mut self) -> Result<(), SystemResourcesError>; + fn handle_event(&mut self, event: &SystemEvent) -> Result; + + fn width(&self) -> u32; + fn height(&self) -> u32; + fn vsync(&self) -> bool; + fn target_framerate(&self) -> Option; +}