Back to top

Dynamic Sprites Utility (dsu)

by Koopster - Version: 1.0.1

Table of contents

  1. Overview
    1. Compatible sprites
  2. Installation
  3. Configuration
    1. Global settings
    2. Local settings
    3. Free RAM
  4. Technical information
    1. Importing graphics
    2. Importing graphics in freedata
    3. Importing large graphics (> 32 kB)
    4. Dynamic slots
    5. Multi slot sprites
    6. Other routines
    7. RAM details
  5. Acknowledgements
  6. Changelog

Overview

The Dynamic Sprites Utility (dsu) is an UberASM Tool resource that enables dynamic sprites to be used.

A dynamic sprite is one that has a number of animation frames larger that could feasibly fit in SP1-SP4 (tiles 0x400-0x5FF in Lunar Magic's 8x8 editor, where all sprite graphics must be available), and thus needs to upload graphics on the fly to get around that space limitation. Because graphics cannot be uploaded directly from sprite code, a separate engine is required. Historically, the Dynamic Sprites Patch (dsx) has been the standard, but it poses a few issues and limitations that dsu hopes to get around.

One glaring point is the region of SP1-SP4 where the graphics are uploaded to. In dsx's system, that region cannot be changed: it's always the bottom half of SP4 (tiles 0x5C0-0x5FF). In the figure below, the red, green, blue and yellow 4x4 tile squares indicate the space that is claimed, in that order, by a new dynamic sprite that comes up on screen, up to a limit of four sprites at once. Therefore, sprites that normally use that region (like Bob-omb, Wiggler and Lakitu) will get their graphics tampered by dynamic sprites, even if they look right in Lunar Magic.

dsx's mapping.

dsu is different from dsx in that:

dsu's default mapping for the maximum number of sprites (6).
Example remapping (to 0x580, 0x567, 0x4E0 and 0x4C8) and capping (to 4).

The remapping and capping features introduce a lot of versality to dynamic sprite usage, enabling them to be off the bat compatible with any non-dynamic sprite regardless of the graphics region it uses. The graphics restoration feature can enhance this further if the level structure ensures certain non-dynamic sprites will never coexist with dynamic sprites. As for the increased limit of six dynamic sprites on screen, while it does allow for dynamic sprites to be placed more densely, the true potential of this change resides in facilitating and even enabling larger dynamic sprites (that require multiple dynamic slots).

Compatible sprites

It's important to note that dynamic sprites must be programmed accordingly to use dsu. Currently, dynamic sprites in SMW Central's section are compatible with dsx. You can find a list of converted sprites here or in dsu's release thread in SMW Central.


Installation

dsu requires UberASM Tool (version 2.1 or superior) to be installed in the ROM. Furthermore, PIXI (minimal version is unknown, but 1.42 or superior is recommended) must be used to insert the dynamic sprites. Once both tools are set up, dsu's installation is quite straightforward.

  1. Drag the contents of the uber_files folder inside your UberASM Tool directory.
  2. In UberASM Tool's list.txt, add the file dsu.asm as every (*) gamemode and run the tool (if there's already another file inserted as *, separate the filenames with a comma). Your list should look like this:
    gamemode:
    * dsu.asm
  3. Drag the contents of the pixi_files folder inside your PIXI directory to give support to dynamic sprites made for dsu.

Configuration

This section describes the configuration and usage of dsu's features.

Global settings

In your UberASM Tool directory, find the file dsu/settings.asm. That file has the following settings:

  • !dsu_mapping_default: The mapping of each dynamic sprite, unless overriden at a given level.
  • !dsu_max_slots_default: The maximum number of allowed sprites on screen, unless overriden at a given level.
  • !dsu_restore_default: If set to 0, the graphics restoration setting will be turned off by default and it will be possible to turn it on on a per-level basis. If set to 1, the inverse will be true.
  • !unpatch_dsx: If set to 1, dsu will spot and uninstall dsx from the ROM when UberASM Tool is ran (nothing will happen if dsx is not installed). Most SA-1 ROMs will have dsx installed as it is a default setting in the SA-1 Pack. Keeping dsx in a ROM with dsu installed shouldn't be harmful, but you're absolutely sure you will not need dsx anymore, you can turn that setting on.

Local settings

To change the mapping of each dynamic sprite in a specific level, write this to UberASM Tool's level list (using level 105 as an example):

105 dsu_remap.asm: ($aaa) ($bbb) ($ccc) ($ddd) ($eee) ($fff)

where $aaa through $fff are the new mappings ($400-$5FF) of each of the six dynamic slots. The number of arguments is variable. In practice, it should match the maximum number of allowed dynamic sprites on screen in that level.

To change the maximum number of allowed dynamic sprites on screen in a specific level, write this to UberASM Tool's level list (using level 105 as an example):

105 dsu_cap.asm: n

where n is the maximum number (0-6).

To toggle the graphics restoration setting (that is, turn it on if !dsu_restore_default is equal to 0, or turn it off if that define is equal to 1), write this to UberASM Tool's level list (using level 105 as an example):

105 dsu_restore.asm

Note that it is possible to use multiple of these files in the same level as well as any other files, for example:

105 dsu_cap.asm: 2, dsu_remap.asm: ($500) ($580), unrelated.asm, dsu_restore.asm

Free RAM

Free RAM settings are at the file dsu/ram.asm in UberASM Tool's directory and at the top of the file asm/ExtraDefines/dsu.asm in PIXI's directory. Both files should match.

As you can see, there are three ranges of free RAM. The first two have separate defines for LoROM and SA-1 and are used regardless of dsu's settings. !dsu_freeram requires 45 (LoROM) or 64 (SA-1) bytes, and !dsu_freeram_nmi requires 54 bytes. Both of these ranges can be in the long access region ($7E2000-$7FFFFF, or $402000-$41FFFF on SA-1), but it is preferable that !dsu_freeram_nmi is in the direct access range ($0100-$1FFF, add $6000 on SA-1), since it is used primarily in the NMI routine and direct accesses are faster.

The third range, !dsu_restore_freeram, is only ever used if the graphics restoration setting is turned on at a given level. It's used to back up the original graphics, so how much of it is taken depends on the maximum number of allowed dynamic sprites on screen: each will take 512 bytes, to a maximum of 3072 bytes. Furthermore, only one define is used because it can use WRAM (banks $7E-$7F) on SA-1 without issue.

You shouldn't normally have to change these addresses. The default !dsu_freeram and !dsu_freeram_restore point to fairly untouched regions even by custom resources, and the default !dsu_freeram_nmi is used exclusively in the overworld and Iggy/Larry's battles (which you usually won't use a dynamic sprite in). If for whatever reason you need to change them, again, make sure to do so in the two files.


Technical information

This section is dedicated to programmers who wish to convert or program new dynamic sprites for dsu.

Upon installation, users will add two things to their PIXI environment: the dsu routines, at routines/Dynamic, and the global defines and macros, made available via the file asm/ExtraDefines/dsu.asm. When programming a sprite for dsu, the availability of these can be assumed.

Importing graphics

One convenient thing about dsx is the very user-friendly expected arrangement of the .bin files containing the frames: they are disposed in 32x32 blocks, typically matching how they are displayed in game. Unfortunately, this arrangement would require four separate DMA uploads for each sprite in dsu's engine. To simultaneously keep dsx's easily editable .bin files arrangement and have DMA-efficient storage in the ROM (see the figures below), a conversion between the two formats is done during assembly.

Editable .bin file for the user.
DMA-efficient graphics in the ROM.

To perform this conversion, instead of the typical:

incbin graphics.bin

graphics must be imported with the dsu_incbin macro instead:

%dsu_incbin(graphics.bin)

Aside from the mandatory filename, this macro has two optional parameters. The second parameter is the number of frames: if it's not informed, it will be calculated based on the file size, always as a multiple of four. If the actual number of frames is not a multiple of four, one to three empty frames will be inserted (and if the trailing space of the last row has been removed from the .bin file, the macro will try to reference an area that doesn't exist and output an error). In such cases, manually informing the number of frames is recommended for the sake of saving some ROM space.

The third parameter is the hexadecimal offset of the .bin file to start importing graphics from. In a typically formatted file, this offset should be a multiple of 0x800. This parameter is mostly useful when importing graphics larger than 32 kB. This will be talked about further in the section after the next.

A made up example that wishes to import frames 4 to 13 (count starts at 0) of a given .bin file would look like this:

%dsu_incbin(graphics.bin, 10, $0800)

Importing graphics in freedata

Naturally, sprite code is inserted in freecode blocks. Likely because of old assemblers lacking easy freespace search, it has been typical to import the graphics of dynamic sprites in the same block as the sprite itself. By modern standards, it is wasteful to insert data in a freecode block, so it's better practice to include the graphics in a separate freedata block instead.

Say the graphics are in a separate freedata block under the label gfx. First and foremost, any separate freespace block should be protected to prevent it from leaking. The prot command can be used for this, but note that it can only be used at the start of a freespace block, so this should be written at the very start of the sprite code:

prot gfx

The separate freedata block containing the graphics can then be placed anywhere in the sprite code in the following way:

pushpc

freedata
gfx:
%dsu_incbin(graphics.bin)

pullpc

Because it is quite repetitive to write all of this everytime, the dsu_incbin_freedata macro handles all of this internally, including the freespace protection (for that reason, it must be called at the start of the sprite code). The macro has two mandatory parameters: the filename and the label. The third and fourth optional parameters correspond to the second and third optional parameters of the dsu_incbin macro.

So, to do the exact same thing as above, the macro can be called as follows (again, mandatorily at the start of the sprite code):

%dsu_incbin_freedata(graphics.bin, gfx)

Importing large graphics (> 32 kB)

With the deprecation of the file.bin -> label command and the check bankcross off command not functioning, Asar 1.91 essentially does not permit assembling freespace blocks larger than 32 kB. This poses a problem for dynamic sprites with more than 64 frames, of which there are quite a few.

The optional parameters of the dsu_incbin macro were introduced as a workaround for this. Because splitting the .bin file in two would be incovenient, these parameters allow a single .bin file to be inserted in two separate chunks, in two separate freespace blocks.

For example, a .bin file containing 80 frames (40 kB) can be imported across two freespace blocks under two labels. The first block, under the label gfx_1, holds the first 64 frames (32 kB, so a full bank), and the second block, under the label gfx_2, holds the remaining 16 frames (8 kB). Just like above, the labels should be protected at the start of the sprite code:

prot gfx_1
prot gfx_2

The graphics can then be actually imported anywhere in the sprite code in the following way:

pushpc

freedata
gfx_1:
%dsu_incbin(graphics.bin, 64)

freedata
gfx_2:
%dsu_incbin(graphics.bin, 80-64, $8000)

pullpc

Again, to avoid the repetition of writing all of this everytime, the dsu_incbin_large macro handles all of this internally, including the freespace protection. The macro has four mandatory parameters: the filename, the two labels and the total number of frames. For compatibility with dsu's routines, the first block will always hold 64 frames, and the remaining frames will be in the second block.

So, to do the exact same thing as above, the macro can be called as follows (again, mandatorily at the start of the sprite code):

%dsu_incbin_large(graphics.bin, gfx_1, gfx_2, 80)

Dynamic slots

With dsx, a dynamic sprite currently on screen will reupload its graphics every frame in the first "slot" it finds. This is wasteful if the dynamic sprite's frame hasn't changed. To improve upon this, dsu allocates a dynamic slot to a sprite that calls either of the DynamicGetSlot or DynamicAssignSlot routines and is thus able to keep track of where it has been drawn and which frame is currently drawn, preventing unecessary reuploads when the frame hasn't changed.

A sprite's dynamic slot is kept in a sprite table at !sprite_dsu_slot (an empty slot holds the value of 0xFF), though it is typically not necessary to access it directly as the subroutines already handle everything (how all of dsu's RAM works will be detailed later).

The DynamicGetSlot routine is comparable to the GETSLOT routine used in dsx sprites in that it takes the current frame as input, signals a graphics upload (though only when necessary) and outputs the base tile location. There are many key differences, though. For one, dsu does not use a buffer, so the routine does not perform a DMA: it only adjusts the pointers that will be used during the NMI routine. Secondly, because it is a general routine, the pointer(s) to the graphics must be informed on input. And lastly, there is support for animating on a frame rule (at 15 FPS).

A fair warning before getting into it: do not call this routine during a sprite's init! Due to the order of operations of the game loop, it's entirely possible that a sprite spawns in place of a multi slot dynamic sprite (see the next section) that just despawned on the same frame, and if that happens, !sprite_dsu_slot will be in a corrupted format for this routine. By the time the sprite's main runs, dsu will already have taken care of this conflict.

These are the parameters of the routine:

DynamicGetSlot
Parameter
Type
Description
$8A-$8C
Input
24-bit pointer to graphics of frames 0-63.
$8D-$8F
Input
24-bit pointer to graphics of frames 64-127. Not needed if the sprite only uses up to 64 frames.
Accumulator
Input
Frame number to upload (0-127).
Carry
Input
If clear, only upload graphics on a frame rule. If set, always upload graphics when the frame changes.
Carry
Output
If set, a dynamic slot wasn't available.
Accumulator
Output
If a dynamic slot was available, top left tile number.
$8A
Output
If a dynamic slot was available, page bit.

The dsu_gfx_pointers macro can assist with setting up $8A-$8F (it is merely a series of LDA : STA assuming 8 bits accumulator). The first parameter of this macro is the pointer that goes in $8A-$8C, and the second, optional parameter is the pointer that goes in $8D-$8F for sprites that use more than 64 frames.

Assuming a sprite has more than 64 frames, the graphics are located at pointers gfx_1 (frames 0-63) and gfx_2 (frames 64+), !frame holds the current frame and the frame rule is enabled, this is how the routine would be called:

%dsu_gfx_pointers(gfx_1, gfx_2)
LDA !frame
CLC
%DynamicGetSlot()
BCS no_slot

On output, the accumulator holds the top left tile value: add two for the top right tile, two more for the bottom left tile and two more for the bottom right tile. $8A holds the page bit (the T bit in the YXPPCCCT format).

When a dynamic slot isn't found, the routine itself will despawn the sprite. Despawn, not kill: if the sprite has an entry at the load status table, it will be cleared.

If the frame rule is enabled, a sprite at a given dynamic slot will only upload graphics during a specific state of the two least significant bits of the frame counter ($14). An upload will happen according to the following table:

$14 & 3
Dynamic slot
0
1
2
3
4
5
0
1
2
3

It's possible to opt out of the frame rule because some sprites rely on updating their graphics at 30 or 60 FPS, but it's strongly recommended to enable it whenever possible to prevent NMI overhead.

The DynamicAssignSlot routine is a simplified version of the previous routine that assigns a dynamic slot to the sprite, but does not set a valid frame or signal an upload. The carry is its only input and output parameter:

DynamicAssignSlot
Parameter
Type
Description
Carry
Input
If set, despawn the sprite if a slot isn't found.
Carry
Output
If set, a dynamic slot wasn't available.

This routine was primarily created to assist with spawning a dynamic sprite from a shooter, a generator or another sprite. In such cases, it should be called with input carry clear after the search for a free sprite slot. If a dynamic slot isn't found, it shouldn't be allowed to spawn.

There's a second, more niche use for this routine. There are some dsx sprites in which the GETSLOT routine won't be called right away. For example, in Yoshi's Island Wild Piranha, it isn't called if the player is far away, as the bulb does not use dynamic graphics. This means the bulb will spawn if a dynamic slot isn't available, but immediately despawn as soon as the player comes close as it tries to load its dynamic graphics. In such sprites, the DynamicAssignSlot routine should be called under the sprite's init with input carry set.

Multi slot sprites

Large dynamic sprites have existed for dsx for a long time. These sprites would simply take multiple "slots" in dsx by calling GETSLOT multiple times. Because dsu has an actual slot assignment system, DynamicGetSlot cannot simply be called multiple times.

Instead, the DynamicGetMultiSlots routine must be used for such sprites. This routine will format and handle !sprite_dsu_slot bitwise instead of numerically (this will be detailed later). For reasons analogous to those stated in the previous section, this routine should not be called during a sprite's init! These are the parameters of the routine:

DynamicGetMultiSlots
Parameter
Type
Description
$8A-$8C
Input
24-bit pointer to graphics of frames 0-63.
$8D-$8F
Input
24-bit pointer to graphics of frames 64-127. Not needed if the sprite only uses up to 64 frames.
$04-$09
Input
Frame numbers to upload at each of up to six dynamic slots (0-127).
Accumulator
Input
Number of dynamic slots used, minus 1 (0-5).
Carry
Output
If set, not enough dynamic slots were available.
$04-$09
Output
If dynamic slots were available, top left tile numbers for each of up to six dynamic slots.
$8A
Output
If dynamic slots were available, page bits in the format %--987654, with 4 corresponding to the tile number at $04 and so on.

The input accumulator determines how much of the $04-$09 scratch RAM range will be used. For example, if only three slots are used (so the input accumulator is 2), only $04-$06 need to be set up on input and read on output, and $07-$09 will not be touched at all.

Using that example, now assuming that the sprite uses 64 frames or less (located at a single pointer gfx) and !frame_n holds the frame to upload at the n-th dynamic slot, this is how the routine would be called:

%dsu_gfx_pointers(gfx)
LDA !frame_1
STA $04
LDA !frame_2
STA $05
LDA !frame_3
STA $06
LDA #$02
%DynamicGetMultiSlots()
BCS no_slots

The top left tile numbers should be handled similarly to the single slot routine (subsequently add two to get the four tiles of the frame). To fetch the page bits, a recommended strategy is to use the code LSR $8A : LSR !15F6,x : ROL !15F6,x after drawing each set of four tiles.

Other routines

This section goes over other routines that are included in dsu's PIXI environment but have more niche uses.

DynamicGetSlotGeneric is a version of DynamicGetSlot that can be used for other sprite types. The original routine expects normal sprite indexing to keep track of the sprite's dynamic slot, so it can't be used for other sprite types. In this routine, Y holds the dynamic slot number plus 1 on input and output, so it must be kept track of within sprite code i.e. in a sprite table. If input Y is equal to 0x00, a dynamic slot search will be performed. Very importantly, the dynamic slot must be freed manually when the sprite despawns! For that, see DynamicClearSlotGeneric.

DynamicGetSlotGeneric
Parameter
Type
Description
$8A-$8C
Input
24-bit pointer to graphics of frames 0-63.
$8D-$8F
Input
24-bit pointer to graphics of frames 64-127. Not needed if the sprite only uses up to 64 frames.
Accumulator
Input
Frame number to upload (0-127).
Y
Input/Output
Dynamic slot, plus 1. If 0, search for a slot.
Carry
Input
If clear, only upload graphics on a frame rule. If set, always upload graphics when the frame changes.
Carry
Output
If set, a dynamic slot wasn't available.
Accumulator
Output
If a dynamic slot was available, top left tile number.
$8A
Output
If a dynamic slot was available, page bit.

dsu uses sprite tables to keep track of a normal sprite's dynamic slots to automatically free them up should it despawn. This isn't done for other sprite types. Therefore, the DynamicClearSlotGeneric routine must be called when such sprites despawn, or else their dynamic slot will never be freed. It simply takes the dynamic slot plus 1 as an input.

DynamicClearSlotGeneric
Parameter
Type
Description
A
Input
Dynamic slot, plus 1.

DynamicTransferSlot transfers the dynamic slot used by the sprite in X to the sprite in Y. This can be used for sprites that spawn other dynamic sprites and only use dynamic graphics themselves during the spawning. If the sprite in X wasn't assigned a dynamic slot in the first place, the output carry will be set. It can only be used for single slot sprites.

DynamicTransferSlot
Parameter
Type
Description
X
Input/Output
Source sprite slot.
Y
Input/Output
Destination sprite slot.
Carry
Output
If set, the source sprite slot was not assigned a dynamic slot.

RAM details

The RAM addresses described in this section are globally available in both UberASM Tool's and PIXI's environments.

We're hackers, so one may find uses to manipulating some of them directly (such as reading a sprite's dynamic slot to create a custom frame rule). However, it must be noted that dsu strives for making the most common processes only require calling the enclosed routines and macros, as that facilitates maintenance and sprite programming on itself. With that in mind, if you have suggestions for new routines, macros and functionalities that could be bundled with dsu, please reach out!

!dsu_freeram
Define
Offset
Default
Length (B)
Description
LoROM
SA-1
!dsu_nmi_index
0
$7F8B4E
$409C00
1
How many dynamic sprites to upload, times two. Cleared on game load and always cleared on the NMI routine prior to the uploads. Should always be zero outside of a level.
!dsu_max_slots
1
$7F8B4F
$419C01
1
Maximum number of allowed dynamic sprites on screen. Set to !dsu_max_slots_default on level load, but readily overriden if the file dsu_limit.asm is called.
!dsu_mapping
2
$7F8B50
$409C02
12
Mapping of each dynamic sprite. Set to the values from the table at !dsu_mapping_default on level load, but readily overriden if the file dsu_remap.asm is called. Bits are (and are expected to be) masked so the mappings are always in the range 0x0000-0x01FF.
!dsu_restore
1
$7F8B5C
$409C0E
1
Flag that enables graphics restoration if set. Set to !dsu_restore_default (which can only be either 0x00 or 0x01) on level load, but readily flipped if the file dsu_restore.asm is called. Set to 0x80 when the graphics are backed up to the buffer to ensure the process is only done once.
!dsu_frame
15
$7F8B5D
$409C0F
6
Current frame of each dynamic sprite. A dynamic slot is considered free if its entry in this table is equal to 0xFF. 0x00-0x7F are valid frames, and 0x80-0xFE are invalid frames that still indicate a taken slot. Set to 0xFE when the DynamicAssignSlot or DynamicTransferSlot routines are called to mark the slot as taken and force a valid frame to be uploaded when the DynamicGetSlot routine is called. Cleared (to 0xFF) on level load and on frame end when a dynamic sprite is no longer alive or no longer matches !dsu_sprite_num.
!sprite_dsu_slot
21
$7F8B63
$409C15
!SprSize
Custom sprite table 1.
  • If the sprite is not using a dynamic slot, holds the value of 0xFF.
  • If the sprite is a single slot dynamic sprite, holds the dynamic slot (0x00-0x05).
  • If the sprite is a multi slot dynamic sprite, uses the bitwise format %-m543210, with m = 1 and bits 0-5 set if this sprite is occupying that corresponding dynamic slot and clear otherwise.
Cleared (to 0xFF) on level load and on frame end when the dynamic sprite is no longer alive or no longer matches !sprite_dsu_num.
!dsu_sprite_num
21+!SprSize
$7F8B6F
$409C2B
!SprSize
Custom sprite table 2. Holds a copy of the sprite's !new_sprite_num table. This is used as a failsafe if a dynamic sprite spawns on the same sprite slot as a different dynamic sprite that just despawned on the same frame. If the sprite numbers don't match, the old dynamic slot is cleared.
!dsu_freeram_nmi
Define
Offset
Default
Length (B)
Description
LoROM
SA-1
!dsu_src_bank
0
$0B06
$6B06
6
Bank byte of the source address of each upload.
!dsu_src_1
6
$0B0C
$6B0C
12
Middle and low bytes of the source address of each upload, first line.
!dsu_dest_1
18
$0B18
$6B18
12
VRAM destination address of each upload, first line.
!dsu_src_2
30
$0B24
$6B24
12
Middle and low bytes of the source address of each upload, second line. Always equal to !dsu_src_1+$0100.
!dsu_dest_2
42
$0B20
$6B20
12
VRAM destination address of each upload, second line. Always equal to !dsu_dest_1+$0100.

!dsu_gfx_backup, located at $7F8B7B by default, is made equal to !dsu_freeram_restore. It reserves 512 bytes to back up each of up to six mapped regions, up to 3072 bytes.


Acknowledgements

I would like to thank the following people for making dsu possible or supporting its development in one way or another:


Changelog

Version 1.0.1 (May 22nd, 2026):

Version 1.0.0 (May 21st, 2026):

Version 0.7.3 (May 15th, 2025):

Version 0.7.2 (May 10th, 2025):

Version 0.7.1 (May 7th, 2025):

Version 0.7.0 (May 1st, 2026):

Version 0.6.0 (April 21st, 2026):

Version 0.5.1 (April 17th, 2026):

Version 0.5.0 (April 16th, 2026):

Version 0.1.0 (April 15th, 2025):