beenadoc
An attempt at understanding internals of the Sega Toys’ Advanced Pico Beena by reversing the BIOS and a few games. (WIP)
TOC
Tooling
- Ghidra loader + FID database containing labels for BIOS and common library functions;
- CPU emulator used to verify various attempts at homebrew;
- MAME driver + artwork for a custom crosshair and settings icons (copy directories over to
crosshairpath
andartpath
defined in yourmame.ini
);
Debug features
JTAG
The BIOS dumping writeup has instructions on how to connect to this debug port, including interfacing with telnet. However, you can instead interface with GDB, which allows for more convenient features such as scripting.
On a Debian-based Linux distro, you can install gdb-multiarch
which is compiled with support for other architectures beyond what your host uses. In our case, we want to connect to an ARMv4t via OpenOCD, which exposes a GDB server by default on port 3333:
gdb-multiarch \
-ex='set architecture armv4t' \
-ex='set endian big' \
-ex='target remote localhost:3333'
Test mode
Most games contain a hidden test mode that can be activated by the same inputs. There are at least 3 variants, each identified by the ROM header’s library date (the release date is usually a few months later):
- Before 2005-10-19: Test results are encoded in the background color, by updating the first palette entry. In some cases there are 2 distinct palettes and tilesets for graphical marks (e.g. Kazoku Minna no Nouryoku Trainer uses addresses
0x80074dac
and0x80074dbe
for test passed,0x80074a1c
and0x80074a2c
for test failed); - 2005-10-19 up to 2007-05-28: In addition to the background color update, ROM header fields are read and displayed, followed by test results under “Test Cartridge” (e.g. Soreike! Anpanman Hajimete Kaketa yo! Oboeta yo! Hiragana Katakana uses addresses
0x80748f14
and0x80746892
for the font’s palette and tileset); - 2007-05-28 and after: MIDI audio plays when test mode is successfully activated. Depending on the game, it can include a SD-Card test (e.g. Anpanman no Waku Waku Game Oekaki) or peripheral specific tests (e.g. Cars 2 Racing Beena: Mezase! World Champion!).
Anpanman no Waku Waku Game Oekaki
The following video capture shows how the SD-Card reader is tested:
- Sound plays at the start;
- Reader is disconnected, its status remains blank after cartridge checksum is computed;
- Reader is connected, its led flashes, status becomes
OK
; - We can repeat the process by disconnecting it again, status is blanked;
On the card, a file is written under /BEENA/S100031/TESTDATA.BIN
.
Hex dump:
00000000: 8000 0040 8000 00a0 8000 00b0 8000 0060 ...@...........`
00000010: 0000 0000 0080 0000 ........
These appear to be ROM header fields displayed on screen (except for what we call “product_name”):
0x80000040
: product_id0x800000a0
: app_date0x800000b0
: lib_date0x80000060
: product_name0x00000000
: flash_size0x00800000
: mask_size
Cars 2 Racing Beena: Mezase! World Champion!
Courtesy of André Coelho (@hitmanmcc), also described how the peripheral is tested:
As you interact with the buttons the table below shows its status and once you’ve tested all of the functionality of the wheel you get a green screen indicating everything is OK.




Instructions
To activate test mode:
- Insert a game on the console;
- Flip all pages of the Storyware to expose all page sensors, and cover the sensors for pages 1, 3, and 5 with some opaque objects. Here’s a crude diagram of how that should look:
- Hold down the left red button;
- Power on / Reset the console;
If all goes well, instead of the game showing the logo animations, it will stay locked in test mode until the PSU connector is removed and then reconnected (assuming there’s no batteries connected).
Unfortunately, this method is somewhat unreliable, likely due to a very short time window between powering on the console, stabilizing the page sensors, and running test mode input checks.
This can be workaround by triggering a soft reset via the JTAG port, which involves pulling strong-low on pin 15 (nSRST). For example, you can short nSRST to one of the ground pins:

Alternatively, if you are using GDB: monitor soft_reset_halt
.
Otherwise, make sure that pages are being identified as expected. For example, if you open page 3 (i.e. 3 sensors are exposed), and you cover the leftmost sensor with an opaque object, page 2 should be loaded. If you also cover the 2nd leftmost sensor, page 1 should be loaded. If you uncover the leftmost sensor, but keep the 2nd leftmost covered, then page 3 should be loaded.
Technical overview
All variants perform the same tests:
- CRC32 checksum computed over the game’s ROM data after the header, compared against the expected checksum stored in the ROM header at offset
0xd0
; - Memory integrity checks consisting of reading a string from RAM to a cache, then writing those cached bytes at the same RAM address, which is then compared with the expected string.
Decompilation example:
iVar1 = check_crc();
if (iVar1 != 1) {
sVar2 = strlen("Nana");
len_nana = sVar2 + 1;
__s2 = (char *)((int)&DAT_c00c6d00 - len_nana);
strncpy(__s2,"Nana",len_nana);
iVar1 = check_memcache_read(__s2,0x800 - len_nana,len_nana);
if ((iVar1 != 1) && (iVar1 = read_500300bc_pre1(__s2,0x800 - len_nana,len_nana), iVar1 != 1)) {
uVar3 = strcmp("Nana",__s2);
uVar3 = (-uVar3 | uVar3) >> 0x1f;
goto ret;
}
}
Input checks
All addresses in this section are from Soreike! Anpanman Hajimete Kaketa yo! Oboeta yo! Hiragana Katakana.
Checks to enable test mode are done in FUN_800291a0()
(here named is_test_mode_active
). It is called at the beginning of FUN_80000f40()
(main
), and the result is used to conditionally call FUN_80020af8()
(test_mode
). These calls happen before the palettes and tilesets of the logo screens are loaded in FUN_8001ffec()
:
int main() {
// ...
iVar1 = is_test_mode_active();
if (iVar1 == 0) {
test_mode();
}
// ...
FUN_8001ffec();
// ...
}
Pad buttons are directly read from I/O port address 0x50020034
(here named IO_BUTTONS
). Page sensors are then read by callbacks run in a loop until the effective page is set. Afterwards, each page state is checked as well:
undefined8 is_test_mode_active(void) {
byte bVar1;
int iVar2;
int uVar3;
PageState *iVar3;
long in_lr;
iVar3 = read_volatile_4(IO_BUTTONS);
/*
* Check if the left red button is held down.
*
* This value encodes bits `XXXX XXAB CDEF GHIJ`, where:
* - `X`: unused,
* - `ABCDE`: red + directional buttons of one pad, and
* - `FGHIJ`: red + directional buttons of the other pad.
* They can be pressed (= 0) or released (= 1).
*/
if (iVar3 == 0x1ff) {
/*
* `PageState` is an 8 byte structure:
* - `0x0`: effective_page: the last page open,
* which will be loaded by the game;
* - `0x1`: is_effective_page_set: flag that indicates
* the value in `effective_page` is ready to be read (= 1);
* - `0x2..0x7`: flags for pages 1..6 to indicate if
* they are closed (= 1) or open (= 0).
*/
iVar3 = (PageState *)get_work_page_read();
bVar1 = iVar3->is_effective_page_set;
while (bVar1 == 0) {
run_callbacks();
bVar1 = iVar3->is_effective_page_set;
}
uVar3 = 0; // Activate test mode
// Are pages 1, 3, and 5 not closed, or pages 2, 4, and 6 not open?
if ((iVar3->page1 + iVar3->page3 + iVar3->page5 != 3) ||
(iVar3->page2 + iVar3->page4 + iVar3->page6 != 0)) {
uVar3 = 1; // Skip test mode
}
}
else {
uVar3 = 1; // Skip test mode
}
return CONCAT44(in_lr,uVar3);
}
TV Ocha-Ken
Plug and play TV game 「テレビとお茶札 お茶犬『ほっ』と生活」 also has a test mode: Hold down A B C, then power on the system, release all buttons, and press B 3 times.
Video update tints
All addresses in this section are from Soreike! Anpanman Hajimete Kaketa yo! Oboeta yo! Hiragana Katakana.
An unreferenced variable at address 0xc00c7cc7
(named w_debug_tint
) is used to toggle a debug feature, likely used to track different phases of a game’s video update callback:
undefined4 do_callbacks(void) {
bx_r3_chain_bind_callback();
read_40000010();
// (1)
if (w_debug_tint != '\0') {
write_volatile_4(DAT_40000030,0x1f00); // green tint
written_to_40000030 = 0x1f; // red tint
w_video_regs_dirty = 1;
}
// (2)
bx_r3(&w_wait_not_video_dirty_cb,&w_video_dirty,extraout_r1);
if (w_debug_tint != '\0') {
write_volatile_4(DAT_40000030,0x1f0000); // blue tint
written_to_40000030 = 0x1f0000; // blue tint
}
bx_r3_chain_bind_callback();
// (3)
return in_lr;
}
The following GDB commands can be run to explore this logic:
set *(char**)(0xc00c7cc4) = 0x01000001
hbreak *0x8002c74a # (1)
hbreak *0x8002c768 # (2)
hbreak *0x8002c784 # (3)
Shown in this video capture:
- Stopped at breakpoint (1);
- Debug variable is set;
- Execution continued at (2), video callback is run, green tint is set;
- Execution continued at (3), callbacks in
bx_r3_chain_bind_callback()
are run, blue tint is set; - Another iteration is done, note that at the 2nd blue tint, a frame was advanced;
- Breakpoints are disabled, execution is continued. Note some red tints are applied for a short time at the beginning;
Red tints can also be observed when switching pages.
Intro skip
A few games check for an input sequence at boot that skips the initial warning screen and the Beena / Sega Toys logo animations screen. Hold down the left pad’s up + down buttons, then power on the console.
Decompilation example:
dVar1 = read_volatile_4(IO_BUTTONS);
if (dVar1 != 0x39f) { // 0b0_00000_11100_11111
// Show first screens...
}
Confirmed games:
- Oshare Majo Love and Berry - Cute ni Oshare
- Shoku Iku Series 1 Soreike! Anpanman - Sukikirai Nai Ko Genki na Ko!
- Soreike! Anpanman Hajimete Kaketa yo! Oboeta yo! Hiragana Katakana - Gojuuon Board Kinou-tsuki
Input parsing
All addresses in this section are from Soreike! Anpanman Hajimete Kaketa yo! Oboeta yo! Hiragana Katakana.
Pad buttons
There are two functions involved: FUN_80029388
stores a negated reading from I/O port address 0x50020034
(here named IO_BUTTONS
):
void read_io_buttons_prev(void) {
uint uVar1 = read_volatile_4(IO_BUTTONS);
work_io_btn_prev = work_io_btn_prev | ~uVar1;
return;
}
Then FUN_800293a0
will use that value along with another reading, to distinguish between a button being held, pressed, or released. We see that the total bits read are 0x3ff
, and each pad corresponds to 0x1f
bits:
// Take read values and compute masks
btns = read_volatile_4(IO_BUTTONS);
btns_press_mask = (work_io_btns_prev | ~btns) & 0x3ff;
btns_press = (work_io_btns_prev_mask ^ btns_press_mask) & btns_press_mask;
btns_press_neg = (work_io_btns_prev_mask ^ btns_press_mask) & ~btns_press_mask;
// Store pad1 state
btns1_hold = (work_io_btns_prev | ~btns) & 0x1f;
btns1_press = btns_press & 0x1f;
btns1_release = CONCAT11((char)btns_press_neg,work_io_btns1_state.field3_0x3) & 0xffff1fff;
work_io_btns1_state = btns1_hold << 0x18 | btns1_press << 0x10 | btns1_release;
// Store pad2 state
btns2_press = (byte)(btns_press >> 5);
btns = (uint)CONCAT11((char)(btns_press_mask >> 5),btns2_press) << 0x10;
btns_press_neg = CONCAT11((char)(btns_press_neg >> 5),work_io_btns2_state.field3_0x3) & 0xffff1fff;
work_io_btns2_state = btns | btns_press_neg;
// Store both pad1 and pad2 state
work_io_btns1n2_state.hold = (byte)(btns >> 0x18) | (byte)btns1_hold;
work_io_btns1n2_state.press = btns2_press | (byte)btns1_press;
work_io_btns1n2_state.release = (byte)(btns_press_neg >> 8) | (byte)(btns1_release >> 8);
// Prepare previous values for next time
work_io_btns_prev_mask = btns_press_mask;
work_io_btns_prev = 0;
return in_lr;
Unfortunately the decompilation doesn’t come out very clean, so let’s look at some examples to better understand what gets stored in these state variables after finishing parsing. These variables are layed out in memory as follows:
0xc00d5448 = work_io_btns_prev
0xc00d544c = work_io_btns1_state
0xc00d5450 = work_io_btns2_state
0xc00d5458 = work_io_btns1n2_state
Let’s start by pressing the red button in pad1:
# read 0x1ff
0xc00d5448 00 00 00 00 10 10 00 00 00 00 00 00 10 10 00 00
# repeat
0xc00d5448 00 00 00 00 10 00 00 00 00 00 00 00 10 00 00 00
Both fields “hold” and “press” were set, in both pad1 and pad1n2. On the second time, only “hold” was set. Let’s try one of the directional buttons in pad2:
# read 0x1fe
0xc00d5448 00 00 00 00 10 00 00 00 01 01 00 00 11 01 00 00
# repeat
0xc00d5448 00 00 00 00 10 00 00 00 01 00 00 00 11 00 00 00
Ok, so pad1n2 now has the accumulated bits from both pads. If we press all directional buttons in pad2…
# read 0x1f0
0xc00d5448 00 00 00 00 10 00 00 00 0f 0e 00 00 1f 0e 00 00
# repeat
0xc00d5448 00 00 00 00 10 00 00 00 0f 00 00 00 1f 00 00 00
Seems like the “hold” field in pad1n2 is saturated with 5 bits. If we also press the red button in pad2…
# read 0x1e0
0xc00d5448 00 00 00 00 10 00 00 00 1f 10 00 00 1f 10 00 00
# repeat
0xc00d5448 00 00 00 00 10 00 00 00 1f 00 00 00 1f 00 00 00
Same value, and if we press all buttons…
# read 0x000
0xc00d5448 00 00 00 00 1f 00 00 00 1f 00 00 00 1f 00 00 00
# repeat
0xc00d5448 00 00 00 00 1f 00 00 00 1f 00 00 00 1f 00 00 00
Now let’s try releasing one of the directional buttons in pad1:
# read 0x100
0xc00d5448 00 00 00 00 17 00 08 00 1f 00 00 00 1f 00 08 00
# repeat
0xc00d5448 00 00 00 00 17 00 00 00 1f 00 00 00 1f 00 00 00
Now field “release” gets updated in pad1 and pad1n2 instead of field “press”. If we press it again and release the red button:
# read 0x200
0xc00d5448 00 00 00 00 0f 08 10 00 1f 00 00 00 1f 08 10 00
# repeat
0xc00d5448 00 00 00 00 0f 00 00 00 1f 00 00 00 1f 00 00 00
That’s pretty much it. But how do we know that the red button is the 5th most significant bit? It’s an educated guess…
In page 0 (i.e. Storyware closed), an intro sequence controlled by FUN_800019ec()
shows a title and 2 strings at a time while a song is playing. We see pointers to the title address 0x8002f144
and the strings table address 0x8002f18c
.
The sequence happens in a while loop, which has these exit conditions:
while( true ) {
uVar1 = get_page();
if ((uVar1 & 0xff) - 1 < 6) {
// ...
return in_lr;
}
uVar12 = intro_seq_check_inputs();
if ((int)uVar12 - 1U < 4) break;
// ...
}
In intro_seq_check_inputs()
, the pad state for pad1n2 is returned by get_io_set_hold(3)
, followed by a check for the held button at bit 5:
io_set_btns1n2 = (byte *)get_io_set_hold(3);
psVar1 = (short *)FUN_8002a114(3);
page = (char *)get_work_page_read();
iVar4 = -1;
iVar5 = -1;
if (((((io_set_btns1n2->hold & 0x10) != 0) && (*(char *)(psVar1 + 2) == '\x02')) &&
(iVar5 = iVar4, *(char *)((int)psVar1 + 9) != '\0')) &&
((*page == '\0' && (page[1] != '\0')))) {
pwVar3 = &WORD_80055d48;
iVar2 = 0;
do {
if ((((int)(short)*pwVar3 <= (int)*psVar1) &&
((int)*psVar1 <= (int)(short)pwVar3[2] + (int)(short)*pwVar3)) &&
(((int)(short)pwVar3[1] <= (int)psVar1[1] &&
(iVar5 = iVar2, (int)psVar1[1] <= (int)(short)pwVar3[1] + (int)(short)pwVar3[3]))))
break;
iVar2 = iVar2 + 1;
pwVar3 = pwVar3 + 4;
iVar5 = iVar4;
} while (iVar2 < 6);
}
return CONCAT44(in_lr,iVar5 + 1);
If the button at bit 5 is not hold (!= 0), we enter the if branch, otherwise intro_seq_check_inputs()
returns 0, causing the while loop in the caller function to exit. There’s also a check for some other input, and if the page still remains at value 0. It makes sense that this intro sequence stop playing if the player flips to another page, or if some input is done. But it’s unlikely that a single specific directional button would cause this behaviour and not the others buttons. Therefore, bit 5 is likely the red button.
Page sensors
There are two functions involved: FUN_80029498
directly stores two 4-byte readings from I/O port addresses 0x5002002c
and 0x50020030
(here named IO_PAGE_U4_1
and IO_PAGE_U4_2
):
void read_io_page(void) {
work_io_page_u4_1 = read_volatile_4(IO_PAGE_U4_1);
work_io_page_u4_2 = read_volatile_4(IO_PAGE_U4_2);
return;
}
We can already tell how it deviates from the Pico parsing logic, which directly read 6 bits that encoded each page on/off from a single address.
On FUN_800294bc
7 bytes are parsed, 6 are used. The last 0x24 read values are kept for each byte, ranged in 0x00..0xff. These seem to be raw readings from the photodiode sensors.
// Constrain index to store up to 0x24 read values
if (work_io_page_i < 0x23) {
page_i = work_io_page_i + 1;
}
else {
page_i = 0;
}
// Store each read byte from 1st address in the corresponding index
io_page = work_io_page_u4_1;
work_io_page_raw_val[0] = (byte)((uint)work_io_page_u4_1 >> 0x18);
work_io_page_i = page_i;
work_io_page_raw_0x24_vals[page_i] = work_io_page_raw_val[0];
work_io_page_raw_val[1] = (byte)((uint)io_page >> 0x10);
work_io_page_raw_0x24_vals[page_i + 0x24] = work_io_page_raw_val[1];
work_io_page_raw_val[2] = (byte)((uint)io_page >> 8);
work_io_page_raw_0x24_vals[page_i + 0x48] = work_io_page_raw_val[2];
work_io_page_raw_val[3] = (byte)io_page;
work_io_page_raw_0x24_vals[page_i + 0x6c] = work_io_page_raw_val[3];
// Store each read byte from 2nd address in the corresponding index
io_page = work_io_page_u4_2;
work_io_page_raw_val[4] = (byte)((uint)work_io_page_u4_2 >> 0x10);
work_io_page_raw_0x24_vals[page_i + 0x90] = work_io_page_raw_val[4];
work_io_page_raw_val[5] = (byte)((uint)io_page >> 8);
work_io_page_raw_0x24_vals[page_i + 0xb4] = work_io_page_raw_val[5];
work_io_page_raw_val[6] = (byte)io_page;
work_io_page_raw_0x24_vals[page_i + 0xd8] = work_io_page_raw_val[6];
Each val is normalized into range 0x0..0x7, then accumulated on work_io_page_parsed_val
. It’s then compared to thresholds: val < 0x38
is page on (= 0), val > 0x48
is page off (= 1).
// Accumulate normalized values
puVar4 = work_io_page_raw_0x24_vals;
page_acc_i = 0;
do {
acc = 0;
j = 0;
do {
pbVar1 = puVar4 + j;
j = j + 1;
acc = acc + (uint)*pbVar1;
} while (j < 0x24);
bVar2 = ext_21c(i,0x24); // bVar2 = (int) i / 0x24;
acc = page_acc_i + 1;
(&work_io_page_parsed_val)[page_acc_i] = bVar2;
puVar4 = puVar4 + 0x24;
page_acc_i = acc;
} while (acc < 7);
// Compute thresholds based on the lowest accumulated page value
page_val = 0xff;
page_i = 0;
do {
if ((&work_io_page_parsed_val)[page_i] < page_val) {
page_val = (uint)(&work_io_page_parsed_val)[page_i];
}
page_i = page_i + 1;
} while (page_i < 7);
page_val_added = page_val + 0x48;
work_page_val_off = (byte)page_val_added;
if (0xff < page_val_added) {
work_page_val_off = 0xff;
}
work_page_val_on = (char)page_val + 0x38;
if (0xff < page_val_added) {
work_page_val_on = 0xff;
}
This function is called multiple times, and after 0x12 iterations, a flag is set to finally update the effective work_page_read
value.
page_i = 0;
do {
// If a threshold was passed, then update page state
if (work_io_page_read_val[page_i] == '\0') {
if (work_page_val_off < (&work_io_page_parsed_val)[page_i]) {
uVar3 = 1;
[page_i]=X:
work_io_page_read_val[page_i] = uVar3;
}
}
else if ((&work_io_page_parsed_val)[page_i] < work_page_val_on) {
uVar3 = 0;
goto [page_i]=X;
}
// Compute effective page = last open page
page_i = page_i + 1;
if (5 < page_i) {
if (work_io_page_read_val[5] == '\0') {
page_read = 6;
}
else if (work_io_page_read_val[4] == '\0') {
page_read = 5;
}
else if (work_io_page_read_val[3] == '\0') {
page_read = 4;
}
else if (work_io_page_read_val[2] == '\0') {
page_read = 3;
}
else if (work_io_page_read_val[1] == '\0') {
page_read = 2;
}
else if (work_io_page_read_val[0] == '\0') {
page_read = 1;
}
else {
page_read = 0;
}
work_is_page_read_set = false;
if (work_prev_page_read == page_read) {
if (work_prev_page_i < 0x12) {
work_prev_page_i = work_prev_page_i + 1;
}
else {
work_is_page_read_set = true;
}
}
else {
work_prev_page_i = 0;
work_prev_page_read = page_read;
}
if (work_is_page_read_set) {
// We have 0x12 readings, update effective page with computed value
work_page_read = (byte)page_read;
}
return in_lr;
}
} while( true );
We can also look at some examples to see how page variables change. We can try the pattern used for the test mode (recall that the byte at 0xc00d5464
isn’t parsed, and the byte at 0xc00d5467
is parsed but unused when computing the effective page):
set *(char**)(0xc00d5460) = 0xff00ff00
set *(char**)(0xc00d5464) = 0x11ff0022
On the 1st iteration, we see the raw values stored starting at 0xc00d556c
, with some temporary values for each page increasing with value 0x07:
0xc00d5460 ff 00 ff 00 11 ff 00 22 00 ff 00 00 00 00 00 00
...
0xc00d5560 00 00 00 00 00 00 00 00 00 00 00 00 ff 00 ff 00
0xc00d5570 ff 00 22 07 00 07 00 07 00 00 48 38 00 00 00 00
On the 2nd iteration, the values continue increasing, and the temporary value for the least significant byte at 0xc00d5467
is now 0x01:
0xc00d5460 ff 00 ff 00 11 ff 00 22 00 ff ff 00 00 00 00 00
...
0xc00d5560 00 00 00 00 00 00 00 00 00 00 00 00 ff 00 ff 00
0xc00d5570 ff 00 22 0e 00 0e 00 0e 00 01 48 38 00 00 00 00
After more than 0x12 iterations, we now have an effective page = 6, stored at 0xc00d5564
:
0xc00d5460 ff 00 ff 00 11 ff 00 22 ff ff ff ff ff ff ff ff
...
0xc00d5560 22 22 22 22 06 01 01 00 01 00 01 00 ff 00 ff 00
0xc00d5570 ff 00 22 ff 00 ff 00 ff 00 22 48 38 00 00 00 00
Now let’s try closing page 6:
set *(char**)(0xc00d5464) = 0x11ffff22
After more than 0x12 iterations, we now have an effective page = 4, since page 5 was already closed:
0xc00d5460 ff 00 ff 00 11 ff ff 22 ff ff ff ff ff ff ff ff
...
0xc00d5560 22 22 22 22 04 01 01 00 01 00 01 01 ff 00 ff 00
0xc00d5570 ff ff 22 ff 00 ff 00 ff ff 22 48 38 00 00 00 00
Data formats
- Palettes:
0x0..0x2
: header = number of entries0x2+2*i..0x4+2*i
: entry i, BGR555
- Bitmaps:
0x0..0x4
: header0x0..0x1
: type =0x80
for uncompressed,0x90..0x9f
for compressed (currently unknown algorithm)0x1..0x3
: number of entries k, where0x80 * k
= bitmap size0x3..0x4
: unknown- Validated as
header[0] & 0xf = (header[1] + header[2] + header[3] - 1) & 0xf
0x4..EOF
: Palette indexes, where 0 is the 1st entry in a global palette, other values reference the bitmap’s corresponding palette