Repository navigation
Adding second screen compatibility for a game
Every game GBA DS knows is one MapProfile entry in src/profiles.c; the fields are documented in src/profiles.h. Adding a game mostly means writing a profile and testing it with the harness. You'll need a host build (README → Building) and the ROM.
There are three kinds of second screen, and a profile can use the parts it needs:
| Kind | Games | What GBA DS does |
|---|---|---|
| Map capture | Castlevanias, Metroids, A Link to the Past | Opens the game's own map on a hidden copy of the game and shows it. |
Screen snapshot (logoOnly + snap) |
Tony Hawk's Pro Skater 2 | Copies a screen the game shows (the goal sheet) to the second screen while it's useful. |
HUD split (logoOnly + hud) |
Mario Kart: Super Circuit | Draws some layers and sprites on the second screen instead of the game screen. |
Every profile also gets a logo for the game list and the second screen.
{
.codes = { "A2CE" }, /* game codes (header offset 0xAC) */
.title = "Castlevania: Aria of Sorrow",
...
},-
codes: the four-letter codes from the ROM header. The harness prints a ROM's code as it loads it. -
listName: optional name for the game list instead of the file name. -
Other regions reuse the USA profile through the
variants[]table at the bottom ofprofiles.c:memShiftis added to every memory address when the region's RAM layout moved (find it by matching the code that reads an address in the two ROMs, or by searching RAM dumps), and apatchfunction changes anything else (an extra language menu at boot, a different title-screen timing).
.logo = { .wait = 1700, .layer = 1, .maxY = 160 },GBA DS boots the game silently, waits wait frames (optionally pressStart then wait2, or a button sequence in steps), and keeps BG layer layer above row maxY, transparent where the layer is. blackIsClear for a layer with a black fill; mask (from tools/make_logo_masks.py) when the logo is painted into the whole title picture. Bump rev when you change a profile's logo, so cached logos are made again.
Find the layer with the harness: wait to the title screen, then layershot title writes each layer alone (title_L0 ... title_L3, title_L4 for sprites). logo NAME runs the real extraction.
The hidden copy is given the live game's state, the map button is pressed, and the result is checked before it's shown.
-
open: the button sequence ({ K_SELECT, 4 }: hold Select 4 frames).open2If/open2Delay/open2press a second button once the map is up, when a condition holds (A Link to the Past zooms out the world map). -
settleMax,openWithin,stableFrames,maxDiff: how long to wait for the map, and when it counts as settled (consecutive frames with at mostmaxDiffchanged pixels, so blinking markers are allowed). -
Checks (all must pass, so a title screen or cutscene is never shown):
-
mem: memory values that hold only while the map screen is open, read on the hidden copy. Find them withramdiff.py(below). -
probes/minProbes/tolerance: screen pixels with a fixed colour while the map is up (the frame or background).bgColor/minForeground: the map isn't empty. -
live: values that must hold on the game you're playing before a capture is tried at all (in gameplay, not in a menu). -
menu: values that mean "back at the title or file menu": the map is forgotten and the logo comes back. -
room: values that change on a room change, which triggers a capture straight away.
-
-
Crop and clean-up:
crop_*(what's shown), and withmap_cropon:tight_*,glyphs(lettering to erase),recolor,maskswithgridX/gridY/gridCell(rebuild the map grid),captureHide(sprites drawn over the map),underRect/underLayers/underHide(pieces of bezel that give way to the map under them) andtab. -
Animation:
animFrames×animStepframes are recorded so markers blink;animCycleandstillPickchoose the frame for Map Animations: Off. -
hideSprites: sprites hidden on the game screen while a map is shown (the Metroids' mini-map). Sprite rules give a screen box, a tile range and a priority (-1: any). -
frame: a border from one of the game's menus, for the generated surround (buttonstepsfrom power-on, then a frame of thicknesstaroundx0,y0–x1,y1on BGlayer).gridBackgroundfills the screen with the map's own grid tile (Metroid).ratecopies text from another screen onto the map (Circle of the Moon's completion rate). -
dimWhenUnavailable: dim the last map while it can't be opened.
For games without a map but with a screen worth keeping (logoOnly = true): snap.show probes (at least showMin, for stable frames) say the screen is up; it's copied to the second screen and stays until one of the revert screens matches. snap.hide leaves sprites out of the copy. controls adds the four-face-button picker (Tony Hawk's).
For games whose HUD is better on the second screen (logoOnly = true). A second renderer receives the same video writes as the game's and draws only the HUD layers and sprites; the game screen draws everything else, so nothing behind the HUD is lost.
-
hud.on: while all match (I/O registers like DISPCNT at0x04000000, or RAM), the split is on.hud.keep: while these match (the pause menu), the last HUD picture stays. -
hud.bgMask(bit 0 = BG0) andhud.objs: what moves. -
hud.layout: while these also match (racing, not results),sign(a box left on the game screen when nothing else of the HUD is beside it),map(scaled up 2× or 1.5×), thebackdrop(BG layers grabbed above rowbackdropRows, dimmed) andmovesapply. -
hud.moves:HudMoveboxes measured each frame and placed by an edge or centre ('L' 'C' 'R'atx,'T' 'M' 'B'aty), on the second screen or withtoGameback on the game screen (Mario Kart's item box and place). -
hud.scene: another scene with its own sprites and moves, with the logo as the backdrop (Mario Kart's podium).
The second screen is a 480×320 canvas (2× GBA pixels), so moved and scaled pieces stay sharp.
build/host/harness ROM OUTDIR SCRIPT runs a ROM headless, frame by frame. One command per line (# comments):
| Command | |
|---|---|
hold KEYS N |
hold buttons for N frames (A, B, L, R, START, SELECT, UP... joined with +; - for none) |
tap KEYS [COUNT] |
press and release (3 frames down, 9 up), COUNT times |
wait N |
run N frames |
shot NAME / seq NAME N STEP
|
screenshot / N screenshots STEP frames apart |
save NAME / load NAME
|
save states in OUTDIR (NAME.ss); GBA DS's own .state files load too when renamed |
layershot NAME |
each BG layer and the sprites alone, transparent as magenta |
peek ADDR LEN / poke ADDR LEN VAL
|
read / write memory |
dumpram NAME |
EWRAM then IWRAM to NAME.ram, for ramdiff.py
|
watch ADDR LEN then diff
|
what changed in a range since watch
|
map NAME |
run the real map capture against the profile (prints why it failed) |
logo NAME / frameatlas NAME
|
run the logo / border extraction |
split BGMASK PRIO / split off / hudshot NAME
|
try a HUD split: BG layers and sprites of one priority |
SDL_VIDEODRIVER=offscreen SDL_AUDIODRIVER=dummy GBADS_WINDOW=1024x1536 GBADS_DEBUG=1 \
build/host/gba-ds --state-dir=/tmp/st --load-state=race.ss --input-script=test.txt ROMScript lines: TICK BUTTONS (A, B+UP, -), TICK SHOT:file.bmp (both screens), TICK TAP:x,y (fractions of the window), TICK QUIT. Check how it looks on both screens before trying it on a device.
-
tools/ramdiff.py --on a.ram b.ram --off c.ram d.ram: addresses whose value is the same in every "on" dump (map open) and different from all "off" dumps (gameplay). Take dumps in several places of the game to narrow it down. -
tools/explore_bits.py a.ram b.ram c.ram ...: bytes whose bits are only ever set across an ordered series of dumps (progress flags, collected items). -
tools/sheet.py OUT.png [--cols N] [--scale S] FILES...: tiles screenshots into a contact sheet. -
tools/ra_mock.py: a stand-in RetroAchievements server on 127.0.0.1:8099, for testing without an account.
- Play to a point where the map can be opened and make a save state in GBA DS; copy it into the harness's OUTDIR as
NAME.ssandload NAME. - With the harness,
holdthe map button,shotuntil the map is up, and note how many frames it takes. -
dumpramwith the map open and closed in a few places;ramdiff.pygives thememcandidates. Check them withpeek. - Pick
probesfrom the screenshots: frame or background pixels that never change. - Write the profile, then
map NAMEin the harness until captures pass, and try it from a title screen and a cutscene too, where captures must fail. - Add
logo, test the other regions (variants), run the app headless, and send it for testing on the device.