Monday, May 29, 2017

Level Load Block

Rather than being made directly out of tiles, Sonic levels are built out of larger. precomposed tile patterns which are progressively assembled in order to generate the full layout. Specifically, combining four tiles makes a 16x16 "block", and combining 64 blocks makes a 128x128 "chunk". Chunks are then placed along a grid, forming the level's layout.

The array which holds tile, block and chunk data for each level is called the "level load block". Here's what it looks like in the current disassembly:
;   1st PLC         palette                          2nd 8x8 data                                     2nd 16x16 data                                      2nd 128x128 data
;           2nd PLC           1st 8x8 data                                    1st 16x16 data                                    1st 128x128 data

    levartptrs $B,  $B,  $A,  AIZ1_8x8_Primary_KosM, AIZ1_8x8_Secondary_KosM, AIZ1_16x16_Primary_Kos, AIZ1_16x16_Secondary_Kos, AIZ1_128x128_Kos,         AIZ1_128x128_Kos              ; ANGEL ISLAND ZONE ACT 1
    levartptrs $C,  $C,  $B,  AIZ2_8x8_Primary_KosM, AIZ2_8x8_Secondary_KosM, AIZ2_16x16_Primary_Kos, AIZ2_16x16_Secondary_Kos, AIZ2_128x128_Kos,         AIZ2_128x128_Kos              ; ANGEL ISLAND ZONE ACT 2
    levartptrs $E,  $F,  $C,  HCZ_8x8_Primary_KosM,  HCZ1_8x8_Secondary_KosM, HCZ_16x16_Primary_Kos,  HCZ1_16x16_Secondary_Kos, HCZ_128x128_Primary_Kos,  HCZ1_128x128_Secondary_Kos    ; HYDROCITY ZONE ACT 1
    levartptrs $10, $11, $D,  HCZ_8x8_Primary_KosM,  HCZ2_8x8_Secondary_KosM, HCZ_16x16_Primary_Kos,  HCZ2_16x16_Secondary_Kos, HCZ_128x128_Primary_Kos,  HCZ2_128x128_Secondary_Kos    ; HYDROCITY ZONE ACT 2

Yikes, it's a bit packed there, isn't it. levartptrs is a macro, and here's the definition:
; macro for declaring a "main level load block" (MLLB)
levartptrs macro plc1,plc2,palette,art1,art2,map16x161,map16x162,map128x1281,map128x1282
    dc.l (plc1<<24)|art1
    dc.l (plc2<<24)|art2
    dc.l (palette<<24)|map16x161
    dc.l (palette<<24)|map16x162
    dc.l map128x1281
    dc.l map128x1282
Okay, so each entry is made up of six longwords, each a pointer to some kind of level data. From the top, two pointers to 8x8 tile data, then two pointers to 16x16 block mappings, and finally two pointers to 128x128 chunk mappings.

Additionally, since the Mega Drive's ROM address space only goes up to 0x400000, the top byte in a longword pointer is never set, so we can sneak in some extra pointers: two PLC pointers and a palette pointer (which appears twice but only the first one is used).

Here's what it looks like unpacked:
    dc.l HCZ_8x8_Primary_KosM+$E000000
    dc.l HCZ1_8x8_Secondary_KosM+$F000000
    dc.l HCZ_16x16_Primary_Kos+$C000000
    dc.l HCZ1_16x16_Secondary_Kos+$C000000
    dc.l HCZ_128x128_Primary_Kos
    dc.l HCZ1_128x128_Secondary_Kos
The reasoning for two of each level data comes from Sonic 3's act system. The second act of each Zone tends to shoot off and do its own thing, often requiring a new set of tiles, blocks and chunks. Almost inevitably though, some elements end up being present in both acts; they're the same Zone, after all.

Rather than duplicating the common bits in both act 1 and act 2's data, a more space-efficient solution is to split them into their own "primary" data, and then each act adds on its own "secondary" data as needed. A similar trick was used in Sonic 2's Hill Top Zone because geez, that stage did everything first.

The three byte pointers are indexes to arrays elsewhere in the ROM. The palette byte points to an array called, well:
    dc.l Pal_HCZ1
    dc.w $FC20
    dc.w $17
Each entry is made up of one longword and two words. The longword is a ROM pointer to the actual color data, the first word is a RAM pointer to the destination address, and the last word is the number of longwords to copy minus 1. Recall that Mega Drive colors are 16 bits each: each longword holds two colors, making $18 longwords hold $30 colors, which completely fills out three palette lines.

The PLC bytes point to the PLC offset array, which is itself a pointer array that points to individual PLCs:
    dc.w PLC_0E-Offs_PLC        ; HCZ 1 PLC 1
    dc.w PLC_0F-Offs_PLC        ; HCZ 1 PLC 2
Here's what a PLC looks like:
PLC_0E: plrlistheader
    plreq $45C, ArtNem_Bubbles
    plreq $3CA, ArtNem_HCZMisc
    plreq $426, ArtNem_HCZButton
    plreq $37A, ArtNem_HCZWaterRush
    plreq $42E, ArtNem_HCZWaveSplash
    plreq $43E, ArtNem_HCZSpikeBall

PLC_0F: plrlistheader
    plreq $44C, ArtNem_HCZDragonfly
Oh great, more macros. Can I just take a minute to ask, does anybody actually like these? I'm not even going to bother with the macro definition; it's gibberish for all I can tell. You'll just have to believe me when I say it unpacks to this:
    dc.w 5
    dc.l ArtNem_Bubbles
    dc.w $8B80
    dc.l ArtNem_HCZMisc
    dc.w $7940
    dc.l ArtNem_HCZButton
    dc.w $84C0
    dc.l ArtNem_HCZWaterRush
    dc.w $6F40
    dc.l ArtNem_HCZWaveSplash
    dc.w $85C0
    dc.l ArtNem_HCZSpikeBall
    dc.w $87C0
    dc.w 0
    dc.l ArtNem_HCZDragonfly
    dc.w $8980
PLCs, or Pattern Load Cues, are essentially pairs of pointers: the first pointer, a longword, is a pointer to graphic data in ROM; the second pointer, a word, is the destination address in VRAM. The first word in each PLC is the number of entries minus 1.

Sonic 3 loads most of its sprite graphics through PLCs. They allow graphics to be arranged and reused between levels, allowing elements to be shared between both acts of a Zone, or used globally such as spikes and springs.

Next time, we'll look at one way PLCs can go very, very wrong.


  1. "Can I just take a minute to ask, does anybody actually like these?"

  2. Speaking as the guy that added those macros in the first place - yeah.

  3. The links in this post all seem to go to the blogger domain and not to S3unlocked pages. Not sure what happened.

    1. Oops. The WYSIWYG editor mangles all relative links like that when you switch to it. Fixed.