Xenon 2

How it works · write-up

Shop screen palette

xenondoc/shop-palette.md · 5 KB · updated 2026-09-17

Same class of bug, same class of fix, as the interstitial/title-screen palette problem (see XENON2.MD §6.2/§8 and SpriteAtlasExporter.UsesInterstitialPalette's own comment): the sprite atlas bakes every sprite to one static RGBA snapshot at pack time, using whichever ST palette happened to be live when the "Masked Sprite" debug window was first opened — normally during gameplay. The shop screen ("Colin's Bargain Basement", the post-level-1 buy/sell panel) uses a different palette once its fade-in completes, so its chrome/hand/alien sprites were being baked with the wrong colors.

Where the shop's palette comes from

ShopScreen_InitAndOpen_FUN_00010520 calls palette_fade_FUN_000088ea(&DAT_0001fb72) (confirmed via ReVa disassembly of the call site) to fade the screen in toward a fixed 16-word table at ST address 0x0001FB72. This is ordinary static data compiled into the game's own binary — not a runtime scratch variable — so, exactly like the interstitial screens' 0x86FA table, it's present and readable in ST RAM at all times, regardless of which screen is currently showing at capture time. No live visit to the shop is needed before capturing.

palette_fade_FUN_000088ea fades from black up through $666,$555,...,$000 steps toward this table (per its own header comment, A2/A1 = pointer to the 16-colour source palette, destination $FFFF8240 = the ST hardware palette registers) — i.e. this is a genuine, standard ST fade-in, and the table is the settled end state once the fade completes.

Raw table (0x0001FB72, 16 words, 0x0RGB format, 0-7 per channel)

Reg Raw word R G B
0 0x0000 0 0 0
1 0x0753 7 5 3
2 0x0742 7 4 2
3 0x0632 6 3 2
4 0x0223 2 2 3
5 0x0334 3 3 4
6 0x0445 4 4 5
7 0x0767 7 6 7
8 0x0556 5 5 6
9 0x0112 1 1 2
10 0x0001 0 0 1
11 0x0200 2 0 0
12 0x0300 3 0 0
13 0x0410 4 1 0
14 0x0750 7 5 0
15 0x0521 5 2 1

Comparison against the gameplay palette

Decoded a real gameplay capture (assets/palette.bin, Hatari's own STRGBPalette snapshot) back into the same 0-7-per-channel form (each byte in that file is a clean multiple of 0x22, i.e. byte = channelValue * 0x22 — confirmed empirically from the dump, not assumed) and compared index by index against the table above:

Registers 4-15 are byte-for-byte identical between the shop and gameplay palettes. Only registers 0-3 differ, and register 0 is black in both — so really just 1, 2, and 3 are genuinely redefined:

Reg Gameplay (R,G,B) Shop (R,G,B) Same?
0 0,0,0 0,0,0 yes
1 1,1,1 7,5,3 no
2 3,2,1 7,4,2 no
3 6,3,1 6,3,2 no (blue channel only, off by 1)
4-15 (12 entries) (same 12 entries) yes, all 12

This lines up with what's visible on screen: registers 1-3 are exactly the tones used for the shop's chrome/border/text elements (the warm tan/orange-brown "orange dashes on grey metal panel" look in the screenshots), while registers 4-15 — the wall-tile/sprite/enemy palette shared with gameplay — are untouched. That also explains why the alien/hand sprites (ordinary SpriteData format, same as gameplay objects, and drawn with colors mostly in the 4-15 range) looked reasonably close to correct even before this fix, while the chrome specifically did not.

The fix

Mirrors InterstitialPaletteDump_SaveToFile/UsesInterstitialPalette exactly:

  • src/xenonRender.c: ShopPaletteDump_SaveToFile reads the 16 words at 0x1FB72, converts each through ST2RGB[] (the same live conversion table STRGBPalette itself is built from), and writes assets/palette_shop.bin. Declared in src/includes/screentrace.h.
  • src/screentrace.c: called from the same one-shot "Masked Sprite" debug-window trigger (DebugWindow_UpdateFromSTLowResBase) as palette.bin/palette_interstitial.bin — no extra manual step needed.
  • hatari_dotnet/Program.cs: reads palette_shop.bin alongside the other two, falling back to the main palette if the file is missing (pre-fix capture).
  • hatari_dotnet/SpriteAtlasExporter.cs: UsesShopPalette(stAddress) gates on a single address range, 0x00012640-0x0001F412 (exclusive) — confirmed contiguous with zero gaps across every shop-specific region (the "shop sprites" SpriteData walk from 0x12640, the UNMASKED_CUSTOM chrome pieces from 0x181AA, and the raw96 alien head/door blocks through 0x1EF12), stopping short of the shared 4x8 font at 0x1F412. SelectPalette now picks interstitial / shop / main palette per record, in that priority order.

Same caveat as the interstitial fix: this is one static snapshot of the settled post-fade-in palette, not a live per-frame reproduction — though unlike the interstitial screens, the shop's own fade is confirmed to settle (not continuously cycle), so a single snapshot is the actual correct steady-state value here, not just "less wrong than gameplay's."