update DosLike system resource with variable screen size support

also re-did DosLikeConfig at the same time, making usage a bit clearer
This commit is contained in:
Gered 2023-03-29 14:19:55 -04:00
parent e18238405c
commit 7143430769
4 changed files with 198 additions and 63 deletions

View file

@ -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<Box<[u32]>>,
}
// 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<Self, SdlFramebufferError> {
// 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(())
}
}

View file

@ -8,6 +8,7 @@ pub mod event;
pub mod input_devices; pub mod input_devices;
pub mod res; pub mod res;
mod framebuffer;
pub mod prelude; pub mod prelude;
fn is_x11_compositor_skipping_problematic() -> bool { fn is_x11_compositor_skipping_problematic() -> bool {

View file

@ -26,14 +26,14 @@
//! ``` //! ```
//! //!
use byte_slice_cast::AsByteSlice;
use crate::audio::queue::AudioQueue; use crate::audio::queue::AudioQueue;
use crate::audio::{Audio, TARGET_AUDIO_CHANNELS, TARGET_AUDIO_FREQUENCY}; use crate::audio::{Audio, TARGET_AUDIO_CHANNELS, TARGET_AUDIO_FREQUENCY};
use crate::graphics::bitmap::indexed::IndexedBitmap; use crate::graphics::bitmap::indexed::IndexedBitmap;
use crate::graphics::font::BitmaskFont; use crate::graphics::font::BitmaskFont;
use crate::graphics::palette::Palette; use crate::graphics::palette::Palette;
use crate::prelude::WindowEvent;
use crate::system::event::{SystemEvent, SystemEventHandler}; 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::keyboard::Keyboard;
use crate::system::input_devices::mouse::cursor::CustomMouseCursor; use crate::system::input_devices::mouse::cursor::CustomMouseCursor;
use crate::system::input_devices::mouse::Mouse; use crate::system::input_devices::mouse::Mouse;
@ -48,22 +48,48 @@ const DEFAULT_SCALE_FACTOR: u32 = 3;
pub struct DosLikeConfig { pub struct DosLikeConfig {
screen_width: u32, screen_width: u32,
screen_height: u32, screen_height: u32,
fixed_screen_size: bool,
initial_scale_factor: u32, initial_scale_factor: u32,
integer_scaling: bool, integer_scaling: bool,
} }
impl DosLikeConfig { impl Default for DosLikeConfig {
/// Returns a new [`DosLikeConfig`] with a default configuration. /// 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 { DosLikeConfig {
screen_width: DEFAULT_SCREEN_WIDTH, screen_width,
screen_height: DEFAULT_SCREEN_HEIGHT, screen_height,
initial_scale_factor: DEFAULT_SCALE_FACTOR, 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 /// Sets an integer scaling factor for the [`System`] being built to up-scale the virtual
/// framebuffer to when displaying it on screen. /// framebuffer to when displaying it on screen.
@ -71,13 +97,6 @@ impl DosLikeConfig {
self.initial_scale_factor = scale_factor; self.initial_scale_factor = scale_factor;
self 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 { impl SystemResourcesConfig for DosLikeConfig {
@ -89,8 +108,6 @@ impl SystemResourcesConfig for DosLikeConfig {
audio_subsystem: &sdl2::AudioSubsystem, audio_subsystem: &sdl2::AudioSubsystem,
mut window: sdl2::video::Window, mut window: sdl2::video::Window,
) -> Result<Self::SystemResourcesType, SystemResourcesError> { ) -> Result<Self::SystemResourcesType, SystemResourcesError> {
let texture_pixel_size = 4; // 32-bit ARGB format
let window_width = self.screen_width * self.initial_scale_factor; let window_width = self.screen_width * self.initial_scale_factor;
let window_height = self.screen_height * 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) { if let Err(error) = window.set_size(window_width, window_height) {
@ -104,9 +121,6 @@ impl SystemResourcesConfig for DosLikeConfig {
Ok(canvas) => canvas, Ok(canvas) => canvas,
Err(error) => return Err(SystemResourcesError::SDLError(error.to_string())), 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 // TODO: newer versions of rust-sdl2 support this directly off the WindowCanvas struct
unsafe { unsafe {
@ -120,30 +134,14 @@ impl SystemResourcesConfig for DosLikeConfig {
); );
} }
// create an SDL texture which we will be uploading to every frame to display the // create the SDL framebuffer at the initial logical screen size
// application's framebuffer
let sdl_texture = match sdl_canvas.create_texture_streaming( let framebuffer = SdlFramebuffer::new(&mut sdl_canvas, self.screen_width, self.screen_height, true)?;
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();
// create the Bitmap object that will be exposed to the application acting as the system // create the Bitmap object that will be exposed to the application acting as the system
// backbuffer // 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, Ok(bmp) => bmp,
Err(error) => return Err(SystemResourcesError::SDLError(error.to_string())), Err(error) => return Err(SystemResourcesError::SDLError(error.to_string())),
}; };
@ -182,13 +180,13 @@ impl SystemResourcesConfig for DosLikeConfig {
Ok(DosLike { Ok(DosLike {
sdl_canvas, sdl_canvas,
sdl_texture, framebuffer,
sdl_texture_pitch, scale_factor: self.initial_scale_factor,
texture_pixels, fixed_screen_size: self.fixed_screen_size,
audio, audio,
audio_queue, audio_queue,
palette, palette,
video: framebuffer, video: screen_bitmap,
font, font,
keyboard, keyboard,
mouse, mouse,
@ -201,9 +199,9 @@ impl SystemResourcesConfig for DosLikeConfig {
/// audio via [`Audio`] and keyboard/mouse input. /// audio via [`Audio`] and keyboard/mouse input.
pub struct DosLike { pub struct DosLike {
sdl_canvas: sdl2::render::WindowCanvas, sdl_canvas: sdl2::render::WindowCanvas,
sdl_texture: sdl2::render::Texture, framebuffer: SdlFramebuffer,
sdl_texture_pitch: usize, scale_factor: u32,
texture_pixels: Box<[u32]>, fixed_screen_size: bool,
/// An [`Audio`] instance that allows interacting with the system's audio output device. /// An [`Audio`] instance that allows interacting with the system's audio output device.
pub audio: Audio, pub audio: Audio,
@ -267,24 +265,8 @@ impl SystemResources for DosLike {
/// to fill the window (preserving aspect ratio of course). /// to fill the window (preserving aspect ratio of course).
fn display(&mut self) -> Result<(), SystemResourcesError> { fn display(&mut self) -> Result<(), SystemResourcesError> {
self.cursor.render(&mut self.video); self.cursor.render(&mut self.video);
self.framebuffer.display_indexed_bitmap(&mut self.sdl_canvas, &self.video, &self.palette)?;
// 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.cursor.hide(&mut self.video); self.cursor.hide(&mut self.video);
Ok(()) Ok(())
} }
@ -295,6 +277,13 @@ impl SystemResources for DosLike {
} }
fn handle_event(&mut self, event: &SystemEvent) -> Result<bool, SystemResourcesError> { fn handle_event(&mut self, event: &SystemEvent) -> Result<bool, SystemResourcesError> {
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) { if self.keyboard.handle_event(event) {
return Ok(true); return Ok(true);
} }
@ -314,3 +303,21 @@ impl SystemResources for DosLike {
self.video.height() 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(())
}
}

View file

@ -3,6 +3,7 @@ use thiserror::Error;
use crate::audio::device::AudioDeviceError; use crate::audio::device::AudioDeviceError;
use crate::audio::AudioError; use crate::audio::AudioError;
use crate::system::event::SystemEvent; use crate::system::event::SystemEvent;
use crate::system::framebuffer::SdlFramebufferError;
pub mod dos_like; pub mod dos_like;
pub mod standard; pub mod standard;
@ -12,6 +13,9 @@ pub enum SystemResourcesError {
#[error("SystemResources SDL error: {0}")] #[error("SystemResources SDL error: {0}")]
SDLError(String), SDLError(String),
#[error("SdlFramebufferError: {0}")]
SdlFramebufferError(#[from] SdlFramebufferError),
#[error("System audioerror: {0}")] #[error("System audioerror: {0}")]
AudioError(#[from] AudioError), AudioError(#[from] AudioError),