{"id":50528918,"url":"https://github.com/pakitovic/gb-emu","last_synced_at":"2026-06-03T11:00:55.849Z","repository":{"id":339002750,"uuid":"1160020039","full_name":"pakitovic/gb-emu","owner":"pakitovic","description":"A basic Game Boy emulator written in Rust.","archived":false,"fork":false,"pushed_at":"2026-03-28T21:40:24.000Z","size":814,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-29T00:18:49.834Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pakitovic.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-02-17T12:56:08.000Z","updated_at":"2026-03-28T21:40:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/pakitovic/gb-emu","commit_stats":null,"previous_names":["pakitovic/gb-emu"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/pakitovic/gb-emu","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pakitovic%2Fgb-emu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pakitovic%2Fgb-emu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pakitovic%2Fgb-emu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pakitovic%2Fgb-emu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pakitovic","download_url":"https://codeload.github.com/pakitovic/gb-emu/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pakitovic%2Fgb-emu/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33860971,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-03T11:00:54.827Z","updated_at":"2026-06-03T11:00:55.829Z","avatar_url":"https://github.com/pakitovic.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# gb-emu\n\nPersonal/hobby Game Boy emulator project written in Rust, focused on learning and incremental milestones.\n\n## Current Scope\n\n### Core Emulation (CPU / Bus / Timing / Input)\n- CPU core with growing opcode coverage.\n- Memory bus + timer/interrupt basics.\n- Core timing contract now exposes an explicit DMG clock-ratio policy layer (CPU m-cycles -\u003e base t-cycles) used at the CPU/bus boundary, keeping current DMG behavior unchanged while reducing future CGB double-speed refactor scope.\n- CPU timing plumbing now derives control/jump/ALU/load/CB instruction return timings from the clock-ratio policy (`mcycles -\u003e tcycles`), and non-instruction CPU timing paths (HALT idle step + interrupt service dispatch) also use explicit policy-derived timing returns, while keeping behavior unchanged in current DMG scope.\n- Bus/memory access now routes VRAM/WRAM/OAM through internal segment helpers (CPU-visible vs hardware-internal access modes) with centralized VRAM/OAM blocking rules, reducing future CGB banking (`VBK`/`SVBK`) refactor scope.\n- MMIO decode now includes DMG-noop scaffolding for CGB-only registers (`KEY1`, `VBK`, `SVBK`, `BGPI/BGPD`, `OBPI/OBPD`) with internal shadowed fields/state (including palette index/data shadow capture), and VRAM/WRAM bus helpers now resolve through DMG-fixed bank-selection scaffolding hooks backed by real multi-bank storage (CGB-sized VRAM/WRAM backing kept DMG-fixed by effective bank policy) so future CGB banking wiring can stay localized to the bus/MMIO layer.\n- FFxx IO routing now uses a centralized declarative register-route decoder (including explicit reserved/unmapped DMG-family ranges and CGB scaffold register precedence) so adding future CGB MMIO cases stays localized to the IO router table instead of spreading FFxx side-effects across the bus.\n- DMA is now modeled as a scheduler-style state machine with incremental `tick(tcycles)` advancement, formal mode/edge state, centralized DMA CPU access-block policy hooks (currently only DMG OAM DMA behavior is active), and DMG-noop scaffolding for future CGB DMA control registers (`HDMA1..HDMA5`) plus model-gated `GDMA/HDMA` scheduler paths (transfer-state/request wiring and HBlank-edge hook integration, inactive for current DMG-family models) to reduce future HDMA/GDMA integration refactor scope.\n- Joypad input API in core with P1 register behavior and joypad interrupt edges.\n- Core now exposes a bounded key-MMIO write event stream (`FF00`/`FF40` with emulated t-cycle stamps) plus a portable SGB scaffold split into `sgb::SgbLink` (JOYP packet decode), `sgb::SgbState` (palette/mask/attribute-map state with `PALxx`, `PAL_TRN`, `PAL_SET`, `ATRC_EN`, `TEST_EN`, `ICON_EN`, `DATA_SND`, `DATA_TRN`, `JUMP`, `PAL_PRI`, `MASK_EN`, `ATTR_BLK`, `ATTR_LIN`, `ATTR_DIV`, `ATTR_CHR`, `ATTR_TRN`, `ATTR_SET`, `MLT_REQ`, `CHR_TRN`, `PCT_TRN`, `OBJ_TRN` command state), `sgb::SgbColorizer` (DMG framebuffer + SGB state -\u003e RGB output), and `sgb::SgbBorderRenderer` (SGB border/object composition) without coupling SGB behavior into DMG CPU/PPU execution paths.\n- SGB JOYP packet decoding now uses the documented pulse polarity from Pan Docs (`P14 low` = bit `0`, `P15 low` = bit `1`) and also treats the boot-ROM header packets (`F1/F3/F5/F7/F9/FB`) with their documented single-packet framing instead of the generic low-bits packet count. That fixes cart-driven SGB command detection on commercial ROMs that previously stayed silent or lost stream sync after the boot ROM header exchange on `sgb`/`sgb2`.\n- `GameBoy::run_frame_with_limit` now also returns after one DMG frame-budget worth of emulated cycles while LCD is disabled, preventing host frame-step timeouts during LCD-off phases (for example early SGB setup loops).\n- SGB mask handling now supports freeze/black/color0 viewport masking in the colorizer, including automatic viewport freeze while the GB LCD is disabled and resume when it is re-enabled.\n- `GameBoy`/`Bus` now expose a hardware-path VRAM block copy helper used by runtime SGB transfer handling, so SGB palette/attribute/border/object transfers do not depend on CPU-visible VRAM blocking.\n- SGB joypad handling now includes `MLT_REQ` multiplayer routing inside the core joypad device for `sgb`/`sgb2`, with `GameBoy`/`Bus` APIs to drive per-player button state while keeping the existing player-1 API stable.\n- Core API bootstrap for portable frontends (frame stepping + framebuffer access).\n- Optional boot ROM startup path in core (`PC=0x0000` + `0x0000..=0x00FF` boot-ROM mapping + `FF50` disable handling) with automatic fallback to existing post-boot defaults when no boot ROM is provided.\n- Public API now exposes a `gb_emu::bus` alias module (`Bus`, `LCD_WIDTH`, `LCD_HEIGHT`, `LCD_FRAME_PIXELS`) while keeping existing `gb_emu::memory::*` paths stable.\n\n### PPU / Video (DMG)\n- DMG background layer rendering to a grayscale framebuffer.\n- Video output now supports palette profiles over canonical DMG shade levels (`dmg` green, `mgb` gray), display-side mGBA-style GB/CGB manual presets (`grayscale`, `gb-pocket`, `gb-light`, `cgb-*`), curated per-game `cgb` header-CRC palette overrides derived from mGBA `overrides.c`, runtime/manual SGB palette paths (`sgb`, `sgb-1a..sgb-4h`), and optional external mGBA-style override INI files keyed by `gb.override.\u003cHEADERCRC32\u003e` with `pal[0]..pal[11]` RGB888 entries. Those external overrides now feed both the display-side `cgb` triplet path and the SGB BIOS-style boot palette HLE path (`pal[0]..pal[3]`) for monochrome carts on real `sgb`/`sgb2` sessions before any cart-driven SGB commands take over. The DMG core also now exports per-pixel DMG palette-selector metadata (`BG`, `OBJ0`, `OBJ1`) so display-side colorizers can apply BG/OBJ palette triplets without coupling those policies into the PPU core.\n- DMG window + sprite (OBJ) composition with priority/palette/flip handling.\n- Mode 3 background/window pixel FIFO stepping per dot with a 6-dot BG fetch cadence and window trigger/restart timing (WX/WY mid-line writes affect only valid trigger windows).\n- Mode 3 OBJ fetch stalls and sprite pixel mixing are stepped per dot with DMG priority/palette rules; OBJ fetch start now waits for BG fetch boundaries for more stable dot arbitration.\n- Mode 3 OBJ tile/attribute resolution now happens at fetch time instead of being fully pre-latched at line start, and overlapping DMG OAM DMA can now substitute the active DMA word into later OBJ fetches on the same line, improving sprite output behavior under mid-line DMA overlap.\n- Mode 3 window trigger comparator now queues pending restarts until a valid BG takeover boundary when OBJ fetch ownership delays immediate window restart.\n- Mode 3 takeover arbitration now handles FIFO-stall boundaries and queued window-trigger release after active OBJ fetch windows, with regression coverage for VRAM/OAM blocking and STAT mode0 timing shifts under runtime contention.\n- Mode 3 fetcher bus-phase modeling now includes an explicit one-dot BG push-stall recovery sleep micro-op before push resumes after FIFO overfill (`\u003e8`), reducing premature OBJ/window handover on FIFO-stall dots and tightening associated STAT/VRAM/OAM timing corner cases.\n- Mode 3 BG `Push` micro-op modeling now tracks an explicit latched `RecoverySleep` substate after FIFO stall resolution (before the later push-ready boundary), improving fetcher bus-phase state-machine clarity and tightening regression coverage for the `stalled -\u003e recovery sleep -\u003e push-ready -\u003e TileIndex` sequence.\n- Mode 3 BG `Push` takeover-boundary classification now distinguishes normal vs post-recovery push-ready states, reserving the `push-ready` handover boundary to the explicit post-`RecoverySleep` path and avoiding accidental classification on the normal (non-stalled) first-tile push flow.\n- Mode 3 takeover arbitration now excludes the FIFO-recovery `Push` sleep dot as a valid BG/OBJ/window handover boundary (while still allowing the stalled `Push` boundary and later push-ready boundary), with regression coverage for window/OBJ timing and mode3 bus/STAT blocking on that edge.\n- Mode 3 `Push` substate corner coverage now explicitly checks the `stalled -\u003e recovery sleep -\u003e push-ready` sequence, including shared window/OBJ arbitration and delayed takeover behavior on the first valid post-sleep boundary.\n- On shared Mode 3 takeover boundaries, queued window restarts now defer to an immediately-eligible OBJ fetch start, with regression coverage for the resulting STAT/VRAM/OAM blocking behavior on that arbitration edge.\n- Mode 3 OBJ/window arbitration now uses the same OBJ fetch-start lookahead as the OBJ fetcher path (including `Push` boundary handling), reducing window/OBJ overlap corruption seen in commercial scenes (e.g. mid-line window restarts around active sprites).\n- Mode 3 pixel composition now carries intermediate DMG pixel metadata (`source`, `color_id`, priority flags, palette selector) and applies DMG grayscale mapping in a final color step, reducing future CGB BG/OBJ palette/priority integration refactor scope.\n- PPU now keeps an explicit formal mode state plus one-tick mode-entry edge events (`entered OAM/Transfer/HBlank/VBlank`) synchronized with STAT mode bits and used internally for mode-sensitive interrupt timing hooks (including VBlank entry), reducing future HBlank-DMA/HDMA integration refactor scope.\n- PPU Mode 3 metadata scaffolding now also defines CGB-oriented BG tile attrs and OBJ palette/bank metadata placeholders, with runtime wiring model-gated off for current DMG-family models (test/debug paths can still exercise the scaffold) while current DMG color output behavior remains unchanged.\n- Mode 3 window restarts now clear any remaining BG fine-scroll discard (`SCX \u0026 7`) so WX-aligned HUD/window lines stay fixed instead of inheriting BG scroll jitter (e.g. Kirby's Dream Land HUD).\n- Mode 3 line-start BG fine-scroll discard now advances the OBJ FIFO in lockstep with discarded BG pixels, fixing left-edge sprite column misalignment when `SCX` uses sub-tile offsets (e.g. Super Mario Land at the camera left boundary).\n- Mode 3 line duration now grows from runtime OBJ fetch contention (including mid-line OBJ enable/disable effects), reducing reliance on static per-line penalty estimates.\n- Additional PPU/DMA timing edge cases: mode0 STAT source enabled during mode3 triggers on HBlank entry, and DMA restart keeps prior transfer running through the full restart-delay window.\n\n### APU / Audio Emulation\n- APU core channel state-machine scaffolding: NR52 power control, NR50/NR51 mixer register gating, CH1/CH2/CH3/CH4 trigger/state progression, and DIV-driven frame sequencer stepping (length/sweep/envelope clocks).\n- APU output path now supports real-device analog calibration profiles (model defaults plus custom per-device overrides) with per-channel DAC shaping/bias, routing matrix gains, stereo mixer drive, post-analog soft-clip/headroom limiting, low-pass + DC-blocking high-pass filtering, and selectable linear/cubic (Catmull-Rom with linear edge fallback) t-cycle-to-PCM resampling.\n- APU frontend output now preserves stereo channel routing (NR50/NR51 left/right masks) end-to-end for SDL2 and WebAudio.\n- APU length-enable edge behavior now includes immediate length clocking on non-length frame-sequencer steps when enabling length mid-playback.\n- APU DMG quirk coverage now includes CH1 sweep overflow/negate-clear disable behavior, trigger+length-zero reload/decrement edges, envelope trigger reload offset on envelope-clock steps, documented/common envelope \"zombie mode\" writes (`NRx2` while active), CH3 wave sample-buffer retrigger semantics plus Wave RAM fetch-window access/retrigger corruption behavior, and CH4 `clock_shift \u003e= 14` no-clock behavior.\n\n### Cartridge / Save Persistence / Metadata\n- ROM-only/ROM+RAM plus expanded MBC1/MBC2/MBC3/MBC5 support.\n- Cartridge header ROM size decoding across standard size codes, mapper-specific RAM enable/banking behavior (including MBC5 rumble register semantics), and battery-backed persistence (`.sav`, plus `.rtc` for MBC3 timer cartridges) with atomic file replace writes.\n- Core cartridge APIs expose import/export of battery save RAM bytes and MBC3 RTC persistence bytes for host adapters/runtime integration.\n- Web demo host now persists battery-backed cartridge data in browser storage (SRAM + MBC3 RTC metadata) with dirty-flag autosave debounce and page-lifecycle flush hooks.\n- Cartridge header diagnostics for Nintendo logo/header checksum/global checksum, exposed as non-blocking warnings in cartridge metadata.\n- Cartridge metadata debug report consumed by CLI (`--cart-info`) and frontends (SDL2 `F1` cart-info panel, web debug panel).\n- Cartridge core now exposes an internal `capabilities()` interface (mapper/ram/battery/timer/rumble/battery-save, compatibility-RAM mode, and normalized CGB/SGB header flag support) plus a model-aware cartridge compatibility policy (`header flags + selected hardware model`) so future DMG/CGB/SGB mode gating can query cartridge support without threading ad-hoc header checks through the bus.\n\n### Audio Output Pipeline / Frontend Audio Integration\n- Shared `runtime/` host utilities for frontend frame pacing, realtime audio queueing, adaptive buffering, t-cycle-to-PCM mixer bridging (SDL2/Web), and file-backed cartridge persistence adapters.\n- Shared `gb_runtime::session::RuntimeSession` now centralizes frontend wiring of `GameBoy + FramePacer + AudioMixer` (used by SDL2 and wasm adapters) so core-clock/audio capture plumbing does not need to be reimplemented per frontend.\n- `RuntimeSession` now also wires optional SGB transport/state/colorization/composition (`SgbLink` + `SgbState` + `SgbColorizer` + `SgbBorderRenderer`) by consuming core key-MMIO event streams. Transfer commands (`ATTR_TRN`/`PAL_TRN`/`DATA_TRN`/`CHR_TRN`/`PCT_TRN`/`OBJ_TRN`) now follow the documented SGB transfer window semantics more closely: runtime waits for the next rendered GB frame, tracks the 5-frame transfer window, prefers an exact `0x8000..0x8FFF` snapshot when the frame is in the documented transfer-screen configuration, falls back to the decoded live DMG transfer signal for palette/attribute transfers when no exact transfer frame is detected, keeps the richest non-exact transfer candidate seen across that delayed window instead of overwriting it with later weaker frames, and only uses the raw-VRAM heuristic for the remaining transfer families. `PAL_SET` now also mirrors the documented shared color-0 behavior across all four effective GB palettes, `PCT_TRN` now updates the global SGB backdrop color so border palette color 0 and GB palette color 0 stay aligned like real hardware, and real SGB runtime rendering uses an SGB-specific 5-bit color expansion curve instead of linear RGB555 expansion to better match mature SGB emulators on commercial borders/palette screens. `OBJ_TRN` now follows the documented 24-entry SNES OAM tail layout (`0x8F90..`) with separate extension bits for 9-bit X and large-size flags, derives OBJ palettes from selected SGB system palettes instead of raw transfer bytes, supports a conservative `16x16` large-object composition path, and runtime refreshes the live OBJ overlay OAM from VRAM while the overlay is enabled. On real `sgb`/`sgb2` models, runtime now HLE-applies the BIOS built-in boot palette for monochrome carts before any cart-driven SGB commands arrive, preferring an exact mGBA-style header-CRC override table and then falling back to the SameBoy/bsnes known-title table (`ZELDA`, `SUPER MARIOLAND`, `MARIOLAND2`, `SUPERMARIOLAND3`, `KIRBY DREAM LAND`, `HOSHINOKA-BI`, `KIRBY'S PINBALL`, `YOSSY NO TAMAGO`, `MARIO \u0026 YOSHI`, `YOSSY NO COOKIE`, `YOSHI'S COOKIE`, `DR.MARIO`, `TETRIS`, `YAKUMAN`, `METROID2`, `KAERUNOTAMENI`, `GOLF`, `ALLEY WAY`, `BASEBALL`, `TENNIS`, `F1RACE`, `KID ICARUS`, `QIX`, `SOLARSTRIKER`, `X`, and `GBWARS`). Cart-driven SGB activation still ignores unsupported/boot-header packet IDs and remains gated by the cartridge SGB header.\n- The headless CLI frontend now also exposes `--sgb-report`, which runs a ROM for `--max-steps` and prints the decoded SGB command IDs/names observed through JOYP packet traffic, making it easier to profile which commercial ROMs actually exercise `OBJ_TRN`, transfer commands, mask behavior, and other SGB families.\n- Shared `gb_runtime::audio_queue::AudioQueueController` now centralizes adaptive queue-targeting and underrun-estimation policy used by SDL2 and the wasm/web queue refill path.\n- Shared runtime audio mixer bridge from emulated APU t-cycle samples to frontend PCM rates (SDL2/Web).\n- Realtime audio block API for fixed-size callback backends, with silence padding when emulated audio budget is short.\n- Queue-based frontends (SDL2/WebAudio queue feeder) now use the same runtime adaptive queue policy and enqueue only currently available emulated audio samples.\n- Runtime queue defaults prioritize stable playback under host jitter (`initial/min/max = 4096/2048/16384`, `refill_block = 512`) while adaptive queueing remains shared across SDL2 and wasm/web frontends.\n- SDL2 now uses runtime queue defaults directly (no frontend-specific queue constants), keeps a conservative host audio buffer request (`AudioSpecDesired.samples = 1024`), and the web demo uses an `8ms` queue-refill cadence.\n- Browser demo (`web/`) with AudioWorklet-based WebAudio hook using realtime mixer blocks.\n- Minimal browser demo audio telemetry plus adaptive queue targeting for underrun recovery and latency tuning.\n- SDL2 and wasm frontends now consume runtime-provided SGB presented RGB frames when the active video palette pipeline is `sgb` (`SgbRuntime`), including automatic promotion from the DMG viewport (`160x144`) to the composed SGB frame (`256x224`) when border data or `OBJ_TRN` overlay data is active, with fallback to existing DMG palette mapping when no SGB command stream is active. In `auto` palette mode, real `sgb`/`sgb2` sessions now start from the BIOS-style SGB boot palette immediately (including CRC/title-based built-in overrides for known monochrome carts) and then keep following cart-driven SGB commands when present; non-SGB models still remain on their DMG/MGB base palette. Manual palette selection also now exposes the 32 built-in SGB BIOS four-shade presets as `sgb-1a..sgb-4h`, the curated per-ROM `cgb` override mode, and mGBA-style GB/CGB display presets without forcing SGB border composition.\n- SDL2 and wasm/web keyboard input now expose per-player SGB input routing on top of the core `MLT_REQ` API: player 1 keeps the existing DMG mapping, while fixed host keyboard clusters can also drive players 2-4 without coupling multiplayer behavior back into the core.\n\n### Validation / CI\n- Blargg + Gekkio ROM test integration in local scripts and CI.\n- CI ROM tests run the Blargg suites in `dev` profile and use relaxed per-ROM timeouts for the `blargg-all` suite / `cpu_instrs` guard on shared runners to reduce false-negative CI timeouts from debug-performance variance while keeping dedicated micro-guards in place.\n- CPU unit regressions include explicit interrupt-control corner coverage (IME/EI/DI/RETI ordering, `EI-\u003eHALT` halt-bug sequencing, `HALT` wake/no-wake behavior when `IF`/`IE` change while halted, halt-bug latch behavior when pending interrupts are cleared/masked or the interrupt source changes before the next step, pending-interrupt preemption of `HALT`/`STOP`, current DMG-scope `STOP` characterization including delayed-service source changes and combined `IF/IE` priority re-evaluation after `EI-\u003eSTOP`, and interrupt-dispatch stack-push side effects when `IE`/`IF` are overwritten mid-dispatch), plus `GameBoy` integration regressions for CPU-visible MMIO contention (`OAM DMA` OAM block and `PPU Mode 3` VRAM block) and `Bus::tick` t-cycle chunking characterization regressions (DIV/TIMA, LY/STAT, OAM DMA progress) to complement Blargg/Gekkio suites.\n\n### Project Architecture / Workspace Layout\n- The repository root is now a virtual Cargo workspace (`default-members = [\"systems/gb\"]`) and no longer owns a Rust package directly.\n- Game Boy core/system package now lives in `systems/gb` (Cargo package name remains `gb-emu`).\n- Shared frontend/host runtime helpers now live in the `runtime` workspace package (Cargo package name `gb-runtime`).\n- Headless CLI frontend is extracted to `frontends/cli` (workspace package/path dependency on `systems/gb`) while preserving the CLI binary name `gb-emu`.\n- SDL2 desktop frontend is extracted to `frontends/sdl2` (workspace package/path dependency on `systems/gb`).\n- Rust/WASM frontend adapter is extracted to `frontends/wasm` (workspace package/path dependency on `systems/gb`), while `web/` remains the browser host assets/demo area.\n- Boundary rule: keep hardware semantics in `systems/gb`, host/runtime helpers in `runtime`, and host platform bindings/UI code in `frontends/*` / `web`.\n\n## Project Structure\n\n```text\nsystems/\n  gb/\n    Cargo.toml\n    src/\n      audio.rs\n      bus.rs\n      cartridge.rs\n      cartridge/\n      cpu.rs\n      cpu/\n      gameboy.rs\n      gameboy/\n      hardware.rs\n      input.rs\n      memory.rs\n      memory/\n      timing.rs\n      lib.rs\n    tests/\n      integration_smoke.rs\nruntime/\n  Cargo.toml\n  src/\n    audio.rs\n    audio_queue.rs\n    bootrom.rs\n    session.rs\n    timing.rs\n    lib.rs\n  tests/\n    integration_smoke.rs\nfrontends/\n  cli/\n    Cargo.toml\n    src/\n      main.rs\n    tests/\n      cli_cart_info.rs\n  sdl2/\n    Cargo.toml\n    src/\n      main.rs\n  wasm/\n    Cargo.toml\n    src/\n      lib.rs\nscripts/\n  dev/\n    bootstrap.sh\n    create_pr.sh\n    run_audio_guard.sh\n    run_sdl2_frontend.sh\n    run_web_demo.sh\n    setup-hooks.sh\n  blargg/\n    fetch_blargg_roms.sh\n    run_blargg.sh\n    rom.txt\n  gekkio/\n    fetch_gekkio_roms.sh\n    run_gekkio.sh\n    rom.txt\n    roms_boot_models.txt\nweb/\n  minimal/\n    audio-worklet.js\n    index.html\n    main.js\n```\n\n## Workspace Layout Guide\n\n- `systems/*`: hardware emulation packages and public system API surfaces.\n  Current `systems/gb` owns CPU/APU/PPU/timer/interrupts/MMIO/DMA, cartridge/mappers and persistence-byte semantics (battery save RAM / MBC3 RTC import-export APIs), framebuffer generation, and emulated audio sample stream generation.\n  It must not contain SDL2 backends, `wasm-bindgen` exports, browser DOM/JS integration, or libretro bindings.\n- `runtime/`: frontend-shared host/runtime helpers that are not hardware semantics.\n  Current `runtime` owns host-time frame pacing (`FramePacer`), a shared frontend runtime session (`RuntimeSession`: `GameBoy + FramePacer + AudioMixer` wiring), frontend audio queue/adaptive buffering helpers, frontend boot-ROM autoload helpers, the frontend-facing t-cycle-to-PCM mixer bridge, and file-backed cartridge persistence adapters (`.sav` / `.rtc`).\n- `frontends/*`: host adapters/UI entrypoints that depend on `systems/gb` and optionally `runtime`.\n  - `frontends/cli`: CLI argument parsing, headless modes (`blargg`, `mooneye`, `cart-info`), CLI error formatting/wiring.\n  - `frontends/sdl2`: SDL2 window/rendering, event loop, keyboard mapping, SDL2 audio queue/device integration.\n  - `frontends/wasm`: `wasm-bindgen` exports, `WebEmulator` browser adapter API, WASM-only glue code.\n- `web/`: browser host assets and demo pages (JavaScript/HTML/CSS/AudioWorklet/browser helper tests); no Rust package should live inside `web/`.\n\nFuture expansion rule:\n- `CGB` support should remain inside `systems/gb` unless a stronger separation is proven necessary.\n- A future Game Boy Advance implementation should be introduced as `systems/gba`.\n\n## Workspace Command Examples\n\n```bash\n# Default workspace members (currently systems/gb core)\ncargo build --locked\ncargo test --locked\n\n# All workspace packages (may require optional host dependencies like SDL2 dev libs)\ncargo build --locked --workspace\n\n# Package-targeted checks\ncargo test --locked -p gb-runtime\ncargo test --locked -p frontend-cli\ncargo build --locked -p frontend-sdl2 --bin frontend-sdl2\ncargo build --locked -p frontend-wasm --lib\n```\n\nNotes:\n- `cargo lint` intentionally skips `frontend-sdl2` in the default alias to avoid requiring SDL2 system libraries in every CI/local environment.\n- Prefer package-targeted commands for frontend work when optional host dependencies are not installed globally.\n\n## Run\n\n```bash\ncargo run -p frontend-cli --bin gb-emu -- \u003cpath_to_rom.gb\u003e\n```\n\nUseful flags:\n\n```bash\ncargo run -p frontend-cli --bin gb-emu -- --trace \u003cpath_to_rom.gb\u003e\ncargo run -p frontend-cli --bin gb-emu -- --blargg --max-steps 120000000 \u003cpath_to_rom.gb\u003e\ncargo run -p frontend-cli --bin gb-emu -- --mooneye --model dmg0 \u003cpath_to_rom.gb\u003e\ncargo run -p frontend-cli --bin gb-emu -- --cart-info \u003cpath_to_rom.gb\u003e\ncargo run -p frontend-cli --bin gb-emu -- --sgb-report --model sgb --max-steps 4000000 \u003cpath_to_rom.gb\u003e\ncargo run -p frontend-cli --bin gb-emu -- --bootrom-dir roms/bootrom --model mgb \u003cpath_to_rom.gb\u003e\ncargo run -p frontend-cli --bin gb-emu -- --no-bootrom \u003cpath_to_rom.gb\u003e\n```\n\nSupported models for `--model`:\n- `dmg0`\n- `dmg` (default)\n- `mgb`\n- `sgb`\n- `sgb2`\n\n## Current Limitations\n\n### Cartridge / Mapper / Persistence Limits\n- Supported cartridge types:\n  - ROM-only / ROM+RAM / ROM+RAM+BATTERY (0x00/0x08/0x09).\n  - MBC1 family (0x01/0x02/0x03) with RAM enable and RAM banking mode support.\n  - MBC2 family (0x05/0x06) with 512x4-bit internal RAM behavior.\n  - MBC3 family (0x0F/0x10/0x11/0x12/0x13) with ROM/RAM banking and RTC register/latch support.\n  - MBC5 family including rumble variants (0x19..0x1E), with ROM/RAM banking support and rumble control-bit tracking.\n- Supported ROM size codes: 0x00..0x08 and 0x52/0x53/0x54 (validated against exact file length).\n- Supported RAM size codes: 0x00..0x05 for supported cartridge families.\n- For compatibility with legacy test ROM conventions, RAM-capable cartridge types declaring RAM size code `0x00` get a transient 8KB external RAM window.\n- ROM-only and ROM+RAM cartridge family (no MBC) is limited to 32KB ROM by hardware design.\n- Unsupported mappers (for example MBC6/MBC7/HuC variants, camera/tama) still fail fast when loading the ROM.\n- MBC3 RTC persistence currently uses a sidecar `.rtc` file; this is emulator-specific metadata and not a hardware cartridge dump format.\n- RTC clock source currently remains a core-local convenience (`SystemRtcClock`) for native hosts, while host/frontends can inject RTC epoch time for portability-sensitive targets (web) and future deterministic/libretro integrations.\n- Web demo battery persistence currently uses browser local storage (base64-encoded SRAM/RTC sidecar blobs keyed per ROM file/hash); browser quota/privacy/security settings can disable or evict stored data.\n- Cartridge capabilities/compatibility policy now normalize the CGB (`0x0143`) and SGB (`0x0146`) header flags and combine them with the selected hardware model. Core execution still treats the result as advisory metadata only (current DMG-family scope does not enforce/reject by CGB header requirements and does not switch to CGB execution), but runtime/frontends now use the SGB header flag as the gate for cart-driven SGB command transport/display scaffolding.\n\n### Cartridge Header Diagnostics\n- Header logo/checksum mismatches are reported as metadata warnings but do not block ROM loading.\n\n### CPU / Core Fidelity\n- CPU correctness and timing confidence are currently driven by the included Blargg + Gekkio suites and project integration tests; untested instruction/interrupt corner cases may still remain.\n- The emulator is currently DMG-family focused (`dmg0`, `dmg`, `mgb`, `sgb`, `sgb2`); CGB-specific CPU/platform behavior (for example double-speed mode and CGB-only hardware interactions) is out of scope.\n- Optional boot ROM startup currently supports DMG-family 256-byte boot ROM windows (`0x0000..=0x00FF`) only; CGB/AGB boot ROM mapping behavior is not implemented in current scope.\n- Cross-subsystem cycle accuracy (CPU vs PPU/APU/DMA/bus contention) is implemented incrementally and is only guaranteed for the timing cases explicitly covered by current tests and documented PPU/DMA behavior.\n- CPU timing plumbing is mostly policy-derived in current DMG scope, but some CPU timing work remains outside this migration (for example future CGB-specific timing behavior/policies and additional non-instruction edge cases not yet explicitly characterized).\n- The bus/memory segment helper layer is currently DMG single-bank only; future CGB VRAM/WRAM bank selection (`VBK`/`SVBK`) and CGB-specific bus access rules are not implemented yet.\n- `KEY1`/`VBK`/`SVBK` and CGB palette MMIO (`BGPI/BGPD`, `OBPI/OBPD`) scaffolding remain DMG-noop in current scope: reads behave as unmapped (`0xFF`), writes do not alter emulation behavior, and while internal placeholder shadows/state (including palette index/data shadow capture and auto-increment scaffold behavior) are recorded for future CGB wiring, no CGB-visible palette semantics or color output behavior is enabled.\n- FFxx IO routing is now centralized/declarative, but the CGB-oriented portion remains scaffold-level only in current DMG scope: many CGB FFxx registers are intentionally documented as reserved/unmapped placeholders and still decode to DMG no-op behavior until real CGB features are implemented.\n- DMA scheduling remains DMG-only in public behavior and implements only OAM DMA transfer rules for current models; `HDMA1..HDMA5` writes are DMG-visible no-ops, and while model-gated `GDMA/HDMA` transfer-state/HBlank-hook scaffolding now exists internally for future CGB wiring, CGB DMA enablement, timing accuracy, and CGB-specific DMA bus restrictions are not implemented yet.\n- SGB scaffolding now decodes and applies a larger subset of commands (`PALxx`, `PAL_TRN`, `PAL_SET`, `ATRC_EN`, `TEST_EN`, `ICON_EN`, `DATA_SND`, `DATA_TRN`, `JUMP`, `PAL_PRI`, `MASK_EN`, `ATTR_BLK`, `ATTR_LIN`, `ATTR_DIV`, `ATTR_CHR`, `ATTR_TRN`, `ATTR_SET`, `MLT_REQ`, `CHR_TRN`, `PCT_TRN`, `OBJ_TRN`) and supports border data composition, basic SNES-object overlay composition, core-side multiplayer joypad routing, plus mask freeze/black/color0 viewport behavior, but full SGB command/data coverage is not implemented yet (audio/command families outside the current control/display subset and deeper SNES-side `OBJ_TRN` quirks are still pending).\n- `GameBoy`/`Bus` currently expose a small set of persistence-byte bridge helpers for `gb_runtime::cartridge_persistence`; tightening or reshaping that host-facing boundary is deferred unless the core API surface grows significantly.\n\n### Runtime / Host Utility Maintainability\n- `runtime/src/audio.rs` is intentionally kept as a single module for now, but if runtime audio helpers continue to grow it should be split into `runtime/src/audio.rs` + `runtime/src/audio/*` submodules (for example `mixer`, `adaptive_queue`, `resampler`) as a maintenance refactor without behavioral changes.\n- Queue-targeting policy is now runtime-owned via `gb_runtime::audio_queue`; browser host code should treat it as the source of truth to avoid SDL2/web drift.\n- SGB multiplayer joypad routing is now reachable from SDL2/wasm/web through fixed keyboard bindings, but host-side gamepad support, input remapping UI, and richer multi-controller UX are still future work.\n- Traffic-driven SGB auto-detection is intentionally conservative: runtime only accepts cart-driven SGB packets for cartridges with the SGB header flag set and ignores unsupported boot/header packet IDs, so plain DMG carts cannot false-positive from arbitrary JOYP polling noise. Frontend `auto` palette promotion remains limited to real `sgb`/`sgb2` sessions; on `dmg`/`mgb`, manual `sgb`, `sgb-1a..sgb-4h`, `cgb`, or `cgb-*` selection is still required if you want a display-only colorizer.\n\n### PPU / Rendering / Timing Fidelity\n- Framebuffer is DMG grayscale and currently focused on correctness over rendering performance optimizations.\n- `cgb` palette selection is still display-side only: it now uses curated mGBA-style header-CRC overrides plus BG/OBJ0/OBJ1 triplets from the DMG framebuffer metadata, but no hardware CGB palette RAM semantics, CGB VRAM/WRAM banking, or true CGB rendering rules are implemented yet.\n- External override INI support currently affects the display-side `cgb` palette path plus the BIOS-style SGB boot palette HLE fallback for monochrome carts on real `sgb`/`sgb2` sessions before any cart-driven SGB command is applied. It still does not override real SGB runtime command processing/border composition after cart traffic starts, nor true CGB hardware palette semantics.\n- Non-SGB-enhanced monochrome cartridges now use the SGB BIOS-style default boot palette path with exact header-CRC overrides first and title-based overrides as fallback, but the broader SNES-side BIOS behavior is still incomplete: the full menu/palette UX, built-in border/menu interactions, and titles not covered by the current built-in HLE tables still fall back to the generic default SGB palette. Manual `sgb-1a..sgb-4h` palette selection remains display-only and does not emulate SNES-side SGB command flow or borders by itself.\n- `OBJ_TRN` is no longer limited to the earlier 28x4-byte approximation and now uses the documented 24-entry OAM tail plus X-MSB/size extension bits and system-palette-derived OBJ colors, with a conservative `16x16` large-object render path and a less binary viewport-priority heuristic. It still remains an HLE approximation of SNES OBJ behavior: full SNES object-size selection, exact base-name/page semantics, and finer priority/composition quirks are still pending.\n- Dot-stepped OBJ fetch contention now extends Mode 3 at runtime and takeover boundaries include FIFO-stall arbitration; some DMG fetcher bus-phase details (for example full hardware sleep/push micro-ops) are still approximated.\n- Mode 3 OBJ fetch now covers active OAM DMA word substitution for overlapping sprite tile/attr reads, but broader DMG OAM corruption behavior and deeper sprite-scan/fetch edge cases outside the current fetch-time path are still approximated.\n- The Mode 3 pixel pipeline now carries DMG-oriented intermediate metadata plus CGB attr/palette scaffolding fields, but CGB BG tile attributes / OBJ palette metadata are not yet used to change rendering rules or final color output (current behavior remains DMG-only, and runtime scaffold wiring is model-gated off for current DMG-family models).\n- Formal PPU mode-entry edge hooks now exist for future HBlank-DMA/HDMA wiring, but no HDMA/HBlank DMA behavior is implemented in current DMG scope.\n- Recent Mode 3 BG `Push` fetcher work refines the internal state-machine (explicit latched `RecoverySleep` substate) and improves regression observability for the `stall/recovery` path, but it does not yet introduce additional hardware-visible micro-ops outside that `stall -\u003e recovery sleep -\u003e push-ready` flow.\n- Remaining high-impact PPU fidelity work is concentrated in timing-sensitive Mode 3 corner cases (finer fetcher micro-ops / bus-phase modeling and additional DMA/STAT contention edge cases beyond the currently covered regressions).\n\n### APU / Audio Fidelity\n- Built-in analog calibration profiles are model-level references; full per-device fidelity requires supplying measured calibration values via `GameBoy::set_audio_analog_calibration(...)`.\n- Envelope \"zombie mode\" (`NRx2` writes while a channel is active) is implemented using documented/common behavior; full unit/model-specific DMG variants are still not exhaustively modeled.\n- CH3 Wave RAM active-access timing is modeled with a t-cycle fetch window approximation (sufficient for common DMG edge-cases), not a fully cycle-accurate bus arbitration model.\n- VIN / external audio input routing (`NR50` VIN bits) is not currently modeled.\n- Audio resampling remains interpolation-based (selectable `linear` or `cubic`, with linear fallback at cubic edges), not a band-limited/FIR resampler.\n\n## Mapper Coverage Examples\n\n- No MBC (ROM-only): `Tetris`, `Dr. Mario`.\n- MBC1: `Super Mario Land`, `Kirby's Dream Land`.\n- MBC2: `Pokemon Red/Blue`.\n- MBC3: `Pokemon Gold/Silver`.\n- MBC5: `Pokemon Pinball`.\n\n## Local Requirements\n\n- Rust stable toolchain (see `rust-toolchain.toml`).\n- `git`, `curl`, `unzip`, `perl`, and `rg` (ripgrep) for ROM fetch/run scripts.\n- Optional for SDL2 frontend: SDL2 runtime/dev libraries available in the OS.\n- Optional for web frontend: `wasm-pack` (or equivalent wasm build tooling).\n- Optional for web frontend unit tests: Node.js (for `node --test`).\n- Optional for `scripts/dev/run_web_demo.sh`: `python3`.\n\nBootstrap helper:\n\n```bash\n# Check core + SDL2 + web dependencies.\nscripts/dev/bootstrap.sh\n\n# Same check, but install wasm-pack automatically when missing.\nscripts/dev/bootstrap.sh --install-wasm-pack\n\n# Skip one frontend when not needed on your machine.\nscripts/dev/bootstrap.sh --skip-sdl2\nscripts/dev/bootstrap.sh --skip-web\n```\n\nCommon SDL2 install hints:\n- macOS (Homebrew): `brew install sdl2`\n- Debian/Ubuntu: `sudo apt-get update \u0026\u0026 sudo apt-get install -y libsdl2-dev`\n- Fedora: `sudo dnf install -y SDL2-devel`\n- Arch Linux: `sudo pacman -S --needed sdl2`\n\n## Quality and Tests\n\nFormatting/lint aliases are defined in `.cargo/config.toml`.\n\n```bash\ncargo fmt-check\ncargo lint\ncargo test --locked\ncargo test --locked -p gb-runtime\n```\n\n`cartridge` tests include a mapper conformance matrix for all currently supported cartridge type codes (`0x0147`) plus integration smoke coverage through `GameBoy`.\n\nOptional web frontend unit test:\n\n```bash\nnode --test web/*.test.mjs\n```\n\nROM test suites:\n\n```bash\n# Blargg\nscripts/blargg/fetch_blargg_roms.sh\n# Targeted audio/realtime mixer guard (local/dev, timeout-based):\nscripts/dev/run_audio_guard.sh\n# Targeted DMA scheduler debug guard (local/dev and CI, timeout-based):\nscripts/dev/run_dma_guard.sh\n# Runs all configured DMG Blargg ROMs:\nscripts/blargg/run_blargg.sh\n\n# Gekkio (Mooneye)\nscripts/gekkio/fetch_gekkio_roms.sh\n# Default is GEKKIO_SUITE=all:\nscripts/gekkio/run_gekkio.sh\n# Default stable Gekkio suite (core + acceptance/ppu):\nGEKKIO_SUITE=all scripts/gekkio/run_gekkio.sh\n# Boot matrix by hardware model (dmg0/dmg/mgb/sgb/sgb2):\nGEKKIO_SUITE=boot_models scripts/gekkio/run_gekkio.sh\n# Run a suite against a specific hardware model:\nGB_MODEL=sgb GEKKIO_SUITE=all scripts/gekkio/run_gekkio.sh\nGB_MODEL=mgb scripts/blargg/run_blargg.sh\n```\n\nUseful environment overrides for scripts:\n- `GB_MODEL` (default: `dmg`) for both `run_blargg.sh` and `run_gekkio.sh`.\n- `GEKKIO_SUITE` (`all`, `core`, `boot_models`) for `run_gekkio.sh`.\n- `ROM_ROOT` to point to a custom ROM directory.\n- `MAX_STEPS` and `TIMEOUT_SECS` to tune execution limits.\n- `GEKKIO_VERSION` to fetch a specific Mooneye bundle version.\n- `TEST_NAME` to override the integration test executed by `scripts/dev/run_audio_guard.sh`.\n- `TEST_NAME` to override the unit test executed by `scripts/dev/run_dma_guard.sh`.\n\n## Frontend Bootstrap (SDL2 + Web)\n\nSDL2 desktop frontend (macOS / Windows / Linux):\n\n```bash\ncargo run -p frontend-sdl2 --bin frontend-sdl2 -- \u003cpath_to_rom.gb\u003e [dmg0|dmg|mgb|sgb|sgb2]\n\n# Skip boot ROM startup for frontend/manual diagnostics\ncargo run -p frontend-sdl2 --bin frontend-sdl2 -- --no-bootrom \u003cpath_to_rom.gb\u003e [dmg0|dmg|mgb|sgb|sgb2]\n\n# Override the boot ROM search directory for SDL2\ncargo run -p frontend-sdl2 --bin frontend-sdl2 -- --bootrom-dir roms/bootrom \u003cpath_to_rom.gb\u003e [dmg0|dmg|mgb|sgb|sgb2]\n```\n\nSDL2 build/run helper (locks deps, prepares Homebrew SDL2 env on macOS, and clean-rebuilds by default):\n\n```bash\n# Build only (clean + locked SDL2 build)\nscripts/dev/run_sdl2_frontend.sh --no-run\n\n# Build and run in release mode (recommended for performance)\nscripts/dev/run_sdl2_frontend.sh --release --no-clean -- \u003cpath_to_rom.gb\u003e\n\n# Build and run\nscripts/dev/run_sdl2_frontend.sh -- \u003cpath_to_rom.gb\u003e [dmg0|dmg|mgb|sgb|sgb2]\n\n# Faster iteration without clean\nscripts/dev/run_sdl2_frontend.sh --no-clean -- \u003cpath_to_rom.gb\u003e\n\n# Pass SDL2 frontend flags through the helper after `--`\nscripts/dev/run_sdl2_frontend.sh --release --no-clean -- --no-bootrom \u003cpath_to_rom.gb\u003e [dmg0|dmg|mgb|sgb|sgb2]\n```\n\nWeb frontend bindings (wasm):\n\n```bash\nwasm-pack build frontends/wasm --target web --out-name gb_emu\n```\n\nMinimal browser demo (AudioWorklet + keyboard + ROM file loader):\n\n```bash\nscripts/dev/run_web_demo.sh\n# Open http://localhost:8080/web/\n```\n\nNotes:\n- The core remains frontend-agnostic and can be embedded by multiple frontends.\n- MBC5 rumble status is exposed from core (`GameBoy::cartridge_has_rumble()`, `GameBoy::rumble_active()`), but no host haptics backend is wired yet.\n- Cartridge metadata is exposed from core via `Cartridge::metadata()` and `GameBoy::cartridge_metadata()` (type code, mapper, ROM/RAM size codes, bank counts, declared/effective RAM, battery/timer/rumble flags, and header diagnostics warnings).\n- Current web entrypoint is `WebEmulator` in `frontends/wasm/src/lib.rs`.\n- `web/` contains browser host assets only; the Rust/WASM adapter crate lives in `frontends/wasm/`.\n- Web builds inject host wall-clock epoch time (`Date.now()`) into the core RTC path for MBC3 RTC state, avoiding browser target traps from direct wall-clock queries inside the core.\n- Web demo now uses a retro DMG-inspired shell layout centered on the Game Boy screen, with a floating `Settings` panel hidden by default and toggled from a top-right two-line button near the `DOT MATRIX WITH STEREO SOUND` strip. Branding in the shell reads `Emulator GAME BOY TM`; runtime status text remains in `Debug` (below cartridge info). The left `BATTERY` indicator lights when a ROM is loaded and turns off when no ROM is active. `Settings` includes sections (`Save Data`, `System`, `Audio`, `Debug`) plus `Load ROM` without a persistent side column. Canvas scaling is manual via `System \u003e Video Size` with integer steps (`x1` to `x4`) from the native DMG resolution (160x144), defaulting to `x4` and never changing automatically on responsive resize. The canvas shows a click target only while no cartridge is loaded (`Load ROM`), mapped to the same action as the settings control. `Reset ROM` lives in `Save Data` near SAV/RTC operations. The demo auto-starts emulation after `Load ROM` (and after `Reset ROM`) and keeps per-model boot ROM import/persistence in browser storage (dmg0/dmg/mgb/sgb/sgb2). `Load Boot ROM(s)` accepts one or many files, classifies each through the shared known-hash normalizer, stores only valid DMG-family matches by their canonical hardware slot, and shows a green validity check for the currently selected hardware when a valid boot ROM is present in browser storage.\n- Web demo video controls now include a palette selector (`auto|dmg|mgb|cgb|sgb|grayscale|gb-pocket|gb-light|cgb-brown|cgb-red-a|cgb-dark-brown-b|cgb-pale-yellow|cgb-orange-a|cgb-yellow-b|cgb-blue|cgb-dark-blue-a|cgb-gray-b|cgb-green|cgb-dark-green-a|cgb-reverse-b|sgb-1a..sgb-4h`) plus a palette-override file importer. `auto` keeps a DMG/MGB base palette on non-SGB models but uses the BIOS-style SGB boot palette immediately on active `sgb`/`sgb2` sessions, `cgb` applies the curated mGBA-style per-ROM override table with a brown fallback, optional imported override INI files can replace those `cgb` colors by exact `header_crc32`, and on real `sgb`/`sgb2` sessions the same imported INI also overrides the BIOS-style monochrome boot palette HLE via `pal[0]..pal[3]` until real cart-driven SGB commands take over. The imported override file is persisted in browser storage and re-applied on later page loads / ROM activations until you clear it. Manual `sgb-1a..sgb-4h` entries still expose the 32 built-in SGB BIOS four-shade presets without promoting the session to composed SGB border rendering.\n- SDL2/web keyboard mapping:\n  - Player 1: arrows=`D-Pad`, `Z`=`B`, `X`=`A`, `Backspace`=`Select`, `Enter`=`Start`\n  - Player 2: `WASD`=`D-Pad`, `F`=`B`, `G`=`A`, `R`=`Select`, `T`=`Start`\n  - Player 3: `IJKL`=`D-Pad`, `U`=`B`, `O`=`A`, `Y`=`Select`, `P`=`Start`\n  - Player 4: numpad `8/4/5/6`=`D-Pad`, `1`=`B`, `2`=`A`, `7`=`Select`, `9`=`Start`\n- SDL2 debug panel: press `F1` to open a cartridge metadata/warnings popup.\n- SDL2/Web runtime wiring now uses `gb_runtime::session::RuntimeSession` (shared `GameBoy + FramePacer + AudioMixer` orchestration) to reduce frontend-specific timing/audio drift.\n- SDL2 audio uses the core mixer clock bridge and queues stereo interleaved PCM in real time (now from the `frontends/sdl2` workspace package).\n- SDL2 queue refill is driven by emulated audio t-cycles and enqueues only available core samples (no synthetic silence padding in refill blocks).\n- SDL2 queue target is auto-tuned over time windows (same policy as web) using estimated underruns from elapsed playback vs queued samples.\n- `scripts/dev/run_sdl2_frontend.sh` is the recommended local command for SDL2 builds/runs (including a `--release` mode for performance) and consistent macOS/Homebrew linker env setup.\n- SDL2 renderer uses accelerated rendering with `present_vsync()` by default to reduce visible tearing during scroll/camera movement; override with `GB_SDL2_VSYNC=0` for diagnostics/perf comparisons.\n- Optional SDL2 debug tone: set `GB_AUDIO_TEST_TONE=1`.\n- Optional SDL2 core APU resampler quality override: set `GB_AUDIO_RESAMPLER=linear` or `GB_AUDIO_RESAMPLER=cubic` (default).\n- Optional SDL2 VSync override: set `GB_SDL2_VSYNC=1` (default) or `GB_SDL2_VSYNC=0`.\n- Optional SDL2 video palette override: set `GB_VIDEO_PALETTE=auto|dmg|mgb|cgb|sgb|grayscale|gb-pocket|gb-light|cgb-brown|cgb-red-a|cgb-dark-brown-b|cgb-pale-yellow|cgb-orange-a|cgb-yellow-b|cgb-blue|cgb-dark-blue-a|cgb-gray-b|cgb-green|cgb-dark-green-a|cgb-reverse-b|sgb-1a..sgb-4h` (default: `auto`, DMG/MGB base palette on non-SGB models and BIOS-style SGB boot palette on active `sgb`/`sgb2` sessions, with cart-driven SGB commands still layered on top when the cartridge declares SGB support; `cgb` uses the curated mGBA-style per-ROM override table, and manual `sgb-1a..sgb-4h` entries stay on the `160x144` DMG-style presentation path).\n- Optional SDL2 palette override INI: set `GB_VIDEO_PALETTE_OVERRIDES=/absolute/path/to/overrides.ini` to load mGBA-style sections such as `[gb.override.302017CC]` with `pal[0]..pal[11]` RGB888 values. These entries override the display-side `cgb` triplet for matching cartridge header CRC32 values and also replace the BIOS-style SGB boot palette HLE (`pal[0]..pal[3]`) for monochrome carts on real `sgb`/`sgb2` sessions before cart-driven SGB commands arrive.\n\nExample override file:\n\n```ini\n[gb.override.302017CC]\npal[0]=0xFFC6FF\npal[1]=0xFF8CD6\npal[2]=0x944A7B\npal[3]=0x4A2952\n```\n- In `cgb` mode, `pal[0]..pal[11]` map to the BG/OBJ0/OBJ1 triplet exactly as in mGBA-style overrides.\n- On real `sgb`/`sgb2` sessions, `pal[0]..pal[3]` also override the BIOS-style monochrome boot palette HLE for that cartridge header CRC32 until a cart-driven SGB command updates palette state.\n- Battery-backed cartridges loaded via `gb_runtime::cartridge_persistence` persist external RAM to a sibling `.sav` file; MBC3 timer carts also persist RTC metadata to `.rtc`. Save writes use atomic temp-file+rename replacement. Current CLI frontend flushes saves on graceful exit; SDL2 also performs dirty-flag autosave with a short debounce window plus a flush on window focus loss, while keeping the graceful-exit flush. The web demo mirrors this policy with browser-side autosave debounce and page visibility/navigation flush hooks using local storage (browser quota/security policies may still block persistence).\n- Web demo audio control uses a toggle button (`Enable audio` / `Disable audio`); audio is auto-enabled when a ROM is loaded/reset (user gesture path) when possible and can still be manually disabled/re-enabled during a session.\n- Core helper: `GameBoy::set_audio_analog_calibration(profile)` to apply measured per-device analog calibration profiles from host/frontends.\n- Web helpers:\n  - `run_for_elapsed_micros(elapsed_micros)` to step as many emulated frames as host time allows.\n  - `audio_clock_tcycles()` / `drain_audio_tcycles()` for raw emulated audio clock access.\n  - `set_audio_sample_rate(rate_hz)` (preserves queued Core APU audio when reconfiguring WebAudio rate) and `drain_audio_samples(max_samples)` for queue-based WebAudio feeding (`Vec\u003cf32\u003e` stereo interleaved: `L,R,L,R,...`).\n  - `observe_audio_queue_target(now_ms, queued_samples)`, `audio_queue_clear_required()`, and `commit_audio_queue_refill(now_ms, queued_samples_after_refill)` to drive queue-based host refill using shared runtime policy.\n  - `audio_queue_refill_block_samples()` / `audio_queue_max_refill_blocks()` to read runtime-owned queue refill limits from wasm/web hosts.\n  - `set_audio_resampler_quality(\"linear\" | \"cubic\")` and `audio_resampler_quality()` to compare interpolation quality/CPU tradeoffs from frontends.\n  - `drain_audio_samples_realtime(block_samples)` for callback-style fixed-size backends (`block_samples` = frames, returned buffer is stereo interleaved and zero-padded when short).\n  - `set_audio_test_tone_enabled(enabled)` for pipeline/debug validation.\n  - `cartridge_debug_report()` and `cartridge_warning_count()` for frontend cartridge diagnostics panels.\n- `web/` is intentionally small and uses `AudioWorkletNode` for lower-latency callback-style audio.\n- `web/` surfaces cartridge metadata/warnings plus audio telemetry (`queued ms`, cumulative underrun samples/ms, and current resampler mode) and auto-adjusts the refill target queue based on recent underrun windows.\n\n## CI\n\nWorkflows:\n- `.github/workflows/quality.yml`: format, lint, build, unit/integration tests.\n- `.github/workflows/rom-tests.yml`: two independent jobs/checks for branch protection:\n  - `rom-blargg` (includes a realtime audio micro-guard and the `cpu_instrs` micro-guard before the full Blargg suite)\n  - `rom-gekkio` (runs `all` + `boot_models`)\n\n## Test ROMs and Licensing Notes\n\n`/roms` is intentionally ignored in `.gitignore` to avoid storing test ROM binaries in this repository.\n\nBoot ROM placement policy (local-only):\n- Store boot ROM binaries under `roms/bootrom/`.\n- Only `roms/bootrom/.gitkeep` is versioned; boot ROM binaries stay ignored.\n- Recommended model file names: `dmg0_boot.bin`, `dmg_boot.bin`, `mgb_boot.bin`, `sgb_boot.bin`, `sgb2_boot.bin`.\n- CLI/SDL2 frontends auto-load `\u003cbootrom_dir\u003e/\u003cmodel\u003e_boot.bin` when present and otherwise keep the existing post-boot default startup path.\n- On frontend boot ROM lookup, the directory is normalized by known SHA-256 hashes (first 256 bytes): recognized dumps are renamed to canonical file names, and unknown/invalid blobs are prefixed with `invalid_` so they are excluded from autoload.\n- CLI and SDL2 add explicit per-run controls: `--no-bootrom` (force post-boot defaults) and `--bootrom-dir \u003cpath\u003e` (override boot ROM lookup root).\n- CLI/SDL2 boot ROM directory resolution order: `--bootrom-dir \u003cpath\u003e` (if provided) -\u003e `GB_BOOTROM_DIR` (if set) -\u003e `roms/bootrom`.\n\nCI and local setup use both:\n- `scripts/blargg/fetch_blargg_roms.sh`\n- `scripts/gekkio/fetch_gekkio_roms.sh`\n\nBoth pull public test ROM sources at runtime.\n\nWhy:\n- Keeps the repository lightweight.\n- Avoids redistributing binaries with mixed or unclear licensing terms.\n\n`scripts/gekkio/run_gekkio.sh` supports two profiles:\n- `GEKKIO_SUITE=all` (default): stable acceptance set defined in `scripts/gekkio/rom.txt`.\n- `GEKKIO_SUITE=boot_models`: dedicated boot-state matrix per model defined in `scripts/gekkio/roms_boot_models.txt`.\n\n`scripts/blargg/run_blargg.sh` runs the full DMG Blargg set defined in:\n- `scripts/blargg/rom.txt`\n\n`scripts/blargg/fetch_blargg_roms.sh` mirrors upstream `retrio/gb-test-roms` layout for the selected ROMs and writes a local listing to:\n- `roms/blargg's_test_roms/.blargg_listing.txt`\n\nCurrent Blargg DMG selection intentionally excludes:\n- `cgb_sound/*`\n- `interrupt_time/*`\n\nCI currently runs `GEKKIO_SUITE=all` and `GEKKIO_SUITE=boot_models` as required ROM checks.\n\nWhen adding new ROM suites, document:\n- Source repository URL.\n- Upstream license/status.\n- Which script/workflow consumes them.\n\n## Development Notes\n\n- Keep code identifiers/comments in English.\n- Prefer small, safe refactors with tests.\n- Add/adjust tests whenever behavior changes (unit + integration as needed).\n- Optional local hook setup: `scripts/dev/setup-hooks.sh` (pre-commit runs `cargo fmt-check` and `cargo lint`).\n- PR helper:\n  - From a feature branch, run `scripts/dev/create_pr.sh` (or `scripts/dev/create_pr.sh main`).\n  - PR title is set to the latest commit subject.\n  - PR body is taken from the latest commit body, or falls back to `.github/pull_request_template.md`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpakitovic%2Fgb-emu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpakitovic%2Fgb-emu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpakitovic%2Fgb-emu/lists"}