Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .github/workflows/windows-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ jobs:
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ github.event.inputs.ruby_version }}
bundler-cache: true

- name: Locate MSYS2
id: msys2
Expand Down Expand Up @@ -70,6 +69,29 @@ jobs:
mingw-w64-ucrt-x86_64-mgba \
mingw-w64-ucrt-x86_64-imagemagick

- name: Restore Bundler cache
id: bundler-cache
uses: actions/cache/restore@v5
with:
path: vendor/bundle
key: bundler-windows-ruby${{ github.event.inputs.ruby_version }}-${{ hashFiles('Gemfile.lock') }}

- name: Bundle install
if: steps.bundler-cache.outputs.cache-hit != 'true'
shell: bash
run: |
MSYS2_ROOT="${{ steps.msys2.outputs.root }}"
export PATH="$MSYS2_ROOT/ucrt64/bin:$PATH"
bundle config set path vendor/bundle
bundle install

- name: Save Bundler cache
if: always() && steps.bundler-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: vendor/bundle
key: bundler-windows-ruby${{ github.event.inputs.ruby_version }}-${{ hashFiles('Gemfile.lock') }}

- name: Restore compiled extension
id: ext-cache
uses: actions/cache/restore@v5
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,38 @@ All notable changes to gemba will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added

- Game Boy and Game Boy Color ROM support (160×144 resolution, correct aspect ratio)
- Input recording and replay — record button inputs to `.gir` files and replay them deterministically
- Video/audio capture to `.grec` files (F10 hotkey)
- Input replay player with dedicated window (open via menu or Cmd/Ctrl+O)
- Open ROM hotkey (Ctrl+O / Cmd+O), context-sensitive when replay player is active
- Settings tabs for save states, recording, and hotkey customization
- Rewind support
- ROM Info window showing title, game code, publisher, platform, resolution
- Session logging
- CLI subcommands for decoding `.grec` and `.gir` files
- ROM Patcher — apply IPS, BPS, and UPS patch files via GUI (View > Patch ROM…) or CLI (`gemba patch`)
- ZIP ROM support in patcher — drag in a zipped ROM and the output is a plain `.gba`
- Mouse cursor auto-hides after 2 seconds of inactivity while a game is playing; restores on movement or pause
- `?` hotkey toggles a floating hotkey reference panel beside the emulator window; emulation auto-pauses while it is open
- Help window auto-pauses emulation while open
- BIOS loading — configure a GBA BIOS file via Settings > System; gemba validates the file size (16 384 bytes), identifies Official GBA BIOS and NDS GBA Mode BIOS by checksum, and copies it to the gemba data directory; "Skip BIOS intro" option available for supported files

### Changed

- `.grec` header FPS field now varies by platform (was always GBA; existing files decode fine)
- Clearer UI labels: video/audio capture ("Capture") vs input recording ("Record Inputs")
- Hotkeys for pause, screenshot, and open ROM now work without a ROM loaded

### Fixed

- Games no longer start paused on Linux/Windows when window doesn't have focus at startup
- Opening the Logs Directory menu item no longer crashes (platform_open was not loaded by Zeitwerk)

## [0.1.1] — 2026-02-17

### Changed
Expand Down
Binary file added assets/placeholder_boxart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions bin/gemba
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# When running from a development checkout (not installed as a gem),
# When running from a development checkout (not installed as a gem or packed by premo),
# add lib/ to the load path and activate Bundler for gem dependencies.
if File.exist?(File.expand_path("../lib/gemba.rb", __dir__))
if File.exist?(File.expand_path("../.git", __dir__))
require "bundler/setup"
path = File.expand_path("../lib", __dir__)
$LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path)
Expand Down
127 changes: 125 additions & 2 deletions ext/gemba/gemba_ext.c
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,8 @@ get_mgba_core(VALUE self)
static VALUE
mgba_core_initialize(int argc, VALUE *argv, VALUE self)
{
VALUE rom_path, save_dir;
rb_scan_args(argc, argv, "11", &rom_path, &save_dir);
VALUE rom_path, save_dir, bios_path;
rb_scan_args(argc, argv, "12", &rom_path, &save_dir, &bios_path);

struct mgba_core *mc;
TypedData_Get_Struct(self, struct mgba_core, &mgba_core_type, mc);
Expand Down Expand Up @@ -324,9 +324,53 @@ mgba_core_initialize(int argc, VALUE *argv, VALUE self)
mDirectorySetMapOptions(&core->dirs, &opts);
}

/* 7b. Load BIOS if provided (must be before reset) */
if (!NIL_P(bios_path)) {
Check_Type(bios_path, T_STRING);
struct VFile *bvf = VFileOpen(StringValueCStr(bios_path), O_RDONLY);
if (bvf) {
if (!core->loadBIOS(core, bvf, 0)) {
bvf->close(bvf);
}
}
}

/* 8. Reset */
core->reset(core);

/* 8b. Re-query dimensions now that the ROM is loaded and the board
* pointer is populated. For GB/GBC, the pre-load query returns the
* SGB frame size (256x224) because core->board is NULL at that point.
* After reset the real model is known, so desiredVideoDimensions
* returns the correct 160x144 for non-SGB games. When the
* dimensions shrink we must reallocate and call setVideoBuffer so
* the stride matches the actual width. */
{
unsigned w2, h2;
core->desiredVideoDimensions(core, &w2, &h2);
if (w2 != w || h2 != h) {
color_t *new_vbuf = calloc((size_t)w2 * h2, sizeof(color_t));
uint32_t *new_prev = calloc((size_t)w2 * h2, sizeof(uint32_t));
if (!new_vbuf || !new_prev) {
free(new_vbuf);
free(new_prev);
free(mc->video_buffer);
mc->video_buffer = NULL;
free(mc->prev_frame);
mc->prev_frame = NULL;
core->deinit(core);
rb_raise(rb_eNoMemError, "failed to reallocate video buffer");
}
free(mc->video_buffer);
free(mc->prev_frame);
mc->video_buffer = new_vbuf;
mc->prev_frame = new_prev;
core->setVideoBuffer(core, mc->video_buffer, w2);
}
mc->width = (int)w2;
mc->height = (int)h2;
}

/* 9. Autoload save file (.sav alongside ROM, or in save_dir).
* Creates the .sav if it doesn't exist yet. */
mCoreAutoloadSave(core);
Expand Down Expand Up @@ -985,10 +1029,67 @@ mgba_count_changed_pixels(VALUE mod, VALUE delta)
return LONG2NUM(changed);
}

/* --------------------------------------------------------- */
/* Core#bios_loaded? */
/* Returns true if a BIOS VFile is attached to this core. */
/* GBA only; returns false for other platforms. */
/* --------------------------------------------------------- */

static VALUE
mgba_core_bios_loaded_p(VALUE self)
{
struct mgba_core *mc = get_mgba_core(self);
if (mc->core->platform(mc->core) != mPLATFORM_GBA) {
return Qfalse;
}
struct GBA *gba = (struct GBA *)mc->core->board;
return gba->biosVf ? Qtrue : Qfalse;
}

/* --------------------------------------------------------- */
/* Init */
/* --------------------------------------------------------- */

/* --------------------------------------------------------- */
/* Core#load_bios(path) */
/* Load a BIOS file from path. Must be called before reset. */
/* Returns true on success, false on failure. */
/* --------------------------------------------------------- */

static VALUE
mgba_core_load_bios(VALUE self, VALUE rb_path)
{
struct mgba_core *mc = get_mgba_core(self);
Check_Type(rb_path, T_STRING);
const char *path = StringValueCStr(rb_path);

struct VFile *vf = VFileOpen(path, O_RDONLY);
if (!vf) {
return Qfalse;
}

bool ok = mc->core->loadBIOS(mc->core, vf, 0);
if (!ok) {
vf->close(vf);
}
/* mGBA takes ownership of vf on success; do not close */
return ok ? Qtrue : Qfalse;
}

/* --------------------------------------------------------- */
/* Gemba.gba_bios_checksum(bytes) */
/* Compute GBA BIOS checksum (mGBA algorithm) on raw bytes. */
/* --------------------------------------------------------- */

static VALUE
mgba_gba_bios_checksum(VALUE self, VALUE rb_bytes)
{
Check_Type(rb_bytes, T_STRING);
long len = RSTRING_LEN(rb_bytes);
uint32_t result = GBAChecksum((uint32_t *)RSTRING_PTR(rb_bytes), (size_t)(len / 4));
return UINT2NUM(result);
}

void
Init_gemba_ext(void)
{
Expand Down Expand Up @@ -1029,6 +1130,13 @@ Init_gemba_ext(void)
rb_define_method(cCore, "rewind_count", mgba_core_rewind_count, 0);
rb_define_method(cCore, "destroy", mgba_core_destroy, 0);
rb_define_method(cCore, "destroyed?", mgba_core_destroyed_p, 0);
rb_define_method(cCore, "load_bios", mgba_core_load_bios, 1);
rb_define_method(cCore, "bios_loaded?", mgba_core_bios_loaded_p, 0);

/* BIOS checksum utility */
rb_define_module_function(mGemba, "gba_bios_checksum", mgba_gba_bios_checksum, 1);
rb_define_const(mGemba, "GBA_BIOS_CHECKSUM", UINT2NUM(GBA_BIOS_CHECKSUM));
rb_define_const(mGemba, "GBA_DS_BIOS_CHECKSUM", UINT2NUM(GBA_DS_BIOS_CHECKSUM));

/* GBA key constants (bitmask values for set_keys) */
rb_define_const(mGemba, "KEY_A", INT2NUM(1 << GEMBA_KEY_A));
Expand All @@ -1042,6 +1150,21 @@ Init_gemba_ext(void)
rb_define_const(mGemba, "KEY_R", INT2NUM(1 << GEMBA_KEY_R));
rb_define_const(mGemba, "KEY_L", INT2NUM(1 << GEMBA_KEY_L));

/* GBA button name → bitmask hash (shared by KeyboardMap and GamepadMap) */
VALUE btn_bits = rb_hash_new();
rb_hash_aset(btn_bits, ID2SYM(rb_intern("a")), INT2NUM(1 << GEMBA_KEY_A));
rb_hash_aset(btn_bits, ID2SYM(rb_intern("b")), INT2NUM(1 << GEMBA_KEY_B));
rb_hash_aset(btn_bits, ID2SYM(rb_intern("l")), INT2NUM(1 << GEMBA_KEY_L));
rb_hash_aset(btn_bits, ID2SYM(rb_intern("r")), INT2NUM(1 << GEMBA_KEY_R));
rb_hash_aset(btn_bits, ID2SYM(rb_intern("up")), INT2NUM(1 << GEMBA_KEY_UP));
rb_hash_aset(btn_bits, ID2SYM(rb_intern("down")), INT2NUM(1 << GEMBA_KEY_DOWN));
rb_hash_aset(btn_bits, ID2SYM(rb_intern("left")), INT2NUM(1 << GEMBA_KEY_LEFT));
rb_hash_aset(btn_bits, ID2SYM(rb_intern("right")), INT2NUM(1 << GEMBA_KEY_RIGHT));
rb_hash_aset(btn_bits, ID2SYM(rb_intern("start")), INT2NUM(1 << GEMBA_KEY_START));
rb_hash_aset(btn_bits, ID2SYM(rb_intern("select")), INT2NUM(1 << GEMBA_KEY_SELECT));
OBJ_FREEZE(btn_bits);
rb_define_const(mGemba, "GBA_BTN_BITS", btn_bits);

/* Toast background generator */
rb_define_module_function(mGemba, "toast_background", mgba_toast_background, 3);

Expand Down
2 changes: 2 additions & 0 deletions ext/gemba/gemba_ext.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include <mgba/core/directories.h>
#include <mgba/core/log.h>
#include <mgba-util/vfs.h>
#include <mgba/internal/gba/bios.h>
#include <mgba/internal/gba/gba.h>

extern VALUE mGemba;

Expand Down
6 changes: 4 additions & 2 deletions gemba.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ Gem::Specification.new do |spec|
spec.extensions = ["ext/gemba/extconf.rb"]
spec.required_ruby_version = ">= 3.2"

spec.add_dependency "teek", ">= 0.1.2"
spec.add_dependency "teek-sdl2", ">= 0.1.3"
spec.add_dependency "teek", ">= 0.1.5"
spec.add_dependency "teek-sdl2", ">= 0.2.1"
spec.add_dependency "zeitwerk", "~> 2.7"

spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rake-compiler", "~> 1.0"
Expand All @@ -32,6 +33,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "listen", "~> 3.0"

spec.requirements << "libmgba development headers"
spec.add_development_dependency "webmock", "~> 3.0"
spec.add_development_dependency "rubyzip", ">= 2.4"

spec.requirements << "rubyzip gem >= 2.4 (optional, for loading ROMs from .zip files)"
Expand Down
16 changes: 1 addition & 15 deletions lib/gemba.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,4 @@
require "teek"
require "teek/sdl2"
require_relative "gemba/runtime"
require_relative "gemba/child_window"
require_relative "gemba/tip_service"
require_relative "gemba/settings_window"
require_relative "gemba/rom_info_window"
require_relative "gemba/save_state_picker"
require_relative "gemba/save_state_manager"
require_relative "gemba/toast_overlay"
require_relative "gemba/overlay_renderer"
require_relative "gemba/input_mappings"
require_relative "gemba/hotkey_map"
require_relative "gemba/recorder"
require_relative "gemba/input_recorder"
require_relative "gemba/input_replayer"
require_relative "gemba/player"
require_relative "gemba/replay_player"
require_relative "gemba/platform_open"
Loading
Loading