The Lion King: Adventures at Pride Rock is a 1995 game for the lesser-known little sibling of the Sega Mega Drive / Genesis: the Sega Pico.

As it usually goes, one of the developers snuck in an easter egg, as evidenced by some strings in the ROM:


Figuring out the sequence to activate an easter egg is fun on its own, but what grabbed my interest was the relative obscurity of this console. Indeed, we will have the pleasure of dealing with the perils of incomplete emulation.

Tooling

Our guiding light is some documentation apparently unchanged since 2008, when it was released along with the PicoDrive emulator, which we will use in this writeup.

PicoDrive bundles a CPU emulator with some debug logging, and… that’s it. Over the years, other emulators added support for the Sega Pico, but none introduced an actual debugger.

Alright, let’s roll up our sleeves…

Ghidra loader

Like other consoles, I/O is memory mapped. This and other documented maps can be automatically set with a custom loader, enabling us to follow I/O data references with labelled addresses.

Luckily, the Sega Pico shares the same CPU as the Sega Mega Drive, and there’s already a nice loader for it that we can use as a base. In addition, a nice tutorial outlines the basics of identifying the ROM, loading the CPU, and working with memory regions.

Here’s what I cobbled together.

Instruction coverage

A lot of functions won’t be automatically disassembled due to lack of cross-references. But we should be able to figure out some by taking an instruction trace log, then use a script to disassemble and highlight all instructions that were executed at runtime. This will also help us in understanding which branches weren’t taken at runtime, some will be related to the easter egg.

Ok, let’s enable this tracing. By default, PicoDrive will compile the Fame CPU emulator for the PC’s x86 architecture, as seen in the Makefile:

ifeq "$(ARCH)" "arm"
use_cyclone ?= 1
# ...
else
use_fame ?= 1
# ...
endif

Then on ./platform/common/common.mak:

ifeq "$(use_fame)" "1"
DEFINES += EMU_F68K
SRCS_COMMON += $(R)cpu/fame/famec.c
endif

Attempt #1

At first, I was under the impression that the program counter would already be logged at each step by enabling these statements under ./cpu/fame/famec.c:

+#define FAMEC_DEBUG 1
 #ifdef FAMEC_DEBUG
        printf("PC: %p\n",PC);
        printf("BasePC: 0x%08x\n",BasePC);
 #endif

Turns out that this logged only some of the instructions, leaving gaps in the addresses, even when there weren’t branches or other instructions that modify control-flow. While these gaps weren’t much of an issue for disassembling, they would still lead to some missing coverage highlighting.

To understand what’s happening, recall that a CPU iterates through instructions in a fetch-decode-execute loop. Searching for ctx->pc, to see where the program counter is updated, leads us to fm68k_emulate() in ./cpu/fame/famec.c, where we see that the processing loop will run for the provided number of cycles:

int fm68k_emulate(M68K_CONTEXT *ctx, int cycles, fm68k_call_reason reason) {
    // ...
#ifdef FAMEC_DEBUG
	printf("PC: %p\n",PC);
#endif
    // ...
famec_Exec:
    NEXT
    // ...
    if (cycles_needed != 0) {
     // ...
        if (ctx->io_cycle_counter > 0) {
            goto famec_Exec;
        }
    }
    // ...
famec_End:
    ctx->sr = GET_SR;
    ctx->pc = GET_PC;

#ifdef FAMEC_DEBUG
	printf("PC: %p\n",PC);
#endif

    return cycles - ctx->io_cycle_counter;
}

Macro NEXT contains the actual processing loop, which expands to a function call for the corresponding fetched opcode, defined in ./cpu/fame/famec_opcodes.h:

 #define NEXT \
     FETCH_WORD(Opcode); \
     goto *JumpTable[Opcode];

fm68k_emulate() is called in ./pico/pico_cmn.c:

Pico.t.m68c_cnt += fm68k_emulate(&PicoCpuFM68k, cyc_do, 0) - cyc_do;

The number of cycles comes from this macro expansion:

#define CYCLES_M68K_LINE     488 // suitable for both PAL/NTSC
#define CYCLES_M68K_VINT_LAG 112

// ...

// Run scanline:
CPUS_RUN(CYCLES_M68K_LINE - CYCLES_M68K_VINT_LAG);

My understanding is that a certain number of instructions should be processed between each rendered scanline, in order to keep the rendered graphics in sync with the game state.

But as we see above, the logging comes before and after the given number of cycles is handled, i.e. between processing of multiple instructions. Instead, we want to log before each instruction is processed.

Attempt #2

Going back to macro NEXT, we can simply log before fetching the next instruction, resulting in the expected full instruction trace:

 #define NEXT \
+    printf("PC: %p\n",PC); \
     FETCH_WORD(Opcode); \
     goto *JumpTable[Opcode];

The emulator uses a base address of 0x02000000 for the game’s code. Since our Ghidra loader uses base address 0x0, and the coverage script doesn’t use 0x prefixes, we can simply remove them from each logged instruction:

./PicoDrive | grep '^PC: 0x2' | sed 's/^PC: 0x2//g' > ./pico.log

Since most traced addresses will be duplicates, let’s also shave those off:

sort -u ./pico.log > ./pico.sort.log

Analysis

Now we’re all set to start messing around with the game. Let’s start by finding the credits.

We can try different types of inputs by pressing key F8. One of them is Input: Pen on Pad, where we can send inputs such as moving the pen (keys 🡇/🡄/🡅/🡆) or pushing the pen button (key c).

On the title screen, we can interact with objects by pressing the pen button. If we do so on the butterfly, we get an odd behaviour:

The dialog that pops out is similar to the one from the copyright screen:

So, why is no text shown, and why is the dialog closed immediately after being open? This seemed like a good starting point for investigation.

Trace log (without credits text)

Input parsing

First, let’s see how the button presses are parsed by the game. From our custom loader mappings, we already know that 0x800003 = IO_BUTTONS. But even with our coverage script, it still has no references identified.

Ok, then let’s try searching for byte patterns corresponding to a read of this address. If we scroll down a bit, there’s 0x80000d = IO_PAGE, which has the following read reference:

000073e8 10 39 00        move.b     (IO_PAGE).l,D0b
         80 00 0d

From these instruction bytes, we can come up with a regex pattern that matches these moves for any register, but reads from 0x800003:

LANG=C grep -Poba '\x10.\x00\x80\x00\x03' 'Lion King, The - Adventures at Pride Rock (USA).md' \
   | cut -d':' -f1 \
   | xargs -I{} printf '%x\n' {}
# 73bc
# 7bc6

If we go through these results, we bump into an unfortunate guess: Ghidra’s auto analysis added some data addresses near these matches, and when we ran our coverage script, the data addresses prevented the disassembly from happening, even though a call reference was added!

                     FUN_000073bc
                     XREF[1]:     000003a6(c)
000073bc 10              ??         10h
000073bd 39              ??         39h    9
000073be 00              ??         00h
000073bf 80              ??         80h
000073c0 00 03 0a 00     addr       DAT_00030a00
000073c4 00 ff 11 c0     addr       DAT_00ff11c0

After modifying the script to clear incorrect data addresses, we now get this decompiled function:

void FUN_000073bc(void) {
  byte bVar1 = read_volatile_1(IO_BUTTONS);
  DAT_ffffa1dc = bVar1 ^ 0xff;
  DAT_ffffa1dd = DAT_ffffa1dc & ~DAT_ffffa2a5;
  DAT_ffffa2a0 = DAT_ffffa2a5;
  DAT_ffffa2a5 = DAT_ffffa1dc;
  return;
}

DAT_ffffa1dc (let’s name it io_buttons_tmp) contains the currently hold down buttons value, and DAT_ffffa1dd (let’s name it io_buttons_masked) is the negation of the current value with the previous read value stored in DAT_ffffa2a5.

This seems to apply some masking to get the last pressed button value, regardless of other buttons that are also hold down. For example, if we pressed x, but then pressed c while still holding down the other key, io_buttons_tmp would contain the value for both buttons, but io_buttons_masked would only contain the value for c.

io_buttons_tmp has these references:

XREF[8]:     00002ccc(R),
             FUN_000073bc:000073c6(W),
             FUN_000073bc:000073da(R),
             00007be8(W), 00007bf8(R),
             00014244(R), 0001464a(R),
             00014bdc(R)

We could go through each of them, but we might prefer to check which ones are closer to the code related to the copyright screen, since it might share the same dialog boilerplate. By searching for one of the copyright strings, we end up here:

                     DAT_000147c0
                     XREF[1]:     0001479a(*)
000147c0 05              ??         05h
000147c1 05              ??         05h
000147c2 05              ??         05h
000147c3 20              ??         20h
000147c4 20              ??         20h
000147c5 40              ??         40h    @
000147c6 6b              ??         6Bh    k
000147c7 20              ??         20h
000147c8 40              ??         40h    @
000147c9 31              ??         31h    1
000147ca 40              ??         40h    @
000147cb 39              ??         39h    9
000147cc 40              ??         40h    @
000147cd 39              ??         39h    9
000147ce 40              ??         40h    @
000147cf 35              ??         35h    5
000147d0 20              ??         20h
000147d1 53              ??         53h    S
000147d2 45              ??         45h    E
000147d3 47              ??         47h    G
000147d4 41              ??         41h    A

If we scroll above 0001479a, we see some byte patterns that suggest code that wasn’t disassembled. One of the more recognizable ones is the return instruction rts, since the opcode bytes match the ASCII string “Nu”:

00014766 4e              ??         4Eh    N
00014767 75              ??         75h    u

Previously found functions seem to usually have 4 bytes of data after rts and before the next code block starts. We can use this assumption to mostly get correct disassembled blocks.

After a few such blocks, we get a promising new cross-reference for io_buttons_tmp at 000146a2. It contains the same conditional branch as 0001464a, located near the copyright dialog code:

The condition was not true during execution, since 0001465c wasn’t highlighted.

Wait… 0x60?

Let’s see which buttons match 0x60 in the documentation:

 addr   acc   description
-------+-----+------------
...
800003  byte  Buttons, 0 for pressed, 1 for released:
                bit 0: UP (white)    ; 0x01
                bit 1: DOWN (orange) ; 0x02
                bit 2: LEFT (blue)   ; 0x04
                bit 3: RIGHT (green) ; 0x08
                bit 4: red button    ; 0x10
                bit 5: unused?       ; 0x20
                bit 6: unused?       ; 0x40
                bit 7: pen button    ; 0x80

Uh… Only 0x20 + 0x40 adds up to 0x60, but those values are from unknown bits.

Well, this documentation might be outdated, how do emulators deal with these bits?

PicoDrive? (./pico/memory.c)

static u32 PicoRead8_pico(u32 a)
{
  u32 d = 0;

  if ((a & 0xffffe0) == 0x800000) // Pico I/O
  {
    switch (a & 0x1f)
    {
      case 0x01: d = PicoPicohw.r1; break;
      case 0x03:
        d  =  PicoIn.pad[0]&0x1f; // d-pad
        d |= (PicoIn.pad[0]&0x20) << 2; // pen push -> C
        d  = ~d;
        break;
      // ...
    }
  }
}

Genesis Plus GX? (./libretro/libretro.c)

case DEVICE_PICO:
   // ...
   if (input_state_cb(player, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT))
      temp |= INPUT_PICO_PEN;
   if (input_state_cb(player, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_RIGHT))
      temp |= INPUT_PICO_RED;
   // ...
   break;

MAME? (./src/mame/drivers/segapico.cpp)

PORT_BIT( 0x0010, IP_ACTIVE_LOW, IPT_BUTTON1 ) PORT_PLAYER(1) PORT_NAME("Red Button")
PORT_BIT( 0x0080, IP_ACTIVE_LOW, IPT_BUTTON2 ) PORT_PLAYER(1) PORT_NAME("Pen Button")

Kega Fusion?

  • New Kega Fusion !!!! (Archive) - Sega-16 Forums

    Added preliminary Sega Pico support. There’s a lot more I plan to do with this. For now, just load as a Genesis ROM. Mouse is required for Pico, along with the following controls: START switches between StoryWare and trackpad, B is the pico red button, A and C turn pages, and U/D/L/R are, well, U/D/L/R.

Nothing… But we can rebuild PicoDrive. We have the technology. We have the capability to build the world’s first Sega Pico emulator that sets these elusive bits.

Yes… “0x60”

Let’s map some keybind to set input bits 5 and 6.

Attempt #1

We already saw the relevant snippet above, we can just add the value, like it’s done for other buttons:

diff --git a/pico/pico/memory.c b/pico/pico/memory.c
index 14bf5d43..d3d0b987 100644
--- a/pico/pico/memory.c
+++ b/pico/pico/memory.c
@@ -36,7 +36,9 @@ static u32 PicoRead8_pico(u32 a)
       case 0x03:
         d  =  PicoIn.pad[0]&0x1f; // d-pad
         d |= (PicoIn.pad[0]&0x20) << 2; // pen push -> C
+        d |= ((PicoIn.pad[0]&0x40) != 0 ? 0x60 : 0); // unknown bit 5 and 6
         d  = ~d;
+        printf("inpad: %04X, d = %04X\n", PicoIn.pad[0], d);
         break;

       case 0x05: d = (PicoPicohw.pen_pos[0] >> 8);  break; // what is MS bit for? Games read it..

PicoIn.pad[0]&0x40 matches the keybind for z, which was conveniently unused.

Now we compile PicoDrive, load the game, pat a butterfly, and hold down z when the dialog opens… And we see the first string of the credits text! Then we release z… and the dialog closes too soon!

Attempt #2

So, these bits should be working as a toggle. We can revise the logic to keep track of these bits state with some additional variables, and only toggle the state when the key is released.

diff --git a/pico/pico/memory.c b/pico/pico/memory.c
index 14bf5d43..44c70642 100644
--- a/pico/pico/memory.c
+++ b/pico/pico/memory.c
@@ -24,6 +24,9 @@ void dump(u16 w)
 }
 */
+
+static u32 unk_toggle = 0;
+static u32 unk_prev = 0;
+
 static u32 PicoRead8_pico(u32 a)
 {
   u32 d = 0;
@@ -36,7 +39,16 @@ static u32 PicoRead8_pico(u32 a)
       case 0x03:
         d  =  PicoIn.pad[0]&0x1f; // d-pad
         d |= (PicoIn.pad[0]&0x20) << 2; // pen push -> C
+        if ((PicoIn.pad[0]&0x40) != 0 && unk_prev == 0) {
+          unk_prev = 1;
+        }
+        if ((PicoIn.pad[0]&0x40) == 0 && unk_prev == 1) {
+          unk_toggle = !unk_toggle;
+          unk_prev = 0;
+        }
+        d |= (unk_toggle != 0 ? 0x60 : 0); // unknown bit 5 and 6
         d  = ~d;
+        printf("inpad: %04X, d = %04X\n", PicoIn.pad[0], d);
         break;

       case 0x05: d = (PicoPicohw.pen_pos[0] >> 8);  break; // what is MS bit for? Games read it..

Now we can just press and release z to have the entire credits scroll. As expected, the dialog closes without showing “HEY YOU FOUND THE CHEAT” and the subsequent strings.

Trace log (with credits text)

Finding the easter egg input sequence

After running the coverage script with the latest trace log, let’s take a closer look at the dialog code.

The first credits string has some references to constants preceding it:

                     DAT_0000bcae
                     XREF[1]:     00014676(R)
0000bcae 7b              undefined1 7Bh
                     DAT_0000bcaf
                     XREF[1]:     0001470e(R)
0000bcaf 30              undefined1 30h
                     DAT_0000bcb0
                     XREF[1]:     000146b6(R)
0000bcb0 84              undefined1 84h
                     DAT_0000bcb1
                     XREF[1]:     00014722(*)
0000bcb1 20              ??         20h
0000bcb2 20              ??         20h
0000bcb3 20              ??         20h
0000bcb4 20              ??         20h
0000bcb5 52              ??         52h    R
0000bcb6 45              ??         45h    E
0000bcb7 41              ??         41h    A
0000bcb8 4c              ??         4Ch    L
0000bcb9 54              ??         54h    T
0000bcba 49              ??         49h    I
0000bcbb 4d              ??         4Dh    M
0000bcbc 45              ??         45h    E

A quick glance at how many lines are in the credits text and the easter egg text made me consider that those constants might be the number of lines per text. Since each line is terminated with a null byte, we can count how many of those bytes are in each text with python, confirming that the values closely match those constants:

>>> f = open("Lion King, The - Adventures at Pride Rock (USA).md", "rb")
>>> f.seek(0xbcb1)  # credits text offset
>>> v = f.read(0xc1e3 - 0xbcb1)
>>> hex(len(list(re.finditer(rb'\x00',v))))
'0x7d'
>>> f.seek(0xc1e3)  # easter egg text offset
>>> v = f.read(0xc400 - 0xc1e3)
>>> hex(len(list(re.finditer(rb'\x00',v))))
'0x30'

If we follow the reference for DAT_0000bcae, the code block quickly grabs our attention:

The credits lines value stored in D0 is incremented with a value closely matching the easter egg lines, but only if DAT_ffffa1e2 is set. This happens only at the following address:

Which is reached from this block:

Again, some interesting coverage: If some button is currently pressed (i.e. the branch at 0001424a is not taken), it will be or’d with other hold down buttons, then compared with the byte value at an index offset from DAT_00014200, which contains these 9 values:

                     DAT_00014200
                     XREF[2]: 00014252(*), 00014256(R)
00014200 62              ??         62h    b
00014201 66              ??         66h    f
00014202 67              ??         67h    g
00014203 6f              ??         6Fh    o
00014204 ef              ??         EFh
00014205 f0              ??         F0h
00014206 ff              ??         FFh
00014207 63              ??         63h    c
00014208 ec              ??         ECh
00014209 00              ??         00h

Looks like a cheat code, doesn’t it? All these values can be added up with our input buttons!


Before we decode these values, let’s figure out on which screens this function is being called. To do so, I’ve added the following trace log to PicoDrive:

 #define NEXT \
         printf("PC: %p\n",PC); \
+        if (GET_PC == 0x1425a) { \
+                u32 io_buttons_tmp = 0xffffa1dc; \
+                u32 io_buttons_masked = 0xffffa1dd; \
+                u32 io_buttons_tmp_b; \
+                u32 io_buttons_masked_b; \
+                u32 d2 = DREGu8(2); \
+                u32 d0 = DREGu8(0); \
+                READ_BYTE_F(io_buttons_tmp, io_buttons_tmp_b) \
+                READ_BYTE_F(io_buttons_masked, io_buttons_masked_b) \
+                printf("op: %04X cmp.b d2=%04X, d0=%04X (tmp=%04X, mskd=%04X)\n", GET_PC, d2, d0, io_buttons_tmp_b, io_buttons_masked_b); \
+        } \
         FETCH_WORD(Opcode); \
         goto *JumpTable[Opcode];

When one of the code values is being compared, we print our current pressed button value, our accumulated hold down buttons value, and the compared code value. This trace was helpful not only to avoid discovering call references, but also to check at runtime if the sequence was being input correctly.

Activating the easter egg

The previous trace log only outputs during the Sega Pico logo screen, so that’s when we can input the sequence. Miss that chance and a soft reset must be done to try again.

Decoding the input sequence is just a matter of going back to the documentation, and seeing what adds up to each cheat code byte value. We also require the “elusive bits” key z to be pressed and released at the beginning, but not hold down. All remaining keys must be hold down in the correct order.

Input type must be set to Input: Joystick (PicoDrive’s default), otherwise the directional buttons won’t be registered.

Input sequence:

Key Input Value Code Value
z 60h  
🡇 (60h)+02h 62h
🡇+🡄 (60h)+02h+04h 66h
🡇+🡄+🡅 (60h)+02h+04h+01h 67h
🡇+🡄+🡅+🡆 (60h)+02h+04h+01h+08h 6Fh
🡇+🡄+🡅+🡆+c (60h)+02h+04h+01h+08h+80h EFh
x+c (60h)+10h+80h F0h
x+c+🡇+🡄+🡅+🡆 (60h)+10h+80h+02h+04h+01h+08h FFh
🡇+🡅 (60h)+02h+01h 63h
c+🡄+🡆 (60h)+80h+04h+08h ECh

Caption:

Key Mapping
z Toggle bits 5 and 6
x Red Button
c Pen Button
🡇/🡄/🡅/🡆 Down/Left/Up/Right Buttons

Depending on your hardware, you might run into some complications:

  • If you’re using a gamepad, the D-pad should have isolated buttons for each direction, so that you can reliably hold all 4 of them down;
  • If you’re using a keyboard, the lack of n-key rollover could lead to some hold down keys to stop registering.

Alternatively, the cheat can be activated by applying the following patch:

0001425c = 6018 ; Unconditional branch for cheat enabled
00014650 = 600a ; Unconditional branch for io_buttons_tmp == 0x60
000146a8 = 600a ; Same as 00014650

If the input sequence is activated correctly, a third bird will dash through the Sega Pico logo screen. As for the rest, I’ll just leave you with this video: