Gamebuino Classic games ported to SDL2, SDL3 and Playdate selectable in one convient menu
Port c:\github\gamebuino_classic_vircon32 (99 Gamebuino Classic games
behind one shared menu, targeting the Vircon32 fantasy console) to real,
vanilla C + SDL/Playdate - the same relationship Tinyjoypad_SDL has to
tinyjoypad_vircon32. Same overall architecture as Tinyjoypad_SDL
(gameworld/platform translation-unit split, CLI flags, initialization
sequence, SDL2/SDL3/Playdate multi-port structure) - but built on this
project’s own gamebuinoShim/menu system (Gamebuino Classic’s own 84x48
display, 7 buttons, EEPROM) instead of TinyJoypad’s OLED/simpler-input
system. Prompted directly by the user, who named Tinyjoypad_SDL as the
exact template to follow and separately authorized using background agents
for the bulk game-porting work.
All 99 games are now shipped on all three ports (SDL3, SDL2, Playdate)
Mirrors Tinyjoypad_SDL exactly - no repo-wide top-level build, one fully
self-contained CMake project per port:
src/gameworld/ - the platform-independent “game world”: avrCompat.h
(copied verbatim from Tinyjoypad_SDL), machineDependent.h (the
interface boundary, this project’s own real facts - see below),
gamebuinoShim.h/.c (the Gamebuino Classic API compatibility layer),
eepromShim.h/.c, menu.h/.c, menuGameList.h/.c, biosFont.h
(copied verbatim from Tinyjoypad_SDL), gamesMain.h/.c (menu<->game
dispatch + quit-confirmation dialog), games/games.h + one
games/game*.c per ported game (all 99). Never #includes
SDL.h or anything that pulls in <stdint.h> (would hard-conflict with
avrCompat.h’s own uint8_t-aliased-to-int #defines) - this is
what keeps this directory genuinely reusable by all three ports
unmodified, Playdate included.src/sdl3/ - the SDL3 port (built and proven first): main.c,
sdlBackend.h/.c, CInput.h/.c (copied verbatim - fully generic,
no game-specific logic), gamebuinoSDL3.h (this project’s own keybind
header), CMakeLists.txt.src/sdl2/ - the SDL2 port: built by porting src/sdl3/’s own
platform files API-call-by-API-call (gamebuinoSDL2.h, sdlBackend.c,
CInput.c), then verified functionally identical against SDL3 (same
keybinds, same CLI, all 99 games spot-checked).src/playdate/ - the Playdate port, a genuinely different kind of port
(fixed 400x240 1-bit hardware, its own C SDK, no CLI/window/desktop
concept at all) - see “The Playdate port” below for the full writeup.assets/thumbnails/ - thumb_NN.bmp (99 files, zero-padded 2-digit
index in real registration order - menuGameList.c’s own addGame()
call sequence, NOT alphabetical display order) plus the generated
thumbnailData.h, shared unmodified by both src/sdl3/ and
src/sdl2/ via each port’s own target_include_directories() - one
compiled-in copy, not duplicated per-port. See “Thumbnail generation”
below for the full pipeline.metadata/screenshots/ - one .bmp (the real, raw captured format -
.png copies sit alongside for README.md’s own display convention,
matching both sibling projects) + one .gbu stub per game, generated by
manually running the built binary’s own -ms/-gbu flags and copying
the output in - see “The .gbu convention” and “Generating metadata”
below for why this is a manual step, not build-time automation.Build from inside a port directory (e.g. cd src/sdl3 && cmake -B build -G
Ninja && cmake --build build), same as Tinyjoypad_SDL.
Every games/*.c file, plus gamebuinoShim.c/eepromShim.c/menu.c/
menuGameList.c/gamesMain.c, is its own separate translation unit -
gamesMain.c never #includes any of their .c bodies (unlike the
sibling Vircon32 build, which stitches every game into one TU via
#include in main.c). This is why every games/game*.c file needs its
own #include "avrCompat.h" / "machineDependent.h" / "gamebuinoShim.h"
at the top - a real, easy mistake made and caught during this project’s
own first hand-port (Pong Solo initially failed to compile with dozens of
implicit declaration of function errors from omitting these), and one
every one of the dispatched batch-porting agents had to be told explicitly
up front for the rest of this project’s history.
LCD_WIDTH/LCD_HEIGHT/
LCD_PAGES in machineDependent.h), not TinyJoypad’s 128x64/8 pages.md_playTone(freqHz, durationSeconds) interface
(matching the real Gamebuino NUM_CHANNELS-defaults-to-1 hardware
fact) - but, like both sibling projects’ own backends, actually
implemented as a real multi-voice square-wave mixer under the hood on
SDL2/SDL3, since nothing about the interface requires the backend
to enforce single-voice-ness.md_beginFrame()) - the
Gamebuino’s PCD8544 is a reflective LCD, the opposite polarity from
TinyJoypad’s self-illuminating OLED. This needed a second,
genuinely different clear primitive, md_beginMenuFrame() (BLACK
background), for the menu’s own white-on-black BIOS text (SDL2/SDL3
only - see “The Playdate port” for its own different menu design).md_drawColumnGray() - ported from the Vircon32 build’s own real-
gray-color feature (GB_GRAY, Button R toggle): while the toggle is
on, gbRenderFrame() draws a second, targeted pass over exactly the
pixels gbGrayBuffer[] marks as real gray content, painting a real
solid mid-gray directly. See “Grayscale rendering” below for the full
picture across all three ports, including Playdate’s own genuinely
different, native-pattern-fill mechanism for the identical toggle.MD_COLOR_RED, a real RGB tint (not monochrome like
MD_COLOR_BLACK/WHITE) - the SDL menu itself is full-color chrome,
and a markUnfinished()-flagged game’s own list/info text draws in
red, matching the Vircon32 build’s own set_multiply_color(color_red)
feature. md_drawColumnPixelsColor() and biosDrawTextColor()/
biosDrawCharColor() exist only for this. Playdate has no equivalent
color concept (a strictly 1-bit panel) - see “The Playdate port” for
its own real, different unfinished-game marker.EEPROM_SLOT_DATA_SIZE 1024, not 512 - matches the sibling
Vircon32 build’s own sizing (a real ATmega328’s own full 1024-byte
EEPROM), not TinyJoypad_SDL’s ATtiny85-sized 512. SDL2/SDL3 persist via
a real file-backed card; Playdate persists via the real Playdate SDK’s
own pd->file data-store API.GAME_SCALE/GAME_ORIGIN_X/Y in
sdlBackend.c) - 84*7+26*2=640, 48*7+12*2=360, matching the
sibling Vircon32 build’s own real TILE_SCALE/ORIGIN_X/ORIGIN_Y
layout exactly. Playdate uses its own different GAME_SCALE=4/
GAME_ORIGIN_X=32/GAME_ORIGIN_Y=24 (336x192 centered in the fixed
400x240 panel - 4 is the largest clean multiplier that still fits;
5 would overflow the panel’s own real width).The SDL2/SDL3 menu draws with the real Vircon32 BIOS font (biosFont.h),
copied byte-for-byte verbatim from the sibling Tinyjoypad_SDL
project - per a direct user request (“the menu needs to use same font as
tinyjoypad menu’s font”). Playdate’s own menu uses the real Playdate SDK’s
own native system font instead - a from-scratch, Playdate-specific menu
design, not a biosFont.h port (see “The Playdate port” below).
sdlBackend.c’s own gEffectState/gVolume/gMuted), read directly
off CInput’s own button state with no machineDependent.h surface at
all - these are pure presentation/audio-hardware concerns.md_inputR() and handled
in gamesMain.c’s own dispatch loop (SDL2/SDL3 only - see below) -
the one toggle that genuinely needs to cross the TU boundary, since it
flips gbRealGrayColor, a real global inside gamebuinoShim.c (the
“game world” side), which the platform backend has no business reaching
into directly.gamebuinoSDL3.h/gamebuinoSDL2.h)CInput’s own SButtons struct (copied verbatim from Tinyjoypad_SDL,
fully generic) has exactly one field per real gamepad face/shoulder/
trigger button (ButA/ButB/ButX/ButY/ButLB/ButRB/ButLT/
ButRT) - not a dedicated “ButC”. Real Gamebuino Button C is mapped onto
ButX; real-gray-color is ButRB; the combined effect cycle is ButLB;
real volume down/up are ButLT/ButRT.
The pixel-grid/glow/CRT cycle went through several real design
iterations before landing here, each a direct, explicit user request, not
a single up-front design: pixel-grid started as this project’s own
original, already-shipped standalone toggle (ported ahead of glow/CRT,
its own dedicated button); glow/CRT (glowEffect.h/crtEffect.h, copied
verbatim from Tinyjoypad_SDL) were then added on a separate button,
first attempting to faithfully match that project’s own curated 5-state
cycle (none -> pixel-grid+glow -> pixel-grid -> CRT -> glow -> none)
while deliberately keeping pixel-grid’s own already-shipped button
untouched; then, per a direct follow-up, the two were unified onto one
button and one real 3-bit counter (gEffectState) enumerating all 8
combinations of pixel-grid/glow/CRT (a genuine superset of Tinyjoypad_SDL’s
own curated subset, which never combines pixel-grid with CRT, for
instance) - gPixelGridEnabled/gGlowEnabled/gCrtEnabled are always
recomputed from gEffectState’s own 3 bits on every press, never tracked
independently. This freed pixel-grid’s own original button/slot, which a
final follow-up request repurposed for real analog volume control instead
(see below) - pixel-grid itself has no separate toggle button anymore, it
is purely one of the three bits reachable through the combined cycle.
Real volume control (gVolume, a float scaling AUDIO_AMPLITUDE in
the mixer, 0.05f step per press, independent of gMuted) was added on
direct request once the project was checked against Tinyjoypad_SDL and
found missing this exact feature (that project’s own PageDown/PageUp,
ButLB/ButRB) - remapped here onto the two real analog shoulder
triggers (ButLT/ButRT, L2/R2) instead, since ButLB/ButRB were
already spoken for by the effect cycle and real-gray-color respectively.
Gamepad-only, per direct request - no keyboard key was added for it.
Final keyboard layout: X=A, C=B, D=Gamebuino Button C, S=mute,
G=effect cycle (L/W/Z also work as alternates - L’s own original
meaning, pixel-grid, now reachable through the combined cycle instead),
R=real-gray-color (V also works as an alternate), Escape/Enter=
Start. BUTTON_DARKSWITCH is kept defined only because CInput.c’s own
keyboard-event switch references it - D is already claimed by Button C,
so it’s routed to Enter instead, a genuine second keyboard shortcut for
Start, not dead code.
Real gamepad layout (Xbox/Switch-style face buttons): South=A, East=B, West=C, North=mute, Left shoulder(L1)=effect cycle, Right shoulder(R1)= real-gray-color, Left/Right trigger(L2/R2)=volume down/up, Start/Back= Start.
While retargeting gamebuinoShim.c from the Vircon32 build, this
session’s own earlier reading of the real gbFont3x5/gbFont3x3/
gbFont5x7 data tables had fallen out of context across a conversation
compaction - when writing the new project’s own copy of these tables,
they were reconstructed from memory instead of being re-read from the
source, producing genuinely fabricated data. gbFont5x7 happened to
still render two digits recognizably by pure coincidence; gbFont3x5
(Agaruino’s own “AGARUINO !” title text) rendered visibly garbled, which
is what surfaced it via a direct user report. Fixed by re-extracting the
real decimal data directly from
gamebuino_classic_vircon32/src/gamebuinoShim.c via a small Python
script (never hand-retyped) and diff-verifying byte-for-byte against the
source for all three tables.
This is a durable, project-wide lesson, not a one-off fix: every game
ported into this project - including by every background agent across
every batch - must re-read the real source file fresh for any data array
(sprite bitmaps, level tables, font/lookup tables) and copy/script it
verbatim, never retype or recall it from memory, then diff-verify it
byte-for-byte against the source before trusting it. Saved to this
session’s own persistent memory (font-tables-must-be-copied-not-
recalled.md) so it survives future compactions too.
Confirmed to transfer directly from Tinyjoypad_SDL’s own established
recipe, since both projects port from the same Vircon32 C dialect:
int[N] name; -> int name[N]; (array-declaration order) - applies to
every array, including data tables.struct Foo { ... }; Foo x; -> typedef struct { ... } Foo; - so
pointer parameters (Foo* f) and plain declarations both work with no
struct keyword at the use site.int*/int[N] used for text strings -> char*/char[N] - a
genuine runtime-correctness fix (Vircon32 strings are one 32-bit word
per character). Bitmap/font/level data tables stay int[]/int* -
genuinely numeric byte tables, not text.typedef void(void) GameFunc;,
* re-added at each use site) -> real C typedefs
(typedef void (*GameFunc)( void );, * lives in the typedef itself).games/*.c file needs its own #includes (see “Translation-unit
boundary” above) - the one part of this recipe with no Vircon32-side
equivalent to translate from.a?b:c into if/else), this direction of the conversion is
free: any already-explicit if/else in the Vircon32 source can stay
as-is, or be tightened into a ternary here, purely a style choice.Used the same parallel-isolated-worktree agent workflow both sibling
projects have already established: one background agent per batch of
~10-15 games, each in its own git worktree (Agent tool,
isolation: "worktree", run_in_background: true), given the full
dialect-conversion recipe and the font-table-verbatim-copy lesson above
explicitly in its own prompt (never assumed inherited), each mechanically
converting its own batch’s real Vircon32 source into standard C and
reporting back exact addGame()/games.h lines. The orchestrating
session integrated one batch at a time (copy files, add declarations/
registrations, rebuild, spot-verify, commit) to avoid parallel-edit
conflicts on the shared menuGameList.c/games.h files - eight batches
total to reach all 99.
menuGameList.c’s own registration order was found to have drifted
from the sibling Vircon32 project’s own real order (ad-hoc batch-
integration order instead) - caught by direct user review (“make sure the
addgame calls order at the end match sibling project”) and fixed via a
full rewrite matching the source project’s exact sequence, including all
11 markUnfinished() calls (Bang! Bang!, Save Princesse, MotoCross, No
Name Platform Game, Ralph, MyRPG, DarkShmup, PinBall, Robot, Cruiser,
Community RPG) with their own real reason strings.
A prompt-injection concern was raised and independently self-verified, not silently trusted or dismissed: two separate background agents during the porting batches reported their own scratchpad conversion scripts being silently overwritten mid-task, each accompanied by a system-reminder falsely attributing the change to “the user or a linter.” Both agents refused to comply with any embedded instruction and self-verified their own final output using fresh logic instead of trusting the altered script. Surfaced directly to the user rather than hidden; no forensic root cause was ever found, but no corrupted output made it into the shipped result either, since both agents’ own real deliverables were independently re-verified before integration.
Established the hard way, worth stating explicitly for any future work on this project:
-nd (uncapped framerate, vsync off) for any manual
smoke test or -ms batch-screenshot run - a -ms run without it is
vsync-throttled and can take minutes instead of seconds for all 99
games. Every agent dispatched to do screenshot/build verification work
must be told this explicitly in its own prompt.-ms must capture real, active gameplay for every game, not a
title/menu/difficulty-select screen. The default screenshot script
(screenshotScriptFor() in main.c, a few generic button taps) is a
starting point only - many games need their own tuned tap-count/gap/
held-direction sequence (or a fully custom function) to actually reach
gameplay. screenshotButtonPulse()’s own real root-cause fix (see
main.c’s own comment) widens every simulated press/release phase to 8
real engine frames, comfortably exceeding every game’s own tick
divisor (worst case a 10fps game needs 6), so one clean tap per game
action reliably registers - superseding an earlier workaround that
fired several rapid, narrower taps per action to route around the same
underlying input-sampling-phase problem.-ms’s own batch-capture mode is
a separate case, exempt from this: it drives input via internal
simulated-state functions (sdlBackend_simulate*Frame()), not real OS-
level events, so it works correctly regardless of window focus..gbu conventionThis project’s own naming choice (not inherited from Tinyjoypad_SDL,
which uses .joy) - -gbu CLI flag, writeGbuFiles(), .gbu extension
detection for positional-arg direct-launch. Purely a file-extension/
naming difference; the mechanism (one stub file per registered game, an
external frontend’s own per-game launch target) is otherwise identical to
the sibling’s .joy design.
metadata/screenshots/, metadata/gbu/)A deliberate, direct correction from the user, worth remembering: an
earlier attempt added code to main.c so -ms/-gbu would write
directly into metadata/ (new METADATA_DIR/ensureDir() machinery) -
told directly this was wrong (“it should have been left alone you just
needed to run the binary and copy the files to directory”). Reverted all
of that path/directory-creation code; -ms/-gbu still write to the
current working directory exactly like Tinyjoypad_SDL’s own identical
flags always have. The real 99 screenshots + 99 .gbu files in
metadata/ were generated by manually running the built binary from
inside src/sdl3/build/ and copying the output files into metadata/
by hand - a one-off manual step, not build automation, matching this
project’s own established “no checked-in generator for a one-off asset-
staging step” precedent (see “Thumbnail generation” below for the
identical precedent applied to assets/thumbnails/).
tools/gen_thumbnails.py is a near-verbatim port of Tinyjoypad_SDL’s
own script of the same name/purpose: probes assets/thumbnails/
thumb_00.bmp, thumb_01.bmp, … sequentially (stopping at the first
missing index), embeds each as a C byte array, and writes
assets/thumbnails/thumbnailData.h - a single shared file both
src/sdl3/ and src/sdl2/ #include via their own
target_include_directories() pointing at ../../assets/thumbnails
(that CMake wiring was actually present from this project’s own earliest
scaffolding, unused until the real restructuring below landed).
This shared-file structure was NOT how this project’s own thumbnails
were first implemented - an earlier pass had gen_thumbnails.py crop/
downsample directly from metadata/screenshots/<TITLE>.bmp (the raw
640x360 gameplay captures) and write two separate, fully-duplicated
thumbnailData.h copies straight into src/sdl3/ and src/sdl2/ - it
worked (both ports’ own target_include_directories() already lists
${PROJECT_SOURCE_DIR} itself, so the per-port copy resolved first), but
didn’t match the sibling project’s own real convention. Corrected via a
direct user catch (“normally … tools should generate from
assets\thumbnails like on tinyjoypad_sdl and … assets\thumbnails
contain the bmp files”) - assets/thumbnails/thumb_NN.bmp (99 files, a
crop + point-sample of each real metadata/screenshots/<TITLE>.bmp
capture down to MD_THUMBNAIL_WIDTHxHEIGHT, 256x128, at the real LCD
sub-rectangle within each 640x360 capture - 588x336 at pixel offset
(26,12)) now exist as real, checked-in staging files (that crop step
itself has no checked-in generator, matching the sibling’s own identical
precedent - a one-off manual step, redone by hand if a thumbnail is ever
recaptured), gen_thumbnails.py was rewritten to the sibling’s own exact
probe-and-embed shape, and the two old per-port thumbnailData.h copies
were deleted so the shared file at assets/thumbnails/ is the only one
that exists.
Playdate’s own thumbnails are a genuinely different, separate pipeline
.png files bundled directly in
src/playdate/Source/thumbnails/thumb_NN.png (not compiled in - Playdate
loads them at runtime as real image assets, the same real registration-
order-not-alphabetical convention), generated by src/playdate/tools/
gen_thumbnails.py, its own script (a real enhancement beyond
Tinyjoypad_SDL’s own Playdate port, which has no such generator script
at all - its own thumb_NN.png files are manually staged with no
checked-in tool).A real, easy gitignore trap hit twice while staging these: this
project’s own .gitignore globally ignores both *.bmp and *.png
(everywhere - build output, batch-screenshot captures, etc.), with
per-directory !-exceptions carving out the real, checked-in assets
(assets/**/*.png, assets/**/*.bmp, metadata/screenshots/**/*.bmp,
metadata/screenshots/**/*.png, src/playdate/Source/thumbnails/**/
*.png) - each new checked-in image directory added to this project
needed its own new exception line, and it’s easy to add the files/
directory without remembering the matching exception (caught directly by
the user twice: once for the Playdate agent’s own recovered thumbnails,
once for assets/thumbnails/ itself - “assets should not be ignored it
should be staged”).
GB_GRAY)Real Gamebuino Classic hardware fakes a third shade on its strictly 1-bit
display via a checkerboard dither that flips with the frame counter -
this is GB_GRAY’s own unconditional default rendering on every port
here, computed per-pixel directly in gamebuinoShim.c’s own
gbDrawPixel(), needing zero special handling on any of the three
backends (SDL2/SDL3’s true-color canvas, or Playdate’s real 1-bit panel).
gbRealGrayColor is a real, optional enhancement past real hardware -
when on, GB_GRAY content renders as a genuine, solid, flat gray instead
of the flickering dither:
md_drawColumnGray())
fills exactly the pixels gbGrayBuffer[] marks as real gray content
with a real solid mid-gray, layered directly onto the same canvas
md_drawColumn() already draws onto - both ports default this on
at startup (gamesMain_init(), the one function both ports call and
Playdate never does, so this default reaches only these two ports),
still a live, per-session Button R toggle from there. Verified working
via a fresh capture of FlappyBirdo’s own real GB_GRAY sky band/pipe -
renders as a clean, flat gray, not a flickering dither, immediately.md_drawColumnGray() never actually fired, since this port never set
gbRealGrayColor at all - the real per-pixel checkerboard dither was
judged sufficient on a genuine 1-bit panel). Reworked on direct user
request (“on playdate it should draw using color pattern byte pattern
any color can be a pattern when drawing things”) to use the real
Playdate SDK’s own native pattern-fill mechanism instead: LCDColor can
be either a solid color or a pointer to a real LCDPattern
(typedef uint8_t LCDPattern[16] - 8 rows of bitmap + 8 rows of mask,
confirmed directly from the installed SDK’s own pd_api_gfx.h, not
guessed) - kColorGrey, a real named pattern constant with the exact
byte content given directly by the user (alternating 0b10101010/
0b01010101 bitmap rows, full 0xFF mask - the same real checkerboard
gbDrawPixel()’s own dither formula already produces), is now used via
(LCDColor)kColorGrey at the real Playdate-native fill call site.
md_drawColumn()’s own run-batching draw loop was factored out into a
shared drawRunOfPixels(col, page, value, LCDColor color) helper so
both the plain-black path and this gray path go through the same
color-agnostic mechanism. Playdate’s own init() now also sets
gbRealGrayColor = true at startup, matching the SDL default. Verified
visually via a real before/after Simulator screenshot comparison
(Copter’s own unconditional GB_GRAY city skyline) - the “after” version
shows a visibly finer, genuine device-pixel-level native checkerboard
versus the “before” version’s coarser, blockier per-logical-pixel one -
and independently confirmed working by the user’s own live testing.Thumbnail generation now deliberately captures each port’s own real
visual truth, not one shared image for both - per direct request. Since
Playdate’s own kColorGrey pattern-fill is visually identical to the
checkerboard dither (gray off), never the flat solid gray SDL2/SDL3
show with it on (their own default), a genuinely accurate Playdate
thumbnail needs a gray-off source capture, while SDL’s own thumbnails
(and the README’s own screenshot display) correctly keep using a gray-on
capture matching their own real default. A new -gray <0|1> CLI flag
(main.c, both SDL ports, via a new gamesMain_setRealGrayColor()
in gamesMain.h/.c) overrides gamesMain_init()’s own default for
exactly this purpose - main.c never reaches into gamebuinoShim.c’s own
gbRealGrayColor global directly, matching this project’s own established
TU-boundary discipline. src/playdate/tools/gen_thumbnails.py’s own
header comment documents the exact regeneration recipe (-ms -ns -nd
-gray 0 into a scratch directory, crop from there, discard the temporary
capture afterward - matching this project’s own “no checked-in generator
for a one-off asset-staging step” precedent).
Built by a background agent, twice - see “A lost-agent recovery” below for why. Genuinely different from SDL2/SDL3 in shape, not just API:
src/playdate/main.c, no separate “backend”
file the way src/sdl3/src/sdl2 split platform code from main.c) -
Playdate has no CLI/window/event-queue surface to justify the split,
matching Tinyjoypad_SDL’s own identical single-file Playdate port
shape.gameworld/menu.c’s own
menu_update() and gamesMain.c’s own dispatch/quit-dialog are NOT
reused as-is (both assume a real 640x360 canvas and the BIOS font) -
this port’s own menuUpdate() still walks the exact same menu.h Game
table (menu_getGame()/gameCount, populated by the unmodified
menuGameList.c), just rendered with Playdate’s own real system font,
paginated, numbered, with a live gameplay thumbnail + author/info
caption for the selected game.GAME_SCALE-based rendering, not pd->display->setScale()/
setOffset() - deliberately rejected (its own legal values are limited
to 1/2/4/8, and even a legal one doesn’t transform draw-call coordinates
the way SDL’s logical-presentation feature does - it re-samples a small
top-left region of the always-400x240 framebuffer instead). Manual
scaling in md_drawColumn() hits this port’s own real best fit
(336x192) directly with no such restriction.gbSetFrameRate()
clamps to [1,60] only because Vircon32/SDL both need to sub-sample a
fixed-rate engine tick, a design constraint Playdate doesn’t share). A
new shared-shim primitive, gbSetEngineFrameRate()/gbGetFrameRate()/
gbEngineFrameRate (defaulting to MD_FRAMES_PER_SECOND, 60, so
SDL2/SDL3 behavior is completely unchanged), lets this port call the
real pd->display->setRefreshRate() every time the active game’s own
requested frame rate changes (including at launch), so Playdate’s own
native hardware-level vsync/timing does the pacing instead of a manual
accumulator sub-sampling a fixed tick. Verified empirically: Catcher’s
own real gbSetFrameRate(10) call visibly dropped the Simulator’s own
live FPS overlay from 50 to a stabilized 10.Tinyjoypad_SDL’s own
Playdate precedent. Holding A+B+Up+Right together returns directly to
the menu, no prompt.pd->system->addMenuItem()) instead of any face button, confirmed via
the real installed SDK docs, not guessed. The system menu’s final,
real shape (after several direct correction rounds) is exactly 3 items,
in this order: “Button C” (fires the equivalent of a Gamebuino
Button-C press), “Menu” (returns directly to the game-select menu,
no confirmation), and a Pixel Grid checkmark toggle. No mute item -
Playdate already exposes its own native system-level Volume control, so
a redundant in-app one was removed on direct request.pd->file
data-store API - verified end-to-end (a highscore correctly survived a
real title -> game-over -> retry cycle in the Simulator).cd src/playdate
cmake -B build -G Ninja # Simulator target - produces GamebuinoClassic.pdx
cmake --build build
Requires the official Playdate SDK (PLAYDATE_SDK_PATH env var, or the
CMake config’s own documented fallback location). Device builds use
-DCMAKE_TOOLCHAIN_FILE=$PLAYDATE_SDK_PATH/C_API/buildsupport/arm.cmake
with a separate build directory (see .github/workflows/build.yml’s own
playdate matrix entry for the exact real CI invocation).
The first Playdate-porting background agent was killed mid-task when the
IDE hosting this session was restarted - its task ID became untrackable
and its own output file was never written to, confirmed via direct
verification (not assumed) before treating the work as lost. Its own
git worktree, however, was still on disk with real, substantial,
well-reasoned progress: a 1156-line main.c (with real architectural
decisions already made and documented in its own header comments -
GAME_SCALE reasoning, the from-scratch menu design, the no-dialog
choice), a complete CMakeLists.txt, pdxinfo, tools/gen_thumbnails.py,
and all 99 real gameplay thumbnails already generated. Recovered by
copying that content into the main tree and committing it as real WIP
(safe to do, since this project’s own git history gets squashed to one
commit as a final step anyway), then dispatching a second background
agent explicitly instructed to read and continue that exact recovered
work rather than restart - which is what produced the finished, verified
port described above. The dead agent’s own orphaned worktree/branch (and,
later, every other already-integrated batch’s own stale worktree/branch)
were cleaned up via git worktree remove --force --force +
git branch -D once confirmed fully redundant with what had already
landed on master.
.github/workflows/build.yml - a full SDL2/SDL3/Playdate build matrix,
modeled directly on Tinyjoypad_SDL’s own workflow of the same name and
shape (same per-runsontype step structure for Windows/mingw, Linux,
macOS, and the Playdate device build), adapted for this project’s own
binary names and asset layout (metadata/gbu instead of metadata/joy
files) - the appimage lane was dropped (this project ships no
build-appimage.sh). The macOS icon-building steps (sips/iconutil,
building a real .icns from metadata/icon.png into the .app bundle’s
own Info.plist) and every LICENSE.txt copy step were initially
dropped too, for lack of a real icon.png/LICENSE.txt at the time -
both were added once the user supplied a real metadata/icon.png and a
real LICENSE.txt existed (see “License” below), restoring this
workflow to the sibling’s own exact shape in both respects.
A required field, per direct request, marks which of the 23 matrix
entries actually gate the workflow’s own pass/fail status
(continue-on-error: $) - every entry still runs
and reports regardless. 7 are required: one canonical build per platform
per SDL version (Windows x86_64, Ubuntu 24.04 x64, macOS 14 arm64, ×2 for
SDL2/SDL3) plus the one Playdate device build. Every other variant
(32-bit Windows, older Ubuntu, ARM runners, extra macOS versions/arches)
is real coverage kept non-blocking, since they all build the identical
source against a different compiler/libc/arch, not a different code path.
Copied the real, verbatim GPLv3 LICENSE.txt directly from either
sibling project (confirmed byte-identical between Tinyjoypad_SDL’s and
gamebuino_classic_vircon32’s own copies before copying) - the same real
reasoning applies here: several shipped games (Agaruino, CrazyCar,
Shufflepuck Cafe, Taquin, and others) are themselves GPLv3, making the
combined executable a GPLv3 combined work regardless of this project’s
own new code’s own license preference. See README.md’s own “License”
section for the one known, unresolved concern (Firemen’s own upstream
LICENSE.md says GPLv2-only, no “or later” clause, a real compatibility
question inherited unmodified from the source Vircon32 project).
src/gameworld/games/
(shared, unmodified across ports), menuGameList.c registration order
matching the source Vircon32 project exactly, all 11
markUnfinished()-flagged games carrying their own real reason text.src/sdl3/) - the primary, most-exercised port: window/
renderer/persistent-canvas video, multi-voice square-wave audio,
file-backed EEPROM, real gamepad+keyboard input via CInput, the
quit-confirmation dialog, the real-gray-color toggle (on by default),
a real pixel-grid overlay, all CLI flags (-w/-h/-f/-ns/-fps/
-nd/-s/-list/-g/-ms/-gbu/.gbu positional/-?/-help),
the fixed-60Hz logic accumulator.src/sdl2/) - functionally identical to SDL3, verified
directly (same keybinds, same CLI, same behavior across all 99 games).src/playdate/) - complete: input (A/B direct, C via
system menu), audio, EEPROM via pd->file, the native menu with real
thumbnails, real per-game frame-rate sync, and native-LCDPattern-based
GB_GRAY rendering. See “The Playdate port” above for the full design.assets/thumbnails/thumbnailData.h for SDL2/SDL3, real bundled .png
files for Playdate).README.md/CLAUDE.md/LICENSE.txt - all real and current as of
this section’s own last update.master, before the very last step below.initial version and
nothing else - including verifying no stray agent worktrees/branches
remain (git worktree list/git branch -a should show only master)
before considering that step done. Do this only as the truly last
action, not before.