Object Size Select
Updated: May 16, 2023 at 19:10 CESTSize select registers define how many and which entries in OAM are displayed as 32*32 objects.
⚠️ The following information is partly based on the research done by Martin Piper and it’s not properly emulated by MAME as of the time of writing.
$9A00
(Write Only)
$9A01
(Write Only)
💡 Both registers accept values from 0 to 15 however values from 0 to 3 do not provide any result.
During initial setup memory from $9A00
to $9AC8
is zero filled by code at $015F
. It’s currently unknown why such large block is used.
Even though single OAM entry contains a lot of information it doesn’t specify the object size. All entries are assumed to be 16*16 objects by default and no additional action is required. To display one or more of 32*32 objects the forementioned registers are used. Four combinations are possible:
Mode | Requirements |
---|---|
16*16 objects only | Set both registers to $00 value. This causes every OAM entry out of 24 available to be displayed as 16*16. |
32*32 objects only | Set $9A00 to $0F and $9A01 to $00 . Every OAM entry out of 12 possible is displayed as 32*32. |
16*16 and 32*32 objects (ordered) | Set $9A00 register with number of 32*32 objects increased by 3. Write all 32*32 entries before 16*16 ones during OAM update. If the number of 32*32 objects changes dynamically also update $9A00 register. This is roughly what game code does at $0958 . |
16*16 and 32*32 objects (mixed) | Use $9A00 as start and $9A01 as end parameter. start points to the first OAM entry to be displayed as 32*32 object while end points to entry following the last one to be displayed as such. Both parameters must be in 1-12 range and then increased by 3 before writing. |
While first two modes are straightforward let’s review the original code located at $0958
:
oam_update:
ld ix, $9878 ; VRAM: OAM last entry (dst for 16*16 obj)
ld iy, $9820 ; VRAM: OAM first entry (dst for 32*32 obj)
ld hl, $8500 ; RAM: OAM mirror (src)
ld b, $0C ; 12 objects * 8 (4 byte entry followed by 4 zeroes ) = OAM size
ld c, 3 ; 32*32 obj counter (starts from 3 because values 0-3 have no effect on $9A00)
_next:
ld a, b ; all 12 objects processed?
cp 0
jr z, _done ; yes
inc hl ; skip 1st byte of OAM entry (obj id)
ld a, (hl) ; get 2nd byte (obj attribute)
dec hl ; move back to 1st byte (obj id)
bit 5, a ; it's 32*32 object if 5th bit is set
jr nz, _obj32
_obj16: ; 16*16
call $0982 ; de = ix, copy 8 bytes from (hl) to (de), ix - 8, b - 1
jr _next
_obj32: ; 32*32
call $0996 ; de = iy, copy 8 bytes from (hl) to (de), iy + 8, b - 1, c + 1
jr _next
_done: ; sorting & update finished
ld a, c ; 32*32 object count
ld ($9A00), a ; tell the hardware how many 32*32 objects are in OAM, starting from 1st entry
ret
This approach simply limits the total number of objects being used in game to 12, avoiding the trouble of using $9A01
register.
Each OAM entry is aligned to 8 byte boundary. Additional 4 bytes are usually zero filled (empty entry) however backup of x/y coordinates related to given
entry might be found sometimes there. This has no visible effect anyway. 32*32 entries are stored (forward) from the beginning and 16*16 entries (backward) from the end of OAM.
At exit $9A00
register is updated with value equal to number of 32*32 entries increased by 3.
Using mixed method is somewhat trickier to understand. It doesn’t require 32*32 entries to be stored at the beginning of the OAM, they must form continuous block nonetheless. Single 16*16 and 32*32 entries can’t be stored interchangeably - please refer to examples below. While it might seem confusing the whole start / end assignment is only for explanation purpose. As long as both values are right it doesn’t matter which register is loaded with which value, because result is the same. Only equal values have no effect. All possible register combinations have been tested by Martin Piper and can be viewed here.
(1,2) $9A00 = $04, $9A01 = $05
OAM Address | 16*16 OBJ | 32*32 OBJ |
---|---|---|
$9820 | 0 | |
$9824 | 1 | |
$9828 | 1 | |
$982C | ||
$9830 | 4 | |
$9834 | 5 | |
$9838 | 6 | |
$983C | 7 | |
$9840 | 8 | |
$9844 | 9 | |
$9848 | 10 | |
$984C | 11 | |
$9850 | 12 | |
$9854 | 13 | |
$9858 | 14 | |
$985C | 15 | |
$9860 | 16 | |
$9864 | 17 | |
$9868 | 18 | |
$986C | 19 | |
$9870 | 20 | |
$9874 | 21 | |
$9878 | 22 | |
$987C | 23 |
(9,3) $9A00 = $0C, $9A01 = $06
OAM Address | 16*16 OBJ | 32*32 OBJ |
---|---|---|
$9820 | 0 | |
$9824 | 1 | |
$9828 | 2 | |
$982C | 3 | |
$9830 | 4 | |
$9834 | 5 | |
$9838 | 3 | |
$983C | ||
$9840 | 4 | |
$9844 | ||
$9848 | 5 | |
$984C | ||
$9850 | 6 | |
$9854 | ||
$9858 | 7 | |
$985C | ||
$9860 | 8 | |
$9864 | ||
$9868 | 18 | |
$986C | 19 | |
$9870 | 20 | |
$9874 | 21 | |
$9878 | 22 | |
$987C | 23 |
(4,12) $9A00=$07, $9A01=$0F
OAM Address | 16*16 OBJ | 32*32 OBJ |
---|---|---|
$9820 | 0 | |
$9824 | 1 | |
$9828 | 2 | |
$982C | 3 | |
$9830 | 4 | |
$9834 | 5 | |
$9838 | 6 | |
$983C | 7 | |
$9840 | 4 | |
$9844 | ||
$9848 | 5 | |
$984C | ||
$9850 | 6 | |
$9854 | ||
$9858 | 7 | |
$985C | ||
$9860 | 8 | |
$9864 | ||
$9868 | 9 | |
$986C | ||
$9870 | 10 | |
$9874 | ||
$9878 | 11 | |
$987C |
This makes it quite flexible solution which can be used in a few different ways. As in previous cases it is recommended to update OAM first, then registers if there were any changes in 32*32 objects number.