release: v0.3.0 - #22
Merged
Merged
Conversation
- Add game library browser showing ROM title, company, and filename - Implement asynchronous directory loading with progress indication - Add configurable keybinds with persistence via serde - Add display scaling modes (integer scaling and stretch) - Add color palette options (greyscale, classic green, pocket) - Expose cartridge module and add company name lookup from licensee codes - Add FPS counter and improve title extraction
- Add APP_NAME constant combining name and version from Cargo.toml - Replace hardcoded "SturdyGB" strings with APP_NAME constant - Simplify title formatting by reusing constant - Remove unused SCALE constant
- Add detailed build instructions for desktop and WebAssembly targets - Add search and sort functionality to game library browser - Update dependencies (rfd 0.17.2, cpal 0.17) - Replace std::time::Instant with instant crate for WASM compatibility - Add web-specific UI for ROM loading on WASM target - Fix copyright year in release workflow (2024) - Enable serial message printing for debugging - Reorganize README sections and improve
- Add halt_bug flag to CPU state to track HALT bug condition - Implement HALT bug when HALT executed with IME=0 and pending interrupts - Fix PC increment behavior during HALT bug (decrement by 1) - Simplify interrupt handling by checking pending interrupts consistently - Remove redundant is_halted check when pushing PC to stack - Mask interrupt flags with 0x1F to check only valid interrupt bits
- Add optional save_path parameter to load_cartridge_from_bytes and build_from_bytes - Generate .sav file path when loading ROMs from filesystem - Pass None for save_path when loading ROMs asynchronously (web/memory) - Extract save path generation to avoid duplication in load_rom_file
- Fix RAM size 0x04 from 0x200000 to 0x20000 (128KB) - Clear serial data buffer after reading to prevent duplicates - Optimize game library loading by reading only header bytes instead of full ROM - Add separate handling for ZIP files when reading ROM headers - Cap leftover audio buffer at 8192 samples to prevent unbounded growth - Configure WGPU to prefer low power and use GL/Metal backends - Remove unnecessary to_owned() call in serial
- This avoid inconsistencies when SturdyGB hits other versions
- Replace GitHub-hosted screenshots with local images in ./images directory - Increase screenshot display size from 300px to 400px - Fix table layout by adding missing closing td tag - Add winresource build dependency for Windows executable icon - Update SVG export settings to 512x512 resolution
- Add pause/resume and reset options to Emulation menu - Store ROM bytes and save path in State for reset functionality - Skip Windows resource compilation when targeting wasm32 - Fix paused state management when stopping or loading ROMs - Only process input and run emulation loop when not paused - Improve WASM welcome screen with app name and instructions - Clone ROM bytes and save path when loading to enable reset
- Add fullscreen toggle via F11 key and View menu - Store fullscreen state in SturdyConfig with persistence - Add emoji icons to menu items for better visual clarity - Support ZIP files in ROM file picker dialogs - Move Stop button from File menu to Emulation menu - Improve game library table column sizing with auto-sizing - Update file filter to include .zip extension for both native and WASM
- Add cfg(not(target_arch = "wasm32")) guards to fullscreen field and logic - Fullscreen API not supported in WASM, causing compilation issues - Add folder emoji icons to ROM picker buttons on WASM welcome screen - Fix WASM file filter to include .zip instead of .gbc extension
Reviewer's GuideReplaces the Notan-based frontend with a new egui/eframe UI (including ROM browser, scaling, palettes, configurable keybindings, and WASM support), adds cartridge/ROM loading from bytes and ZIP archives, fixes HALT/interrupt behavior and other core emulation details, and introduces multi-platform packaging for desktop (macOS, Linux, Windows) while updating documentation and versions for the v0.3.0 release. Sequence diagram for ROM loading and initialization (file/ZIP to running GB)sequenceDiagram
actor User
participant EmuApp
participant FileDialog
participant Fs as Filesystem
participant GbInstance
participant CartridgeModule
participant CartridgeHeader
participant Audio as AudioGlobals
User->>EmuApp: Click Open_ROM
EmuApp->>FileDialog: show_open_dialog(extensions: [gb,zip])
FileDialog-->>User: Choose_file_path
User-->>FileDialog: Confirm
FileDialog-->>EmuApp: Path
EmuApp->>Fs: read(path)
Fs-->>EmuApp: Vec_u8_bytes
alt Bytes_is_ZIP
EmuApp->>EmuApp: extract_rom_from_bytes(bytes)
EmuApp-->>EmuApp: rom_bytes
else Raw_ROM
EmuApp->>EmuApp: bytes_used_as_rom
end
EmuApp->>GbInstance: build_from_bytes(rom_bytes, Some(save_path))
GbInstance->>CartridgeModule: load_cartridge_from_bytes(rom_bytes, Some(save_path))
CartridgeModule->>CartridgeHeader: new(rom_bytes)
CartridgeHeader-->>CartridgeModule: CartridgeHeader
CartridgeModule-->>GbInstance: (Box_Mbc, GbMode)
GbInstance->>GbInstance: Determine_GbTypes_from_GbMode
GbInstance-->>EmuApp: Gb
EmuApp->>Audio: setup_audio(&mut Gb)
Audio->>Audio: create_cpal_stream_and_channel()
Audio-->>EmuApp: AUDIO_PRODUCER_initialized
EmuApp->>EmuApp: Create_State(gb, rgba_buffer, leftover_audio)
EmuApp-->>User: ROM_running_with_video_audio
Sequence diagram for HALT and interrupt handling bug fixsequenceDiagram
participant Gb
participant Cpu
Note over Gb,Cpu: Each CPU tick
Gb->>Gb: handle_interrupt()
Gb->>Cpu: check interrupt_master
alt Interrupts_disabled
Gb-->>Cpu: Return_no_interrupt
else Interrupts_enabled
Gb->>Gb: pending = ie_flag & if_flag & 0x1F
alt pending == 0
Gb-->>Cpu: Return_no_interrupt
else pending != 0
alt Cpu.is_halted
Gb->>Cpu: is_halted = false
end
Gb->>Cpu: interrupt_master = false
Gb->>Cpu: sp = sp - 2
Gb->>Gb: write_word(sp, pc)
Gb->>Gb: interrupt_source = get_interrupt_source(pending)
Gb->>Gb: pc = go_interrupt(interrupt_source)
Gb->>Gb: if_flag &= !interrupt_source
Cpu->>Cpu: pending_cycles += 5
end
end
Note over Cpu,Gb: Executing HALT instruction
Gb->>Gb: halt()
Gb->>Cpu: advance_pc()
Gb->>Gb: pending = ie_flag & if_flag & 0x1F
alt !interrupt_master && pending != 0
Gb->>Cpu: halt_bug = true
else
Gb->>Cpu: is_halted = true
end
Note over Cpu: advance_pc with halt_bug
Cpu->>Cpu: advance_pc()
Cpu->>Cpu: adv = OPCODES_SIZE[current_instruction]
alt halt_bug == true
Cpu->>Cpu: halt_bug = false
Cpu->>Cpu: adv = adv - 1 (saturating)
end
Cpu->>Cpu: pc = pc + adv
ER diagram for updated cartridge metadata (title and company)erDiagram
CARTRIDGE_HEADER {
string title
string company
integer rom_size
integer ram_size
string mbc_type
boolean sgb_flag
integer cgb_flag
}
CARTRIDGE_FILE {
string file_path
string filename
string extension
}
COMPANY_CODE {
string old_code
string new_code
string company_name
}
CARTRIDGE_FILE ||--|| CARTRIDGE_HEADER : "contains_header"
COMPANY_CODE ||--o{ CARTRIDGE_HEADER : "decoded_to_company"
%% Mapping functions (conceptual relationships)
FUNCTION_load_cartridge_from_bytes {
string rom_bytes
string save_path
}
FUNCTION_get_company_name {
string old_code
string new_code
}
FUNCTION_load_cartridge_from_bytes ||--|| CARTRIDGE_HEADER : "parses_header"
FUNCTION_get_company_name ||--|| COMPANY_CODE : "returns_name"
Class diagram for updated core emulator types (v0.3.0)classDiagram
class Gb {
+u8 ie_flag
+u8 if_flag
+Cpu cpu
+void run_one_frame()
+void handle_interrupt()
+void cpu_tick()
+void components_tick()
+void print_serial_message()
+void set_sample_rate(u32 sample_rate)
+[[f32;2]] get_audio_buffer()
+[[u8;160];144] get_screen_data()
+void write_word(u16 addr, u16 value)
}
class Cpu {
+u16 pc
+u16 sp
+bool interrupt_master
+bool is_halted
+bool halt_bug
+u8 current_instruction
+i32 pending_cycles
+void advance_pc()
}
class CartridgeHeader {
+[u8;4] entry
+[u8;48] logo
+String title
+u8 cgb_flag
+bool sgb_flag
+MBCTypes mbc_type
+u32 rom_size
+u32 ram_size
+String company
+new(rom_data: [u8]) Result~CartridgeHeader,&'static str~
}
class GbInstance {
+static build(path: &str) Result~Gb,String~
+static build_from_bytes(rom_data: Vec~u8~, save_path: Option~PathBuf~) Result~Gb,String~
}
class Serial {
+Vec~u8~ serial_data
+Option~String~ get_serial_message()
}
class JoypadButton {
<<enum>>
+A
+B
+Start
+Select
+Up
+Down
+Left
+Right
}
class Mbc {
<<trait>>
+u8 read_rom(u16 address)
+void write_rom(u16 address, u8 value)
+u8 read_ram(u16 address)
+void write_ram(u16 address, u8 value)
+void save_ram()
}
class GbMode {
<<enum>>
+DmgMode
+CgbMode
}
class GbTypes {
<<enum>>
+Dmg
+Cgb
}
class CartridgeModule {
+load_cartridge(filename: &str) Result~Box~dyn Mbc~,GbMode,String~
+load_cartridge_from_bytes(rom_data: Vec~u8~, save_path: Option~PathBuf~) Result~Box~dyn Mbc~,GbMode,String~
+get_company_name(old_code: u8, new_code: &[u8]) String
}
Gb o-- Cpu
GbInstance ..> Gb
GbInstance ..> CartridgeModule
CartridgeModule ..> CartridgeHeader
CartridgeModule ..> Mbc
CartridgeHeader --> MBCTypes
Gb --> GbMode
Gb --> GbTypes
Serial --> Gb
JoypadButton <.. serde_Serialize
JoypadButton <.. serde_Deserialize
class serde_Serialize {
<<trait>>
}
class serde_Deserialize {
<<trait>>
}
Class diagram for new egui/eframe frontend (EmuApp)classDiagram
class EmuApp {
+Option~State~ state
+Option~TextureHandle~ texture
+Option~String~ error_msg
+(Sender~Result~Vec~u8~,String~~, Receiver~Result~Vec~u8~,String~~) rom_load_channel
+bool paused
+SturdyConfig config
+bool show_options
+instant::Instant start_time
+usize frames_rendered
+instant::Instant last_fps_update
+usize current_fps
+new(cc: &CreationContext, initial_rom: Option~String~) EmuApp
+void load_rom_file(path: &str)
+void load_rom_bytes(bytes: Vec~u8~, save_path: Option~PathBuf~)
+void update(ctx: &egui::Context, frame: &mut eframe::Frame)
+void save(storage: &mut dyn eframe::Storage)
}
class State {
+Gb gb
+Vec~u8~ rgba
+Vec~[f32;2]~ leftover_audio
+String title
+Vec~u8~ rom_bytes
+Option~PathBuf~ save_path
}
class SturdyConfig {
+ScaleMode scale
+Palette palette
+Vec~PathBuf~ rom_directories
+HashMap~JoypadButton,egui::Key~ keybinds
+bool fullscreen
}
class ScaleMode {
<<enum>>
+Integer(f32)
+Stretch
}
class Palette {
<<enum>>
+Greyscale
+ClassicGreen
+Pocket
}
class GameEntry {
+PathBuf path
+String filename
+String title
+String company
}
class SortMethod {
<<enum>>
+Filename
+Title
+Company
}
class AudioGlobals {
+static Option~SyncSender~[f32;2]~~ AUDIO_PRODUCER
+static Option~cpal::Stream~ AUDIO_STREAM
+setup_audio(gb: &mut Gb)
}
class FrontendLib {
+APP_NAME: &str
+extract_rom_from_bytes(bytes: &[u8]) Option~Vec~u8~~
+set_btn(ctx: &egui::Context, state: &mut State, key: egui::Key, btn: JoypadButton)
}
EmuApp o-- State
EmuApp --> SturdyConfig
EmuApp --> GameEntry
EmuApp --> SortMethod
EmuApp --> AudioGlobals
EmuApp --> FrontendLib
State --> Gb
State --> JoypadButton
SturdyConfig --> ScaleMode
SturdyConfig --> Palette
SturdyConfig --> JoypadButton
GameEntry --> CartridgeHeader
AudioGlobals ..> cpal_Host
AudioGlobals ..> cpal_Device
AudioGlobals ..> cpal_Stream
class cpal_Host {
<<external>>
}
class cpal_Device {
<<external>>
}
class cpal_Stream {
<<external>>
}
class TextureHandle {
<<external>>
}
class Gb {
}
class JoypadButton {
}
class CartridgeHeader {
}
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- In
EmuApp::updateand related input handling you unconditionallyunwrap()keybind lookups fromconfig.keybinds, which can panic if the stored config is missing entries (e.g., after format changes); consider providing defaults or handling missing keys more defensively. - The async directory loader in
updateusesrx.try_recv()both in thewhile let Ok(entry)loop and again afterwards to detectDisconnected, which can consume and drop an extra entry or mis-detect completion; instead track completion via the loop result or userecv()/recv_timeout()once to determine when the sender is closed. - The global
AUDIO_PRODUCER/AUDIO_STREAMstatics usestatic mutwithout any synchronization besides the implicit main-thread usage assumption; if you expect multi-threaded use or hot-reload scenarios, consider replacing them withOnceLock<Mutex<...>>or a similar safe abstraction to avoid potential data races.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `EmuApp::update` and related input handling you unconditionally `unwrap()` keybind lookups from `config.keybinds`, which can panic if the stored config is missing entries (e.g., after format changes); consider providing defaults or handling missing keys more defensively.
- The async directory loader in `update` uses `rx.try_recv()` both in the `while let Ok(entry)` loop and again afterwards to detect `Disconnected`, which can consume and drop an extra entry or mis-detect completion; instead track completion via the loop result or use `recv()`/`recv_timeout()` once to determine when the sender is closed.
- The global `AUDIO_PRODUCER`/`AUDIO_STREAM` statics use `static mut` without any synchronization besides the implicit main-thread usage assumption; if you expect multi-threaded use or hot-reload scenarios, consider replacing them with `OnceLock<Mutex<...>>` or a similar safe abstraction to avoid potential data races.
## Individual Comments
### Comment 1
<location path="crates/frontend/src/app.rs" line_range="704-713" />
<code_context>
+ let k = &self.config.keybinds;
</code_context>
<issue_to_address>
**issue (bug_risk):** Unwrapping keybind lookups can panic when loading configs that don’t define all bindings.
`SturdyConfig` keybinds are accessed with `k.get(&JoypadButton::X).unwrap()`, so any missing mapping (e.g., from older, edited, or corrupted configs) will panic at load time. Instead of unwrapping, fall back to a default mapping for missing entries so that legacy or malformed configs don’t crash the app.
</issue_to_address>
### Comment 2
<location path="crates/frontend/src/app.rs" line_range="411-412" />
<code_context>
+ if ui.button("📁 Open ROM...").clicked() {
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ if let Some(path) = FileDialog::new()
+ .add_filter("GameBoy ROMs", &["gb", "zip"])
+ .pick_file()
+ {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The desktop file picker omits `.gbc` from the ROM filter, unlike other code paths.
Here you only allow `"gb"` and `"zip"`, but elsewhere you treat both `.gb` and `.gbc` as valid ROMs. To keep behavior consistent and let users open `.gbc` files directly, please add `"gbc"` to this filter (and any related `FileDialog` / `AsyncFileDialog` filters you want aligned).
Suggested implementation:
```rust
if let Some(path) = FileDialog::new()
.add_filter("GameBoy ROMs", &["gb", "gbc", "zip"])
.pick_file()
{
```
There may be other `FileDialog` or `AsyncFileDialog` usages in this file or elsewhere that also define ROM filters. For consistency, update those filters in the same way (add `"gbc"` alongside `"gb"` and `"zip"`) so that all file-picking code paths accept `.gbc` ROMs directly.
</issue_to_address>
### Comment 3
<location path="README.md" line_range="177" />
<code_context>
+5. **Serve the application:**
+ You will need a local web server to serve the files in the `crates/frontend/public` directory. For example, using Python:
+ ```bash
+ cd public
+ python -m http.server 8080
+ ```
</code_context>
<issue_to_address>
**issue:** Clarify the path here to match `crates/frontend/public` mentioned above.
Since earlier steps already run `cd crates/frontend`, it would be clearer and less error‑prone to either use `cd crates/frontend/public` here or explicitly state what the current working directory should be, so readers don’t accidentally serve the wrong path.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
- Replace unsafe static mut with Mutex-wrapped statics for audio components - Add SturdyConfig::default_key() and keybind() helper methods - Simplify input handling loop using button array iteration - Fix directory loading channel disconnection detection - Add .gbc extension to all file picker filters - Fix README web server path instructions to be relative to crates/frontend - Remove unsafe blocks and static_mut_refs lint allowances
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add
eguito replacenotan.Add a bunch of new improvements to the emulator:
Summary by Sourcery
Release version 0.3.0 with a new egui-based frontend, expanded platform targets, and core emulation improvements.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation: