From 71434307691a9766c459f85525b826a60cdcd852 Mon Sep 17 00:00:00 2001 From: gered Date: Wed, 29 Mar 2023 14:19:55 -0400 Subject: [PATCH] update DosLike system resource with variable screen size support also re-did DosLikeConfig at the same time, making usage a bit clearer --- ggdt/src/system/framebuffer.rs | 123 +++++++++++++++++++++++++++++ ggdt/src/system/mod.rs | 1 + ggdt/src/system/res/dos_like.rs | 133 +++++++++++++++++--------------- ggdt/src/system/res/mod.rs | 4 + 4 files changed, 198 insertions(+), 63 deletions(-) create mode 100644 ggdt/src/system/framebuffer.rs diff --git a/ggdt/src/system/framebuffer.rs b/ggdt/src/system/framebuffer.rs new file mode 100644 index 0000000..d165688 --- /dev/null +++ b/ggdt/src/system/framebuffer.rs @@ -0,0 +1,123 @@ +use byte_slice_cast::AsByteSlice; +use thiserror::Error; + +use crate::graphics::bitmap::indexed::IndexedBitmap; +use crate::graphics::bitmap::rgb::RgbaBitmap; +use crate::graphics::palette::Palette; + +pub fn calculate_logical_screen_size(window_width: u32, window_height: u32, scale_factor: u32) -> (u32, u32) { + let logical_width = (window_width as f32 / scale_factor as f32).ceil() as u32; + let logical_height = (window_height as f32 / scale_factor as f32).ceil() as u32; + (logical_width, logical_height) +} + +const SCREEN_TEXTURE_PIXEL_SIZE: usize = 4; // 32-bit ARGB format + +#[derive(Error, Debug)] +pub enum SdlFramebufferError { + #[error("SdlFramebufferError SDL error: {0}")] + SDLError(String), +} + +/// Internal structure to manager the SDL renderer/canvas and texture bits surrounding how we display our +/// [`SystemResources`] implementation managed [`Bitmap`][Bitmap] backbuffer to the actual screen. +/// +/// [Bitmap]: crate::graphics::bitmap::Bitmap +pub struct SdlFramebuffer { + sdl_texture: sdl2::render::Texture, + sdl_texture_pitch: usize, + intermediate_texture: Option>, +} + +// TODO: i'm not totally happy with this implementation. i don't like the two display methods and how the caller +// can technically call the wrong method. while i'm quite sure this won't happen in practice, it still feels +// like a bad design which we could do better. but this is simple for now, so i'll leave it for the time being. +// since this is all not in a public module, that seems like less of a big deal to me as well. for now. +impl SdlFramebuffer { + pub fn new( + canvas: &mut sdl2::render::WindowCanvas, + logical_screen_width: u32, + logical_screen_height: u32, + create_intermediate_texture: bool, + ) -> Result { + // this sets up screen/window resolution independant rendering on the SDL-side of things + // which we may or may not actually need, but this ALSO changes the way that SDL reports things + // like mouse cursor coordinates. so we benefit from setting the canvas logical screen size + // to always match our window size, even when we are using variable screen sizes. + if let Err(error) = canvas.set_logical_size(logical_screen_width, logical_screen_height) { + return Err(SdlFramebufferError::SDLError(error.to_string())); + } + + let sdl_texture = match canvas.create_texture_streaming( + Some(sdl2::pixels::PixelFormatEnum::ARGB8888), + logical_screen_width, + logical_screen_height, + ) { + Ok(texture) => texture, + Err(error) => return Err(SdlFramebufferError::SDLError(error.to_string())), + }; + let sdl_texture_pitch = sdl_texture.query().width as usize * SCREEN_TEXTURE_PIXEL_SIZE; + + let intermediate_texture = if create_intermediate_texture { + // 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 = + (logical_screen_width * logical_screen_height) as usize * SCREEN_TEXTURE_PIXEL_SIZE; + Some(vec![0u32; texture_pixels_size].into_boxed_slice()) + } else { + None + }; + + Ok(SdlFramebuffer { sdl_texture, sdl_texture_pitch, intermediate_texture }) + } + + pub fn display_indexed_bitmap( + &mut self, + canvas: &mut sdl2::render::WindowCanvas, + src: &IndexedBitmap, + palette: &Palette, + ) -> Result<(), SdlFramebufferError> { + let intermediate_texture = &mut self.intermediate_texture.as_mut().expect( + "Calls to display_indexed_bitmap should only occur on SdlFramebuffers with an intermediate_texture", + ); + + src.copy_as_argb_to(intermediate_texture, palette); + + let texture_pixels = intermediate_texture.as_byte_slice(); + if let Err(error) = self.sdl_texture.update(None, texture_pixels, self.sdl_texture_pitch) { + return Err(SdlFramebufferError::SDLError(error.to_string())); + } + canvas.clear(); + if let Err(error) = canvas.copy(&self.sdl_texture, None, None) { + return Err(SdlFramebufferError::SDLError(error)); + } + canvas.present(); + + Ok(()) + } + + pub fn display( + &mut self, + canvas: &mut sdl2::render::WindowCanvas, + src: &RgbaBitmap, + ) -> Result<(), SdlFramebufferError> { + assert!( + self.intermediate_texture.is_none(), + "Calls to display should only occur on SdlFramebuffers without an intermediate_texture" + ); + + let texture_pixels = src.pixels().as_byte_slice(); + if let Err(error) = self.sdl_texture.update(None, texture_pixels, self.sdl_texture_pitch) { + return Err(SdlFramebufferError::SDLError(error.to_string())); + } + canvas.clear(); + if let Err(error) = canvas.copy(&self.sdl_texture, None, None) { + return Err(SdlFramebufferError::SDLError(error)); + } + canvas.present(); + + Ok(()) + } +} diff --git a/ggdt/src/system/mod.rs b/ggdt/src/system/mod.rs index 93334cf..e8886ab 100644 --- a/ggdt/src/system/mod.rs +++ b/ggdt/src/system/mod.rs @@ -8,6 +8,7 @@ pub mod event; pub mod input_devices; pub mod res; +mod framebuffer; pub mod prelude; fn is_x11_compositor_skipping_problematic() -> bool { diff --git a/ggdt/src/system/res/dos_like.rs b/ggdt/src/system/res/dos_like.rs index 6f46a1f..a02c0fa 100644 --- a/ggdt/src/system/res/dos_like.rs +++ b/ggdt/src/system/res/dos_like.rs @@ -26,14 +26,14 @@ //! ``` //! -use byte_slice_cast::AsByteSlice; - use crate::audio::queue::AudioQueue; use crate::audio::{Audio, TARGET_AUDIO_CHANNELS, TARGET_AUDIO_FREQUENCY}; use crate::graphics::bitmap::indexed::IndexedBitmap; use crate::graphics::font::BitmaskFont; use crate::graphics::palette::Palette; +use crate::prelude::WindowEvent; use crate::system::event::{SystemEvent, SystemEventHandler}; +use crate::system::framebuffer::{calculate_logical_screen_size, SdlFramebuffer}; use crate::system::input_devices::keyboard::Keyboard; use crate::system::input_devices::mouse::cursor::CustomMouseCursor; use crate::system::input_devices::mouse::Mouse; @@ -48,22 +48,48 @@ const DEFAULT_SCALE_FACTOR: u32 = 3; pub struct DosLikeConfig { screen_width: u32, screen_height: u32, + fixed_screen_size: bool, initial_scale_factor: u32, integer_scaling: bool, } -impl DosLikeConfig { +impl Default for DosLikeConfig { /// Returns a new [`DosLikeConfig`] with a default configuration. - pub fn new() -> Self { + fn default() -> Self { + DosLikeConfig::fixed_screen_size(DEFAULT_SCREEN_WIDTH, DEFAULT_SCREEN_HEIGHT, false) + } +} + +impl DosLikeConfig { + /// Creates a configuration that will use a fixed screen size at a set scaling factor. Any window resizing + /// will simply scale up or down the final image on screen, but the application will always use the same + /// logical screen resolution, `screen_width` and `screen_height`, at runtime. + pub fn fixed_screen_size(screen_width: u32, screen_height: u32, integer_scaling: bool) -> Self { DosLikeConfig { - screen_width: DEFAULT_SCREEN_WIDTH, - screen_height: DEFAULT_SCREEN_HEIGHT, + screen_width, + screen_height, initial_scale_factor: DEFAULT_SCALE_FACTOR, - integer_scaling: false, + integer_scaling, + fixed_screen_size: true, } } - // TODO: add customization ability for setting different screen dimensions instead of it being hardcoded + /// Creates a configuration that allows the screen size to be automatically updated at runtime to match the + /// current window size, including any arbitrary user window resizing. The final image on screen will always be + /// scaled up by the factor given. The logical screen size at runtime (as seen by the application code) is + /// always based on: + /// + /// `logical_screen_width = ceil(window_width / scale_factor)` + /// `logical_screen_height = ceil(window_height / scale_factor)` + pub fn variable_screen_size(initial_width: u32, initial_height: u32) -> Self { + DosLikeConfig { + screen_width: initial_width, + screen_height: initial_height, + initial_scale_factor: DEFAULT_SCALE_FACTOR, + integer_scaling: false, + fixed_screen_size: false, + } + } /// Sets an integer scaling factor for the [`System`] being built to up-scale the virtual /// framebuffer to when displaying it on screen. @@ -71,13 +97,6 @@ impl DosLikeConfig { 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 { @@ -89,8 +108,6 @@ impl SystemResourcesConfig for DosLikeConfig { audio_subsystem: &sdl2::AudioSubsystem, mut window: sdl2::video::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) { @@ -104,9 +121,6 @@ impl SystemResourcesConfig for DosLikeConfig { 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 { @@ -120,30 +134,14 @@ impl SystemResourcesConfig for DosLikeConfig { ); } - // create an SDL texture which we will be uploading to every frame to display the - // application's framebuffer + // create the SDL framebuffer at the initial logical screen size - let sdl_texture = match sdl_canvas.create_texture_streaming( - Some(sdl2::pixels::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(); + let framebuffer = SdlFramebuffer::new(&mut sdl_canvas, self.screen_width, self.screen_height, true)?; // create the Bitmap object that will be exposed to the application acting as the system // backbuffer - let framebuffer = match IndexedBitmap::new(self.screen_width, self.screen_height) { + let screen_bitmap = match IndexedBitmap::new(self.screen_width, self.screen_height) { Ok(bmp) => bmp, Err(error) => return Err(SystemResourcesError::SDLError(error.to_string())), }; @@ -182,13 +180,13 @@ impl SystemResourcesConfig for DosLikeConfig { Ok(DosLike { sdl_canvas, - sdl_texture, - sdl_texture_pitch, - texture_pixels, + framebuffer, + scale_factor: self.initial_scale_factor, + fixed_screen_size: self.fixed_screen_size, audio, audio_queue, palette, - video: framebuffer, + video: screen_bitmap, font, keyboard, mouse, @@ -201,9 +199,9 @@ impl SystemResourcesConfig for DosLikeConfig { /// audio via [`Audio`] and keyboard/mouse input. pub struct DosLike { sdl_canvas: sdl2::render::WindowCanvas, - sdl_texture: sdl2::render::Texture, - sdl_texture_pitch: usize, - texture_pixels: Box<[u32]>, + framebuffer: SdlFramebuffer, + scale_factor: u32, + fixed_screen_size: bool, /// An [`Audio`] instance that allows interacting with the system's audio output device. pub audio: Audio, @@ -267,24 +265,8 @@ impl SystemResources for DosLike { /// to fill the window (preserving aspect ratio of course). fn display(&mut self) -> Result<(), SystemResourcesError> { self.cursor.render(&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.framebuffer.display_indexed_bitmap(&mut self.sdl_canvas, &self.video, &self.palette)?; self.cursor.hide(&mut self.video); - Ok(()) } @@ -295,6 +277,13 @@ impl SystemResources for DosLike { } fn handle_event(&mut self, event: &SystemEvent) -> Result { + if let SystemEvent::Window(WindowEvent::SizeChanged(width, height)) = event { + if !self.fixed_screen_size { + self.resize_screen(*width as u32, *height as u32)?; + } + return Ok(true); + } + if self.keyboard.handle_event(event) { return Ok(true); } @@ -314,3 +303,21 @@ impl SystemResources for DosLike { self.video.height() } } + +impl DosLike { + fn resize_screen(&mut self, new_width: u32, new_height: u32) -> Result<(), SystemResourcesError> { + let (logical_width, logical_height) = calculate_logical_screen_size(new_width, new_height, self.scale_factor); + + let framebuffer = SdlFramebuffer::new(&mut self.sdl_canvas, logical_width, logical_height, true)?; + + let screen_bitmap = match IndexedBitmap::new(logical_width, logical_height) { + Ok(bmp) => bmp, + Err(error) => return Err(SystemResourcesError::SDLError(error.to_string())), + }; + + self.framebuffer = framebuffer; + self.video = screen_bitmap; + + Ok(()) + } +} diff --git a/ggdt/src/system/res/mod.rs b/ggdt/src/system/res/mod.rs index 6eaea69..c4378fc 100644 --- a/ggdt/src/system/res/mod.rs +++ b/ggdt/src/system/res/mod.rs @@ -3,6 +3,7 @@ use thiserror::Error; use crate::audio::device::AudioDeviceError; use crate::audio::AudioError; use crate::system::event::SystemEvent; +use crate::system::framebuffer::SdlFramebufferError; pub mod dos_like; pub mod standard; @@ -12,6 +13,9 @@ pub enum SystemResourcesError { #[error("SystemResources SDL error: {0}")] SDLError(String), + #[error("SdlFramebufferError: {0}")] + SdlFramebufferError(#[from] SdlFramebufferError), + #[error("System audioerror: {0}")] AudioError(#[from] AudioError),