From fe4d673855166446cdb423cc76356c51ca16f42a Mon Sep 17 00:00:00 2001 From: jsgroth <1137683+jsgroth@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:15:22 -0500 Subject: [PATCH] [GG] add an option to render Game Gear software at SMS resolution (256x192) --- backend/smsgg-core/src/api.rs | 44 ++++++------- backend/smsgg-core/src/vdp.rs | 63 ++++++++++++------- frontend/jgenesis-cli/src/main.rs | 5 ++ frontend/jgenesis-gui/src/app/smsgg.rs | 10 +++ .../jgenesis-gui/src/app/smsgg/helptext.rs | 8 +++ frontend/jgenesis-native-config/src/smsgg.rs | 3 + frontend/jgenesis-native-driver/src/config.rs | 2 + frontend/jgenesis-web/src/config.rs | 1 + 8 files changed, 89 insertions(+), 47 deletions(-) diff --git a/backend/smsgg-core/src/api.rs b/backend/smsgg-core/src/api.rs index 214adf1e..6c2dbb21 100644 --- a/backend/smsgg-core/src/api.rs +++ b/backend/smsgg-core/src/api.rs @@ -60,13 +60,13 @@ impl DerefMut for FrameBuffer { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] pub enum SmsGgHardware { MasterSystem, GameGear, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, EnumDisplay, EnumFromStr)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode, EnumDisplay, EnumFromStr)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum SmsModel { Sms1, @@ -82,7 +82,7 @@ pub enum SmsRegion { Domestic, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Encode, Decode)] pub struct SmsGgEmulatorConfig { pub hardware: SmsGgHardware, pub sms_timing_mode: TimingMode, @@ -93,6 +93,7 @@ pub struct SmsGgEmulatorConfig { pub sms_region: SmsRegion, pub sms_crop_vertical_border: bool, pub sms_crop_left_border: bool, + pub gg_use_sms_resolution: bool, pub fm_sound_unit_enabled: bool, pub overclock_z80: bool, } @@ -110,9 +111,7 @@ pub struct SmsGgEmulator { input: InputState, audio_resampler: AudioResampler, frame_buffer: FrameBuffer, - sms_crop_vertical_border: bool, - sms_crop_left_border: bool, - overclock_z80: bool, + config: SmsGgEmulatorConfig, z80_cycles_remainder: u32, vdp_cycles_remainder: u32, frame_count: u64, @@ -137,7 +136,7 @@ impl SmsGgEmulator { log::info!("PSG version: {psg_version:?}"); let memory = Memory::new(rom, cartridge_ram); - let vdp = Vdp::new(vdp_version, config.remove_sprite_limit); + let vdp = Vdp::new(vdp_version, &config); let psg = Psg::new(psg_version); let input = InputState::new(config.sms_region); @@ -159,9 +158,7 @@ impl SmsGgEmulator { input, audio_resampler: AudioResampler::new(timing_mode), frame_buffer: FrameBuffer::new(), - sms_crop_vertical_border: config.sms_crop_vertical_border, - sms_crop_left_border: config.sms_crop_left_border, - overclock_z80: config.overclock_z80, + config, z80_cycles_remainder: 0, vdp_cycles_remainder: 0, frame_count: 0, @@ -190,24 +187,21 @@ impl SmsGgEmulator { } fn render_frame(&mut self, renderer: &mut R) -> Result<(), R::Err> { - let crop_vertical_border = - self.vdp_version.is_master_system() && self.sms_crop_vertical_border; - let crop_left_border = self.vdp_version.is_master_system() && self.sms_crop_left_border; populate_frame_buffer( self.vdp.frame_buffer(), self.vdp_version, - crop_vertical_border, - crop_left_border, + self.config.sms_crop_vertical_border, + self.config.sms_crop_left_border, &mut self.frame_buffer, ); - let viewport = self.vdp_version.viewport_size(); - let frame_width = if crop_left_border { + let viewport = self.vdp.viewport(); + let frame_width = if self.config.sms_crop_left_border { viewport.width_without_border().into() } else { viewport.width.into() }; - let frame_height = if crop_vertical_border { + let frame_height = if self.config.sms_crop_vertical_border { viewport.height_without_border().into() } else { viewport.height.into() @@ -295,7 +289,7 @@ impl EmulatorTrait for SmsGgEmulator { self.ym2413.as_mut(), &mut self.input, )); - let (t_cycles, remainder) = if self.overclock_z80 { + let (t_cycles, remainder) = if self.config.overclock_z80 { // Emulate a Z80 running at 2x speed by only ticking the rest of the components for // half as many cycles let t_cycles = t_cycles + self.z80_cycles_remainder; @@ -364,17 +358,15 @@ impl EmulatorTrait for SmsGgEmulator { } fn reload_config(&mut self, config: &Self::Config) { + self.config = *config; + self.vdp_version = determine_vdp_version(config); - self.vdp.set_version(self.vdp_version); + self.vdp.update_config(self.vdp_version, config); self.psg.set_version(determine_psg_version(config)); self.pixel_aspect_ratio = config.pixel_aspect_ratio; - self.vdp.set_remove_sprite_limit(config.remove_sprite_limit); self.input.set_region(config.sms_region); - self.sms_crop_vertical_border = config.sms_crop_vertical_border; - self.sms_crop_left_border = config.sms_crop_left_border; - self.overclock_z80 = config.overclock_z80; self.audio_resampler.update_timing_mode(self.vdp.timing_mode()); } @@ -399,7 +391,7 @@ impl EmulatorTrait for SmsGgEmulator { self.z80 = Z80::new(); init_z80(&mut self.z80); - self.vdp = Vdp::new(self.vdp_version, self.vdp.get_remove_sprite_limit()); + self.vdp = Vdp::new(self.vdp_version, &self.config); self.psg = Psg::new(self.psg.version()); self.input = InputState::new(self.input.region()); @@ -419,7 +411,7 @@ fn populate_frame_buffer( crop_left_border: bool, frame_buffer: &mut [Color], ) { - let viewport = vdp_version.viewport_size(); + let viewport = vdp_buffer.viewport(); let (row_skip, row_take) = if crop_vertical_border { (viewport.top_border_height as usize, viewport.height_without_border() as usize) diff --git a/backend/smsgg-core/src/vdp.rs b/backend/smsgg-core/src/vdp.rs index 0e6f146d..ad686e79 100644 --- a/backend/smsgg-core/src/vdp.rs +++ b/backend/smsgg-core/src/vdp.rs @@ -7,6 +7,7 @@ mod debug; mod tms9918; +use crate::SmsGgEmulatorConfig; use bincode::de::{BorrowDecoder, Decoder}; use bincode::enc::Encoder; use bincode::error::{DecodeError, EncodeError}; @@ -28,7 +29,7 @@ pub struct ViewportSize { } impl ViewportSize { - const NTSC_SMS2: Self = Self { + const NTSC_SMS: Self = Self { width: 256, height: 224, top: 0, @@ -38,7 +39,7 @@ impl ViewportSize { left_border_width: 8, }; - const PAL_SMS2: Self = Self { + const PAL_SMS: Self = Self { width: 256, height: 240, top: 0, @@ -58,6 +59,16 @@ impl ViewportSize { left_border_width: 0, }; + const GAME_GEAR_EXPANDED: Self = Self { + width: 256, + height: 192, + top: 0, + left: 0, + top_border_height: 0, + bottom_border_height: 0, + left_border_width: 8, + }; + pub fn height_without_border(self) -> u16 { self.height - self.top_border_height - self.bottom_border_height } @@ -113,11 +124,17 @@ impl VdpVersion { } #[must_use] - pub const fn viewport_size(self) -> ViewportSize { + const fn viewport_size(self, gg_use_sms_resolution: bool) -> ViewportSize { match self { - Self::NtscMasterSystem1 | Self::NtscMasterSystem2 => ViewportSize::NTSC_SMS2, - Self::PalMasterSystem1 | Self::PalMasterSystem2 => ViewportSize::PAL_SMS2, - Self::GameGear => ViewportSize::GAME_GEAR, + Self::NtscMasterSystem1 | Self::NtscMasterSystem2 => ViewportSize::NTSC_SMS, + Self::PalMasterSystem1 | Self::PalMasterSystem2 => ViewportSize::PAL_SMS, + Self::GameGear => { + if gg_use_sms_resolution { + ViewportSize::GAME_GEAR_EXPANDED + } else { + ViewportSize::GAME_GEAR + } + } } } } @@ -599,8 +616,11 @@ pub struct VdpBuffer { } impl VdpBuffer { - fn new(version: VdpVersion) -> Self { - Self { buffer: vec![0; FRAME_BUFFER_LEN], viewport: version.viewport_size() } + fn new(version: VdpVersion, gg_use_sms_resolution: bool) -> Self { + Self { + buffer: vec![0; FRAME_BUFFER_LEN], + viewport: version.viewport_size(gg_use_sms_resolution), + } } #[inline] @@ -623,6 +643,10 @@ impl VdpBuffer { pub fn iter(&self) -> FrameBufferRowIter<'_> { FrameBufferRowIter { buffer: self, row: 0 } } + + pub fn viewport(&self) -> ViewportSize { + self.viewport + } } impl Encode for VdpBuffer { @@ -703,28 +727,20 @@ pub enum VdpTickEffect { } impl Vdp { - pub fn new(version: VdpVersion, remove_sprite_limit: bool) -> Self { + pub fn new(version: VdpVersion, config: &SmsGgEmulatorConfig) -> Self { Self { - frame_buffer: VdpBuffer::new(version), + frame_buffer: VdpBuffer::new(version, config.gg_use_sms_resolution), registers: Registers::new(version), vram: [0; VRAM_SIZE], color_ram: [0; COLOR_RAM_SIZE], scanline: 0, dot: 0, sprite_buffer: SpriteBuffer::new(), - remove_sprite_limit, + remove_sprite_limit: config.remove_sprite_limit, line_counter: 0xFF, } } - pub fn get_remove_sprite_limit(&self) -> bool { - self.remove_sprite_limit - } - - pub fn set_remove_sprite_limit(&mut self, remove_sprite_limit: bool) { - self.remove_sprite_limit = remove_sprite_limit; - } - fn read_color_ram_word(&self, address: u8) -> u16 { if self.registers.version.is_master_system() { self.color_ram[address as usize].into() @@ -1025,6 +1041,10 @@ impl Vdp { &self.frame_buffer } + pub fn viewport(&self) -> ViewportSize { + self.frame_buffer.viewport + } + pub fn read_control(&mut self) -> u8 { self.registers.read_control() } @@ -1090,9 +1110,10 @@ impl Vdp { self.registers.version.timing_mode() } - pub fn set_version(&mut self, version: VdpVersion) { + pub fn update_config(&mut self, version: VdpVersion, config: &SmsGgEmulatorConfig) { self.registers.version = version; - self.frame_buffer.viewport = version.viewport_size(); + self.frame_buffer.viewport = version.viewport_size(config.gg_use_sms_resolution); + self.remove_sprite_limit = config.remove_sprite_limit; } } diff --git a/frontend/jgenesis-cli/src/main.rs b/frontend/jgenesis-cli/src/main.rs index 0db68a6e..d5b7f5b8 100644 --- a/frontend/jgenesis-cli/src/main.rs +++ b/frontend/jgenesis-cli/src/main.rs @@ -121,6 +121,10 @@ struct Args { #[arg(long, help_heading = SMSGG_OPTIONS_HEADING)] sms_crop_left_border: Option, + /// For Game Gear, render at SMS resolution (256x192) instead of native resolution (160x144) + #[arg(long, help_heading = SMSGG_OPTIONS_HEADING)] + gg_use_sms_resolution: Option, + /// Enable SMS FM sound unit #[arg(long, help_heading = SMSGG_OPTIONS_HEADING)] sms_fm_unit_enabled: Option, @@ -464,6 +468,7 @@ impl Args { sms_region, sms_crop_vertical_border, sms_crop_left_border, + gg_use_sms_resolution, sms_fm_unit_enabled -> fm_sound_unit_enabled, smsgg_overclock_z80 -> overclock_z80, ]); diff --git a/frontend/jgenesis-gui/src/app/smsgg.rs b/frontend/jgenesis-gui/src/app/smsgg.rs index 1b4b6896..ab2c160f 100644 --- a/frontend/jgenesis-gui/src/app/smsgg.rs +++ b/frontend/jgenesis-gui/src/app/smsgg.rs @@ -177,6 +177,16 @@ impl App { self.state.help_text.insert(WINDOW, helptext::SMS_CROP_LEFT_BORDER); } + let rect = ui + .checkbox( + &mut self.config.smsgg.gg_use_sms_resolution, + "(Game Gear) Display in SMS resolution", + ) + .interact_rect; + if ui.rect_contains_pointer(rect) { + self.state.help_text.insert(WINDOW, helptext::GG_USE_SMS_RESOLUTION); + } + self.render_help_text(ui, WINDOW); }); if !open { diff --git a/frontend/jgenesis-gui/src/app/smsgg/helptext.rs b/frontend/jgenesis-gui/src/app/smsgg/helptext.rs index b44053b7..d1000ce7 100644 --- a/frontend/jgenesis-gui/src/app/smsgg/helptext.rs +++ b/frontend/jgenesis-gui/src/app/smsgg/helptext.rs @@ -73,6 +73,14 @@ pub const SMS_CROP_LEFT_BORDER: HelpText = HelpText { ], }; +pub const GG_USE_SMS_RESOLUTION: HelpText = HelpText { + heading: "Game Gear Expanded Resolution", + text: &[ + "If enabled, display the full 256x192 frame rendered by the VDP rather than only the center 160x144 pixels.", + "Only the center pixels display on actual hardware, so the expanded parts of the frame may contain garbage.", + ], +}; + pub const PSG_VERSION: HelpText = HelpText { heading: "PSG Version", text: &[ diff --git a/frontend/jgenesis-native-config/src/smsgg.rs b/frontend/jgenesis-native-config/src/smsgg.rs index f71c075a..5042c728 100644 --- a/frontend/jgenesis-native-config/src/smsgg.rs +++ b/frontend/jgenesis-native-config/src/smsgg.rs @@ -24,6 +24,8 @@ pub struct SmsGgAppConfig { pub sms_crop_vertical_border: bool, #[serde(default)] pub sms_crop_left_border: bool, + #[serde(default)] + pub gg_use_sms_resolution: bool, #[serde(default = "true_fn")] pub fm_sound_unit_enabled: bool, #[serde(default)] @@ -58,6 +60,7 @@ impl AppConfig { sms_region: self.smsgg.sms_region, sms_crop_vertical_border: self.smsgg.sms_crop_vertical_border, sms_crop_left_border: self.smsgg.sms_crop_left_border, + gg_use_sms_resolution: self.smsgg.gg_use_sms_resolution, fm_sound_unit_enabled: self.smsgg.fm_sound_unit_enabled, overclock_z80: self.smsgg.overclock_z80, }) diff --git a/frontend/jgenesis-native-driver/src/config.rs b/frontend/jgenesis-native-driver/src/config.rs index 3bf2ebc0..6304aa2d 100644 --- a/frontend/jgenesis-native-driver/src/config.rs +++ b/frontend/jgenesis-native-driver/src/config.rs @@ -179,6 +179,7 @@ pub struct SmsGgConfig { pub sms_region: SmsRegion, pub sms_crop_vertical_border: bool, pub sms_crop_left_border: bool, + pub gg_use_sms_resolution: bool, pub fm_sound_unit_enabled: bool, pub overclock_z80: bool, } @@ -199,6 +200,7 @@ impl SmsGgConfig { sms_region: self.sms_region, sms_crop_vertical_border: self.sms_crop_vertical_border, sms_crop_left_border: self.sms_crop_left_border, + gg_use_sms_resolution: self.gg_use_sms_resolution, fm_sound_unit_enabled: self.fm_sound_unit_enabled, overclock_z80: self.overclock_z80, } diff --git a/frontend/jgenesis-web/src/config.rs b/frontend/jgenesis-web/src/config.rs index f5530bac..4ca6cf3e 100644 --- a/frontend/jgenesis-web/src/config.rs +++ b/frontend/jgenesis-web/src/config.rs @@ -127,6 +127,7 @@ impl SmsGgWebConfig { remove_sprite_limit: self.remove_sprite_limit, sms_crop_left_border: self.sms_crop_left_border, sms_crop_vertical_border: self.sms_crop_vertical_border, + gg_use_sms_resolution: false, fm_sound_unit_enabled: self.fm_unit_enabled, overclock_z80: false, }