Dynamic Xtreme internal workings
| English | Português | Español | 日本語 |
This article explains how Dynamic Xtreme works, from preparing resources during installation to loading and drawing a pose during gameplay.
What installation prepares
Before a sprite can use its graphics or palette, Dynamic Xtreme must store them in the ROM and prepare the information needed to find them. The same applies to any other dynamic resource you add.
The tool reads DynamicInfo, DrawInfo, and PaletteEffect files. Each provides a different part: which resources to load, how to draw poses, and which color changes can be applied.
This generates defines in DXDefines.asm that make it easy to use the resources and call the required routines.
DynamicInfo: resources and upload data
A DynamicInfo brings together the pose graphics, palettes, and general resources needed by a sprite or other code. For dynamic pose graphics, the tool arranges the data in the tile layout expected by the system.
Several properties of each pose are stored in separate tables. All tables use the same order: a pose's entry in one table corresponds to the same pose in the others. In the table below, N is the number of installed dynamic poses.
| Table | Total bytes | Bytes per value | Contents and purpose |
|---|---|---|---|
PoseSize |
2 × N | 2 | Amount of data used by each pose, in bytes. Used to check whether its upload fits within the frame's budget. |
Pose16x16Blocks |
N | 1 | Number of 16×16 blocks to allocate in VRAM for the pose. This does not always match the exact byte count copied, since an incomplete tile can still require a block of space. |
PoseResource |
3 × N | 3 | ROM address of each pose's graphics, including the bank, so upload code can locate the full source address. |
PoseResourceSizePerLine |
4 × N | 2; two values per pose | Two sizes per pose, corresponding to the two sections its graphics may be split into. The upload routine uses them to place the data correctly in VRAM. |
PoseSecondLineOffset |
2 × N | 2 | VRAM offset from the start of the first section to the start of the second. Places the second section correctly even if the first ends with an incomplete row. |
These tables describe every installed pose, not just those on screen. The cache separately tracks which poses are loaded and where they are stored.
For example, when a requested pose is not yet in VRAM, the system checks how many bytes it needs to upload, finds space for its blocks, then uses its source address and section data to prepare the upload.
Palette tables
Palettes declared in DynamicInfo files are collected in PaletteTable. If P is the number of entries across the DynamicInfo palette lists:
| Table | Total bytes | Bytes per value | Contents |
|---|---|---|---|
PaletteTable.IDs |
2 × P | 2 | ID of each palette. |
PaletteTable.Addresses |
3 × P | 3 | ROM address of its colors. |
P counts list entries, not just unique palettes: a shared palette may appear in more than one DynamicInfo.
Both lists follow the DynamicInfo palette order. Each DynamicInfo's offset define identifies where its list starts. Selecting a version adds that index to the offset to find both the ID and address of the chosen colors.
The ID identifies a palette so it can be shared; the address allows its colors to be loaded. Neither is the destination palette number: the same installed palette may be assigned to palette 9 or C, for example.
General resources and defines
Files declared in Resources are stored in ROM. Their address and byte count are available through defines generated in DXDefines.asm. For example, my_resource.bin generates:
!ResourceMyResource: resource address in ROM.!ResourceMyResourceSize: resource byte count.!ResourceIDMyResource: resource ID.
Names are normalized to PascalCase: spaces, hyphens, and underscores are removed, and each word starts with a capital letter.
DrawInfo: drawing data and routines
The DrawInfo describes how a pose is built: its tiles, each tile's position and size, and how the pose changes when flipped.
During installation, Dynamic Xtreme generates drawing routines optimized for the parameters used by the tiles. For example, it considers whether tiles need X and Y offsets, share a size or common properties, and require flipping.
The resulting routine only handles options that the drawing needs. Several poses can share a routine when they need the same handling; this does not mean they also share graphics or IDs.
These tables connect a pose to its drawing data. D is the number of drawing poses, and R is the number of generated routines:
| Table | Total bytes | Bytes per value | Contents and purpose |
|---|---|---|---|
TableOffset |
2 × D | 2 | Starting offset of the pose's data in its drawing routine's tables. Locates its tiles and offsets without scanning earlier poses. |
NumberOfTilesMinus1 |
D | 1 | Number of tiles in the pose minus one. A four-tile pose stores 3; the routine uses this to loop through its tiles. |
GraphicRoutineIDs |
D | 1 | Drawing routine used by each pose. Poses with compatible requirements can use the same routine ID. |
GraphicRoutine |
3 × R | 3 per routine | Drawing routine addresses. The routine ID selects the code to run. |
Each routine also has the tables its poses need. Here, T is the number of entries in each of the routine's tables:
| Data | Total bytes | Bytes per value | Description |
|---|---|---|---|
Tiles |
T, if generated | 1 | Tile numbers making up the pose. |
Properties |
T, if generated | 1 | Each tile's properties, such as flipping and priority relative to the layers. |
XDisplacements and YDisplacements |
T per table, if generated | 1 | Each tile's position relative to the pose's origin. |
| Flip displacement tables | T per table, if generated | 1 | Positions used when flipping a pose that requires a different layout. |
Sizes |
T, if generated | 1 | Each tile's size: 8×8 or 16×16. |
Not every routine needs all these tables. If a value is the same for all tiles or can be obtained from a shared value, the tool can generate a routine that uses it directly.
This optimization happens during installation. For example, when all tiles share a size, the routine can use that common value instead of reading a separate size for each tile.
PaletteEffect: effect data
A PaletteEffect file contains a list of color changes. Installation assigns IDs to them and stores their data in PaletteEffectsTable.
As with pose tables, each effect occupies the same position across the different lists. Effect IDs start at 1; ID 0 means no global effect. In the table, E is the number of installed effects; together, the tables occupy 2 + 7 × E bytes.
| Table section | Total bytes | Bytes per value | Contents |
|---|---|---|---|
Length |
2 | 2 | Number of installed effects. |
Types |
E | 1 | Each effect's type, such as MixH, MixS, or PalTransitionRGB. Selects the calculation to perform.
|
Value1, Value2, and Value3 |
E per table; 3 × E combined | 1 per table | Channel values used by the effect. For transitions, these three bytes store the target palette address instead. |
Ratio1, Ratio2, and Ratio3 |
E per table; 3 × E combined | 1 per table | How much of the change to apply to each component. Range: 0–127. |
For a transition toward another palette, the tool also prepares that palette. It converts the colors to the RGB or HSL format needed by the transition and stores them in ROM. During gameplay, this target can be used without converting it again on every call.
These transition palettes differ from normal palettes uploaded to CGRAM: they contain three components per color and are used to calculate effects.
How code finds installed resources
DXDefines.asm provides the IDs, addresses, sizes, and offsets resources need. For example, !DynamicPoseIDMySpriteIdle identifies a pose, and !PaletteEffectIDMyEffect0 identifies a file's first effect.
Names are normalized to PascalCase: separators such as spaces, hyphens, and underscores are removed, and each word starts with a capital letter. For example, my_sprite becomes MySprite.
Installation also generates BufferTable, which stores the addresses of the blocks containing inserted resources. Each address occupies 3 bytes, followed by a final $FFFFFF value occupying another 3 bytes. For B blocks, the table occupies 3 × (B + 1) bytes. The tool uses this information to find the previous installation's data when reinstalling.
Reinstallation may change IDs and addresses. Reinsert any sprites, blocks, and UberASM code that use the generated defines afterward.
Work performed each frame
The main game logic runs the player, sprites, and other code for the current game mode. Code using Dynamic Xtreme requests poses and palettes, prepares local effects, and calls drawing routines. Accepted uploads are added to the transfer lists for NMI.
Once that logic finishes, Dynamic Xtreme's game mode code performs the tasks enabled by the configuration: preparing VRAM updates for Kevin's optimization, maintaining palette state, and calculating pending global effects.
During NMI, Dynamic Xtreme then performs these tasks in order, according to the enabled features:
- Resets
DX_Dynamic_CurrentDataSendto 0. - Increments
DX_Timer. - Increments
DX_SyncTimerif the action is not frozen and the game is not paused. - Performs graphics transfers to VRAM.
- Performs color transfers to CGRAM.
- Updates color 0,0 if the effect system has a pending result.
The following sections therefore follow this order: main logic → tasks at the end of the game mode → NMI transfers.
Main logic
Each resource decides what it needs. A sprite may change pose, an animation may request new graphics, or UberASM code may upload a palette or select an effect.
If the requested pose is already loaded, Dynamic Xtreme updates its last-use time to protect it from replacement without uploading its graphics again. Otherwise, it checks the frame's data budget and finds available VRAM space before preparing the upload.
Drawing is also prepared here: routines write the positions, tiles, and properties of the pieces to display into the OAM buffers.
A request can fail, for example because its data would exceed the remaining budget. The requesting code must check the result and decide whether to keep the previous state or retry later. A rejected upload is not automatically saved for another frame.
Game mode tasks
This stage performs the following tasks, according to the enabled features.
Before game logic runs:
- If
ControllerOptimizationis enabled, the controller routine runs here instead of during NMI. - Checks whether Dynamic Xtreme should run in the current game mode. If not, its variables are reset. This happens every frame spent in a disallowed mode.
The normal game logic then runs, including sprites and code requesting uploads or palette changes.
After that logic finishes:
- If Kevin's VRAM optimization is enabled, Lunar Magic's update destinations are adjusted before the transfers take place.
- Palette update flags are cleared to 0, and protection timers greater than 0 are decremented. This is skipped while paused and in disallowed modes.
- Applies global effects to palettes that need an update and prepares their color transfers. This is also skipped if the previous check prevents further processing.
VRAM and CGRAM transfers happen later, during NMI. This stage prepares what will be copied.
Allowed game modes
Dynamic Xtreme uses two game mode checks:
- General system:
Routines_AllowedGameMode, exposed as!AllowedGameMode. Determines whether the system is reset, its NMI runs, and palette tasks are performed. Palette assignment macros also use this check when processing an upload from a DynamicInfo. - Sprite routines:
DXAllowedGameModeuses a separate table.!Uploadand its general macros use a table with the same values to decide whether to process a new request.
Enabling the general system therefore does not mean every sprite function accepts new uploads in that mode. This table shows the current checks; IDs are hexadecimal:
| Game mode | SMW screen or stage | General system | DXAllowedGameMode / new Upload requests |
|---|---|---|---|
$06 |
Title screen spotlight opening. | Yes | No |
$07 |
Title screen. | Yes | Yes |
$08 |
File select. | Yes | No |
$09 |
File erase. | Yes | No |
$0A |
One- or two-player selection. | Yes | No |
$0C |
Overworld loading. | Yes | No |
$0D |
Overworld fade-in. | Yes | No |
$0E |
Overworld. | Yes | Yes |
$12 |
Level preparation. | Yes | No |
$13 |
Level fade-in. | Yes | No |
$14 |
In-level gameplay. | Yes | Yes |
$15 |
Fade-out to Game Over / Time Up. | Yes | No |
$1B |
Castle cutscene or credits. | No | Yes |
$1C |
Fade-out to Yoshi's House in the credits. | Yes | No |
$1D |
Loading Yoshi's House in the credits. | Yes | No |
$1E |
Yoshi's House fade-in during the credits. | Yes | No |
$1F |
Yoshi's House in the credits. | Yes | Yes |
$24 |
Enemy roll fade-in. | Yes | No |
$25 |
Credits enemy roll. | Yes | Yes |
$29 |
The End screen. | No | Yes |
Other values from $00 to $2F. |
— | No | No |
Stage names follow the SMW game modes documented in SMWDisX; permissions come from Dynamic Xtreme's tables.
When Upload does not allow a new request, it checks whether the resource has a previous pose. If so, it updates that pose's last-use time and returns carry set; otherwise, it returns carry clear. It does not request another pose on this path.
Modes $1B and $29 show why the columns must be read separately: passing the sprite check does not enable the general system's NMI or prevent the game mode code from resetting its variables. The sprite column alone does not guarantee full operation in those modes.
Transfers during NMI
NMI is an interrupt the SNES can generate at the start of VBlank, after drawing the visible part of the screen. The game uses this interval to update graphics, colors, and other video data.
The CPU runs both the game logic and the NMI routine. During game logic, it moves sprites and prepares upload requests. At the start of VBlank, an NMI can temporarily interrupt that logic to handle video updates. When the NMI routine finishes, the CPU resumes the interrupted code.
This is the usual sequence within a frame:
| Stage | What the CPU does | What happens to graphics |
|---|---|---|
| Game logic | Moves sprites, selects poses, calculates effects, and prepares drawing. | Accepted requests are stored in the transfer lists. |
| VBlank, during NMI | Runs the NMI routine and sets up DMA transfers. | Graphics are copied to VRAM, colors to CGRAM, and OAM data to OAM memory. |
| After NMI | Resumes the interrupted code. | The updated data is used to draw the next image. |
This table shows the usual sequence; during slowdown, SMW may run a shortened NMI, as explained in the DX_Timer section.
In allowed modes, Dynamic Xtreme performs these tasks in order, according to the enabled features:
- Resets
DX_Dynamic_CurrentDataSendto 0 to count requests from the next logic cycle. Transfers already prepared remain in their lists. - Increments
DX_Timer. - Checks
$9Dand$13D4|!addr. If both are 0, incrementsDX_SyncTimer. - Performs pending VRAM transfers if
GraphicsChangeis enabled. - Performs pending CGRAM transfers if
PalettesChangeis enabled. - If
PalettesEffectsis enabled, updates color 0,0 when a result is pending.
The copies use DMA. NMI reads each request from the lists, sets the source address, byte count, and destination, then starts the transfer.
VBlank time is limited, and SMW also needs it for its own screen updates. To limit data sent in one frame and prevent flickering, Dynamic Xtreme uses DX_Dynamic_CurrentDataSend to count requested bytes and DX_Dynamic_MaxDataPerFrame to set the limit. Before accepting a normal upload, it checks whether the additional bytes would exceed that limit.
Transfer lists
Each request stores the source address and bank, byte count, and VRAM or CGRAM destination.
NMI processes the lists from the last entry to the first, then empties them. This does not erase the uploaded graphics or colors: they stay at their destinations until other code changes them.
The VRAM list uses 0 when empty. The CGRAM list uses $FF because it stores the last entry's index rather than the entry count directly.
Code writing to the same destination must coordinate its uploads. Request order does not replace deciding which resource may use each region.
Resources are normally read from ROM. If a resource is in a RAM buffer, you must keep its contents unchanged until NMI performs the transfer. For example, a palette prepared in RAM must not be modified or its buffer reused before the copy. The list stores the address, not a copy of the data; Dynamic Xtreme does not protect the buffer from changes made by other code.
Data budget
Transfer macros such as %TransferToVRAM() and %TransferToCGRAM() check the requested byte count in DX_Dynamic_CurrentDataSend. If the new upload would exceed DX_Dynamic_MaxDataPerFrame, it is rejected. Accepted uploads add their bytes to the counter.
The default limit is $0800, or 2048 bytes. It helps prevent flickering caused by sending too much data in a frame. It limits data volume, rather than measuring the exact time each transfer takes.
DX_Dynamic_CurrentDataSend tracks the requested byte count. The check CurrentDataSend + resource bytes > MaxDataPerFrame rejects a normal upload that would exceed the budget, limiting the data transferred during VBlank.
Dynamic pose requests return the same failure result for insufficient budget and insufficient usable VRAM space. The call does not identify which condition prevented the upload. Failure does not mean VBlank was overloaded either: when the budget causes a rejection, the upload is rejected specifically to prevent that overload.
Variants such as %ForcedTransferToVRAM() and %ForcedTransferToCGRAM() accept uploads without checking the limit. Their bytes still count toward DX_Dynamic_CurrentDataSend, leaving less room for later normal uploads. They are used for priority uploads, such as the player's graphics; they do not prevent flickering if too much data is requested.
See PPU in the ASM API for the difference between byte budget and list capacity, transfer restrictions, and the checks your code must perform.
The two timers
DX_Timer: protecting requests
DX_Timer advances when Dynamic Xtreme's NMI code runs. Besides measuring how long a pose has gone unused, it prevents a resource from repeating requests before its previous ones have been processed.
A logic cycle does not always correspond to a full NMI execution. During slowdown, SMW may run a shortened NMI, allowing two logic cycles to occur without Dynamic Xtreme's transfer lists being processed between them.
Without protection, a resource could add its uploads to those lists again. Duplicate requests would take more space and could cause flickering.
To prevent this, sprite pose upload routines compare DX_Timer with a two-byte value stored by the sprite, usually called SafeFrame. The sprite aliases use !SafeFrameLowByte,x and !SafeFrameHighByte,x.
The process is:
- Compare the timers. If
SafeFrameequalsDX_Timer, that sprite has already processed its pose upload at this timer value. The routine skips a new request. - Record the attempt before processing it. If the values differ, it copies
DX_TimertoSafeFrame, then checks whether it can reuse the pose or must request another. - Check the result. A new pose request can succeed or fail. Even if it fails, the value saved in
SafeFrameis not undone.
SafeFrame therefore records when the last attempt was processed, not when a pose was successfully loaded. If an upload fails, calling the routine again with the same DX_Timer does not allow another attempt, even if the desired pose or available space has changed.
In !Upload and !UploadWithoutPoseOffset, rejected requests and calls skipped by this protection use the same fallback:
- If
!LastPoseHashIndex,xis not$FF, they update that pose's last-use time to protect it from replacement and return carry set. The sprite can keep drawing the last available pose. - If it is
$FF, no fallback pose exists and they return carry clear.
For example, this can happen during slowdown:
DX_Timer = $0030. The sprite saves$0030in itsSafeFrameand tries to obtain a new pose. No data budget remains, so the request fails; it keeps the previous pose if one exists.- SMW runs another logic cycle without processing Dynamic Xtreme's NMI. The timer is still
$0030. The same sprite's next call detects the match and does not repeat the request. - When Dynamic Xtreme's NMI runs,
DX_Timeradvances to$0031. On the next call, the timers no longer match and the sprite can retry. A rejected request does not retry itself: the sprite code must call the routine again.
Sprite pose upload aliases enable this protection. In GeneralUpload and Upload (general macro), safeCheck controls it; checkLast separately controls fallback to the previous pose. TakeDynamicRequest and PPU transfer macros do not perform this SafeFrame check for you.
DX_Timer continues advancing while paused whenever this part of NMI runs.
DX_SyncTimer: synchronized animations
The main purpose of DX_SyncTimer is to give sprites a shared animation clock. For example, sprites using the same poses can use it to select a pose every four frames.
They then change together and request the same pose. The first request may need to upload it; later requests reuse it without scheduling another graphics upload. This saves DMA transfers and data per frame.
With independent timers, sprites could use different poses from the same animation at once, requiring more uploads and VRAM space.
DX_SyncTimer stops while paused or while the action is frozen, so synchronized animations stop too.
Graphics uploads: GraphicsChange
GraphicsChange stores a list of upload requests in RAM. During NMI, those requests are processed and their data is copied to VRAM.
List variables
Here, V is !VRAMTransferSize, which defaults to 64. Each of the four request tables allocates V bytes; with 2-byte entries, that allows 32 requests. Together with the counter, they occupy 1 + 4 × V bytes: 257 by default.
| Variable or table | Total bytes | Bytes per value | Contents |
|---|---|---|---|
DX_PPU_VRAM_Transfer_Length |
1 | 1 | Number of pending requests. 0 means the list is empty. |
DX_PPU_VRAM_Transfer_Source |
V; 64 by default | 2 | Each request's source address, excluding the bank. |
DX_PPU_VRAM_Transfer_SourceBNK |
V; 64 by default | 2 allocated; 1-byte bank | Source bank. |
DX_PPU_VRAM_Transfer_SourceLength |
V; 64 by default | 2 | Number of bytes to copy. |
DX_PPU_VRAM_Transfer_Offset |
V; 64 by default | 2 | VRAM destination address. |
Tables use the same order: the first entry of each table belongs to the first request, the second to the second request, and so on. Each entry occupies 2 bytes, so offsets advance through $00, $02, $04, and so on.
These names represent RAM addresses calculated in DXDefines.asm. Addresses can change with the configuration and enabled features.
Adding a request
%TransferToVRAM() performs these steps:
- Calculates the total byte count if the new upload is added to
DX_Dynamic_CurrentDataSend. - Compares it with
DX_Dynamic_MaxDataPerFrame. If it exceeds the limit, returns carry clear without adding the request. - Uses
DX_PPU_VRAM_Transfer_Lengthto find the next entry and increments that counter. - Stores the source, bank, byte count, and destination in the tables.
- Updates
DX_Dynamic_CurrentDataSendwith the upload's byte count and returns carry set.
%ForcedTransferToVRAM() stores the same information without checking the byte limit. FromAddr and Resource variants make it easier to get the source from a full address or resource defines.
Adding a request does not copy graphics immediately: it prepares the information for NMI.
Processing requests during NMI
The VRAMDMA routine:
- Reads
DX_PPU_VRAM_Transfer_Length. If it is 0, returns because there are no requests. - Calculates the last entry's offset:
(Length - 1) × 2. - Reads its source, bank, and byte count into the DMA registers.
- Writes the VRAM destination to
$2116-$2117and starts the transfer through$420B. - Moves back 2 bytes to process the previous request, repeating until the first is complete.
- Writes 0 to
DX_PPU_VRAM_Transfer_Lengthto mark the list as empty.
The list runs from last to first. There is no need to clear every stored value afterward: with the length at 0, the next request reuses the first entry.
Animations with a fixed destination
Animation code must:
- Select the image for the current animation step.
- Calculate its source address.
- Request a transfer to the animation's VRAM destination.
- Check carry. If the upload is rejected, keep the previous image and retry.
The destination can stay the same. Only the image copied there changes; the transfer list does not decide when the animation advances.
See Creating graphics animations in the UberASM guide for complete code.
Loading a dynamic pose
Pose loading uses the cache to avoid uploading graphics that are already available:
- Looks for the ID in the hash table.
- If already registered, updates its last-use time without adding another upload.
- Otherwise, checks the byte count and finds suitable space in the VRAM map.
- Selects poses that can be replaced, considering their time since last use and the new pose's space requirements.
- Updates the cache tables and map.
DynamicRoutinesplits the pose's data into the required transfers.- NMI performs those transfers.
Dynamic sprites: DynamicPoses covers each step, including hash lookup, the map, replacement policy, and splitting uploads into rows.
Palette uploads and assignment: PalettesChange
The system distinguishes two values:
- Palette ID: identifies colors stored in ROM.
- Destination color: specifies the starting color in CGRAM for the uploaded colors. For example,
$91starts at color 1 of palette 9.
Automatic assignment lets sprites share colors without requiring a fixed destination for each sprite.
CGRAM upload list
Here, C is !CGRAMTransferSize, which defaults to 64. The tables and counter allocate 1 + 7 × C bytes, or 449 by default. Although SourceBNK allocates 2 × C bytes, the code uses one bank byte per request.
| Variable or table | Total bytes | Bytes per value | Contents |
|---|---|---|---|
DX_PPU_CGRAM_Transfer_Length |
1 | 1 | Last pending request's index. $FF means the list is empty; 0 means one request.
|
DX_PPU_CGRAM_Transfer_Source |
2 × C; 128 by default | 2 | Source address, excluding the bank. |
DX_PPU_CGRAM_Transfer_SourceBNK |
2 × C; 128 allocated by default | 1 used per request | Source bank. |
DX_PPU_CGRAM_Transfer_SourceLength |
2 × C; 128 by default | 2 | Number of bytes to copy. Each color occupies 2 bytes. |
DX_PPU_CGRAM_Transfer_Offset |
C; 64 by default | 1 | First destination color in CGRAM. |
During NMI, CGRAMDMA:
- Starts at the last pending request.
- Configures DMA with that request's source and byte count.
- Selects the first color through
$2121and performs the copy. - Continues with earlier requests.
- Writes
$FFtoDX_PPU_CGRAM_Transfer_Lengthwhen finished.
TransferPalette macros also update the color copies and effect state:
- Without the global effect: request the upload and update SMW's buffers, preventing the game from reusing old colors during the goal victory animation.
- With the global effect: store the original colors and prepare the state needed to apply the effect before sending them to CGRAM.
Automatic assignment
| Variable or table | Total bytes | Bytes per value | Purpose |
|---|---|---|---|
DX_Dynamic_Palettes_ID |
32 | 2 | ID assigned to each destination. $FFFF marks a free entry; $FFFE marks one set aside.
|
DX_Dynamic_Palettes_DisableTimer |
16 | 1 | Protects each destination from automatic replacement while in use. |
DX_Dynamic_Palettes_EffectEnabled |
1 | 1; one bit per sprite palette | Tracks sprite palettes assigned with the global effect enabled. Sharing an assignment also requires this setting to match. |
The search follows this order:
- Reuse an assignment with the same ID and compatible effect setting.
- If none exists, use a free entry.
- If none is free, replace an entry that is not set aside and whose timer is 0.
- If no entry qualifies, report assignment failure.
While using the palette, the resource keeps its timer at 2. Reaching 0 does not erase the colors: the assignment can still be reused if another resource has not replaced it.
Manual assignment and version
- Manual with upload: code selects the destination and requests its colors.
- Manual without upload: code selects the destination and uses the colors already available there.
- Version: selects an entry in the DynamicInfo palette list. The last processed version lets the system check whether that selection changed.
Resources choosing the same destination with different colors must coordinate their uploads. Keeping a palette protected does not mean uploading its colors again.
Pose drawing: DrawingSystem
Drawing prepares a pose's pieces in OAM: the tiles to display, their positions, and their properties. The graphics data must be in VRAM.
Drawing steps
- Finds the pose's routine and data through
GraphicRoutineIDs,GraphicRoutine, andTableOffset. - Gets the tile count from
NumberOfTilesMinus1. - Calculates each tile's position from the sprite position, displacements, and applicable flips.
- Checks whether the tile is visible and OAM space remains.
- Writes its position, tile number, properties, and size to the OAM buffers.
The tile number depends on the drawing type:
| Case | How the tile is selected |
|---|---|
| Dynamic pose | Adjusted to the graphics' location in VRAM. |
| Static pose | Uses its fixed tile number. |
| Pose using a shared tile | Code must set !Tile before drawing.
|
Priority and available space
- In LoROM, the system tracks available OAM entries itself.
- In SA-1, it integrates with MaxTile.
- Drawing priority orders sprite tiles. It is separate from the property that places a tile in front of or behind layers.
- SNES per-scanline limits still apply. Loading a pose does not guarantee that all its tiles can be displayed.
Player and Yoshi
PlayerFeatures
The original player has its own routines to select and upload pose pieces. PlayerFeatures integrates that work with Dynamic Xtreme uploads and lets you change the graphics and palette sources.
With the original format, the game still selects the pose pieces. The routine gets their addresses from the configured source and compares them with the previous addresses to upload only those that changed.
These uploads use forced transfers, and their bytes also count toward later normal requests. They retain priority while remaining part of the system's data budget.
The player's palette receives special handling: it combines the game's own colors and selections with the custom source. It is more involved than replacing an ordinary sprite's entire palette.
Support for custom players lets other systems handle their poses and drawing differently. It does not automatically convert any spritesheet to the original player's format.
YoshiFeatures
Yoshi also uploads graphics in pieces. YoshiFeatures lets you change their source while retaining the layout expected by the routines.
The code calculates which pieces are needed and prepares uploads to the corresponding VRAM destinations. Like the player's uploads, these receive priority handling.
This option requires PlayerFeatures and shares its compatibility restrictions. Enabling Yoshi's features alone does not avoid a conflict with the player system.
Differences between LoROM and SA-1
The main differences in Dynamic Xtreme between these configurations are the memory used for its variables and the drawing system:
| Aspect | LoROM | SA-1 |
|---|---|---|
| Memory for variables | Free7E2000 is commonly used to provide space for Dynamic Xtreme's variables in the recovered RAM. If this option causes compatibility problems, it can be disabled in favor of the alternative configuration, which uses a tighter RAM allocation.
|
Does not use Free7E2000. The main variable areas are in BW-RAM, in banks $40 and $41.
|
| Drawing priorities | Uses Dynamic Xtreme's own system, with two drawing priorities. | Uses MaxTile, with four drawing priorities for tiles. |
LoROM's two priorities cannot preserve every ordering distinction available with SA-1's four priorities. Some resources that draw correctly on SA-1 may therefore appear in a different or incorrect drawing order on LoROM. These priorities order sprite tiles; they are separate from the priority that places tiles in front of or behind layers.
See RAM usage for each configuration's memory ranges.
Memory and optimizations
Free7E2000
SMW keeps original player, Yoshi, and some animation graphics in RAM. In ROMs without SA-1, this option adapts uploads to read that data from ROM instead.
Dynamic Xtreme uses the recovered RAM for its internal variables. It is not free space for other code to overwrite.
SA-1 uses different addresses and disables this option. See the ranges in RAM usage.
ControllerOptimization
This option moves controller processing from NMI to the game mode code. It frees time for screen updates without adding a frame of input delay or changing which buttons the game receives.
FixedColorOptimization
The console uses the fixed color for color math. To update it, the game prepares its components and sends them to the corresponding register.
This optimization replaces that preparation with a shorter sequence. The color is still sent to the console, with less work required during NMI.
KevinLMVRAMOptimization
Lunar Magic uses lists called stripe images to update parts of the tilemaps. These contain both the data and destination information.
The integrated version of Kevin's optimization prepares part of the destination adjustment outside NMI, leaving less setup work for transfer time.
This option replaces the standalone original patch. It must not be uninstalled after installation; keep this in mind before enabling it.
StatusBarOptimization
The original status bar updates specific VRAM regions. This option prepares its transfers more directly, reducing the DMA setup needed during NMI.
It modifies updates to the original status bar rather than adding a new one. As a result, it conflicts with resources that replace, modify, or remove that bar.
Dynamic sprites: DynamicPoses
The system works as a pose cache in VRAM. Instead of keeping a graphics copy for each sprite, it stores a pose that every resource needing it can reuse.
- A pose is uploaded only once while it remains in the cache.
- Later requests update its last-use time without adding another graphics upload.
- A pose is not erased when a resource stops using it. It stays until its space is needed and it is selected for replacement.
- If replaced and later requested again, it must be uploaded again.
Replacement depends on time since last use, not time since upload, as well as whether the space fits the new pose.
Three different values
| Value | What it identifies |
|---|---|
| Pose ID | The installed pose. A 16-bit value used to look up its data in ROM. |
| Hash table index | The RAM entry that records the pose. Ranges from 0 to 127. |
| VRAM block | ID of one of the 128 16×16 tile positions in sprite VRAM. Numbered left to right and top to bottom, from 0 to 127. A pose occupying several blocks stores its first block's ID. |
The hash table index and VRAM block do not need to match. A table entry can record a pose starting at any allowed block.
Registered pose tables
| Variable or table | Total bytes | Bytes per value | Contents |
|---|---|---|---|
DX_Dynamic_Pose_Length |
1 | 1 | Number of registered poses. |
DX_Dynamic_Pose_HashSize |
128 | 1 | Number of registered poses for each hash. |
DX_Dynamic_Pose_ID |
256 | 2 | Full pose ID in each entry. $FFFF marks a free entry.
|
DX_Dynamic_Pose_Offset |
128 | 1 | Starting VRAM block of each pose. |
DX_Dynamic_Pose_TimeLastUse |
256 | 2 | DX_Timer value at the pose's last use.
|
These tables occupy 769 bytes of RAM. They track poses registered during gameplay, whereas the DynamicInfo-generated tables describe every installed pose.
Looking up a pose
DynamicPoseHashmap_FindPose uses a 128-entry hash table. The hash selects where to start searching:
hash = pose ID AND $007F
Different IDs can share a hash. For example, $0005 and $0085 both produce $05. The hash alone therefore cannot confirm that a pose is loaded; the full ID must also match.
The search follows these steps:
- Saves the requested ID and calculates its hash.
- If
DX_Dynamic_Pose_Lengthis 0, no poses are registered and the search ends. - Checks
DX_Dynamic_Pose_HashSize[hash]. If it is 0, no registered pose has that hash, so the search also ends. - Starts at the entry whose index matches the hash.
- Reads that entry's ID. If it matches the requested ID, the pose has been found.
- Otherwise, continues to the next entry in the search sequence.
The sequence advances 15 entries per attempt, wrapping around after 127:
next index = (current index + 15) AND $7F
With 128 entries, this step visits every position before repeating one. For hash $05, the sequence starts:
$05 → $14 → $23 → $32 → $41 → ...
The pose count for each hash
Each hash has its own counter in DX_Dynamic_Pose_HashSize. For example, if two registered poses have IDs that produce hash $05, DX_Dynamic_Pose_HashSize+$05 contains 2.
This number tells the search how many poses have that hash, not how many positions to visit. The search uses a temporary copy of the counter:
- Starts with the registered pose count for the requested hash.
- Visits positions in the sequence until it finds the requested ID or has checked that many poses with the same hash.
- Each time it finds a pose with the same hash but a different ID, decrements the temporary counter.
- Free positions and poses with other hashes do not decrement it; the search continues.
- If the temporary counter reaches 0 without finding the ID, the requested pose is not registered and the search ends.
The counter in DX_Dynamic_Pose_HashSize does not change during lookup. It changes only when poses are added to or removed from the cache.
For example, suppose the requested ID is $0105, which produces hash $05, and that hash's counter is 2:
| Visited position | Stored ID | ID's hash | Hash $05 poses still to check |
|---|---|---|---|
$05 |
$0005 |
$05 |
1: same hash, but a different ID. |
$14 |
$0014 |
$14 |
1: different hash, so the counter is unchanged. |
$23 |
$0085 |
$05 |
0: both poses with this hash have been checked. $0105 is not registered.
|
Usually, each hash has one or two poses. This allows lookup with few comparisons and a typical cost close to O(1). It does not mean only one or two positions are always visited: entries with other hashes may lie between them. Many collisions can make the search longer.
A free entry does not end the search. It may have been emptied by replacing a pose, with another pose of the same hash still farther along the sequence. The counter tells the search when all poses with that hash have been checked, even with gaps between them.
When the pose is found
TakeDynamicRequest:
- Gets the matching entry's index.
- Writes the current
DX_Timervalue to that entry'sDX_Dynamic_Pose_TimeLastUse. - Reports request success without scheduling another graphics upload.
All resources requesting that ID therefore use the same pose and keep it protected. They may be normal, cluster, or extended sprites, or any other code using the system.
This also prevents duplicate uploads within one cycle: once the first request is accepted, the pose is registered even though the actual transfer will occur during NMI.
When the pose is not registered
TakeDynamicRequest proceeds with a new upload:
- Reads
PoseSizeand calculatesDX_Dynamic_CurrentDataSend + PoseSize. - If the result exceeds
DX_Dynamic_MaxDataPerFrame, returns failure without changing existing poses. - Reads
Pose16x16Blocksto find the required block count. - Calls
VRAMMap_GetBestSlotto find suitable space. - If no usable space exists, returns failure.
- Removes poses occupying the chosen space from the map and hash table.
- Finds a free hash table entry and registers the new pose.
- Updates the VRAM map and frame data counter.
- Calls
DynamicRoutineto add the transfers.
The budget check happens before replacing poses. If the new upload cannot fit in the frame, no space is freed for a request that cannot be fulfilled.
Adding and removing hash table entries
DynamicPoseHashmap_FindFreeSpacelooks for an entry whose ID is$FFFF, using the same sequence of 15-entry steps.DynamicPoseHashmap_Addstores the ID and last-use time, incrementsDX_Dynamic_Pose_Length, and increments the corresponding hash counter.DynamicPoseHashmap_Removewrites$FFFFto the removed ID and decrements both counters.
Removing an entry does not itself erase VRAM pixels. It marks the pose as unavailable in the cache; the new transfers overwrite the chosen space.
Representing the VRAM map
The map represents 128 16×16 tile positions in sprite VRAM, numbered left to right and top to bottom. Each block is one of those positions, not a contiguous 128-byte range.
A 16×16 tile consists of four 8×8 tiles arranged in two rows. The map manages the positions occupied by a pose; DynamicRoutine later handles how its data must be distributed for upload.
| Table | Total bytes | Bytes per value | Contents |
|---|---|---|---|
DX_Dynamic_Tile_Offset |
128 | 1 | Section's starting position. Bit $80 marks it as restricted: dynamic poses cannot use this space.
|
DX_Dynamic_Tile_Size, also called DX_Dynamic_Tile_Pose |
128 | 1 | If bit $80 is set, the remaining bits hold the section's position count minus one. Otherwise, they hold the occupying pose's hash index. Check Offset to determine whether the space is restricted.
|
These two tables occupy 256 bytes. Together with the pose tables, the manager uses 1025 bytes of RAM, excluding transfer lists and working scratch memory.
Map sections
A section groups consecutive map positions that are restricted, free, or occupied by one pose. Its information is stored at the first and last positions, allowing traversal to jump to the previous section without checking every position in between.
The following examples show three possible states for a 6-position section from $20 to $25. These are alternatives, not three sections occupying the same space at once.
Example 1: restricted section
| Table entries | Value | Meaning |
|---|---|---|
DX_Dynamic_Tile_Offset+$20 and +$25 |
$A0 |
$80 marks the restriction; $20 identifies the start.
|
DX_Dynamic_Tile_Size+$20 and +$25 |
$85 |
The section has 6 positions: ($85 AND $7F) + 1 = 6.
|
Although the high bit of Size is set, this space is not free for dynamic poses: the restriction bit in Offset excludes it. It is not evaluated as a replaceable pose.
DynamicPoseSpaceConfig uses this section type to exclude parts of SP1–SP4 that dynamic sprites must not use.
Example 2: free section
| Table entries | Value | Meaning |
|---|---|---|
DX_Dynamic_Tile_Offset+$20 and +$25 |
$20 |
Starts at $20 and is not restricted.
|
DX_Dynamic_Tile_Size+$20 and +$25 |
$85 |
Bit $80 marks a free section's size: $05 + 1 = 6 positions.
|
This section is available. It can hold a pose needing up to 6 positions; placing a 4-position pose leaves the other 2 as a separate free section.
Example 3: section occupied by a pose
Suppose a 6-block pose is registered at hash entry $07 and occupies positions $20 through $25:
| Table entries | Value | Meaning |
|---|---|---|
DX_Dynamic_Tile_Offset+$20 and +$25 |
$20 |
The section starts at $20 and is not restricted.
|
DX_Dynamic_Tile_Size+$20 and +$25 |
$07 |
Bit $80 is clear: $07 is the pose's hash index, not its block count.
|
To find the position count and time since last use:
- Read the pose ID at
DX_Dynamic_Pose_ID+$0E. The offset is$0Ebecause each ID occupies 2 bytes and$07 × 2 = $0E. - Use that ID to read
Pose16x16Blocks, which contains 6 in this example. - Read
DX_Dynamic_Pose_TimeLastUse+$0Eto calculate how long the pose has gone unused.
DX_Dynamic_Pose_Offset+$07 stores $20, connecting the registered pose to its VRAM position.
Traversal and space search
VRAMMap_GetBestSlot starts at 16×16 tile position $7F. To traverse the map:
- Reads
DX_Dynamic_Tile_Offsetat the current position. - Gets the section's start with
Offset AND $7F. - Evaluates that section and spaces it can form with adjacent sections.
- Moves to
section start - 1, the previous section's last position. - Repeats until reaching the beginning of the map.
For example, if the last sections start at $70, $60, and $50:
| Visited position | Start read from Offset | Next position |
|---|---|---|
$7F |
$70 |
$6F
|
$6F |
$60 |
$5F
|
$5F |
$50 |
$4F
|
This traverses whole sections, not every tile in between.
What the traversal evaluates
A candidate space can consist of one section or several adjacent sections. At each step, the search checks:
- A restricted section cannot be used, and sections cannot be combined across it.
- A pose protected by recent use cannot be part of the space either.
- If the candidate is still too small, an adjacent section can be added to reach the required block count.
- Once the pose fits, the candidate is compared with the best one found: first by its time-since-last-use score, then by the smallest sufficient space.
A section is not chosen merely because it contains the longest-unused pose. The whole space must fit the new pose. When combining sections, the score depends on the most recently used pose within that space.
The search keeps these temporary values:
| Working data | Total bytes | Bytes per value | Purpose |
|---|---|---|---|
VRAMMapCurrentSpace |
3 | 1 per field | Start, score, and block count of the current candidate. |
VRAMMapBestSpace |
3 | 1 per field | Same fields for the best candidate found. |
VRAMMapTMP_Size |
1 | 1 | Block count needed by the new pose. |
These names refer to working scratch memory, not additional persistent RAM.
Replacement policy: time since last use and enough space
The system does not simply choose the first pose uploaded. It uses the time since the pose's last use:
time since last use = DX_Timer - pose TimeLastUse
VRAMMapSlot_GetSizeAndScore gets each region's size and score:
| Case | Score and handling |
|---|---|
| Free region | $FF. Preferred over occupied regions.
|
| Occupied region | Time since last use, capped at $FE.
|
| Unused for 0 or 1 timer increments | Cannot be replaced: the pose is still protected. |
| Restricted region | Excluded: dynamic poses cannot use this space. |
Protection requires at least two increments of DX_Timer without using the pose before it can be replaced. Each resource requesting it again updates its last-use time and protects it again.
Why the longest-unused pose is not always replaced
The new pose needs enough contiguous space. A small space alone is not useful, even if it has gone unused for a long time.
For example, consider a new pose needing 4 blocks:
| Candidate space | Time since last use | Suitable? |
|---|---|---|
| Pose A: 2 blocks, with no usable adjacent space | 20 | No: frees only 2 blocks. |
| Pose B: 4 blocks | 8 | Yes: fits the new pose. |
Here, B can be replaced while A stays. A has gone unused longer, but its space cannot satisfy the request.
If two or more adjacent regions can be replaced, the system can consider them together. It then:
- Adds their block counts.
- Uses the lowest score among them: the shortest time since last use.
- Excludes any restricted or protected region from the combined space.
This prevents a recently used pose from being overwritten just because it is next to a long-unused one.
Choosing among valid candidates
- The candidate must have at least the requested block count.
- The highest score wins: free space or the longest time since last use.
- If scores match, the smaller candidate wins, provided the pose fits.
- If score and size both match, the previously chosen candidate is kept.
Regions are combined during the search when the candidate is still too small. The system does not combine every replaceable region regardless of the requested size.
Replacing and splitting the chosen region
Once the space is chosen:
VRAMMap_RemovePosesInSpacetraverses its regions and removes poses being replaced from the hash table.VRAMMap_RemoveSpacemarks the chosen space as one free region by updating its endpoints.- A free hash entry is found, and the starting block is stored in
DX_Dynamic_Pose_Offset. DynamicPoseHashmap_Addregisters the pose.VRAMMap_AddPoseInSpacewrites the pose's hash index and region start at the first and last blocks.- Any leftover blocks become another free region.
If a 6-block region holds a 4-block pose, it becomes a 4-block occupied region and a 2-block free region. Dynamic Xtreme has no VRAM defragmentation system: it does not move loaded poses to bring free spaces together.
DynamicRoutine: preparing pose transfers
At this point, space has been chosen and the pose registered. DynamicRoutine turns that allocation into transfer requests.
It uses these values:
| Data | Amount read per pose | Purpose |
|---|---|---|
| Starting block | 1 byte | Position chosen by the VRAM map. |
OffsetToVRAMOffset |
2 bytes from a 256-byte table | Converts one of the 128 blocks to a VRAM offset. |
PoseResource |
3 bytes | Full ROM address of the pose data. |
PoseResourceSizePerLine |
4 bytes: two 2-byte values | Byte counts of the first and second sections. |
PoseSecondLineOffset |
2 bytes | VRAM offset for the second section. |
Converting the block to a VRAM address
A 16×16 block does not convert to a simple linear byte offset. Each block uses 8×8 tiles distributed across VRAM rows.
OffsetToVRAMOffset accounts for that layout. For example:
| Block | Offset | VRAM address |
|---|---|---|
$00 |
$0000 |
$6000
|
$01 |
$0020 |
$6020
|
$08 |
$0200 |
$6200
|
These VRAM addresses are measured in 2-byte words, not bytes. The routine adds the $6000 base used for sprite graphics.
Processing sections and rows
- Gets the source from
PoseResource. - Processes the first section's bytes through
DynamicRoutineLine. - If the second section's size is 0, returns.
- Otherwise, advances the source by the first section's byte count.
- Adds
PoseSecondLineOffsetto the VRAM offset and processes the second section.
DynamicRoutineLine splits each section according to its rows and starting position. For destination V measured in VRAM words, it prepares:
First row end = (V + $0100) AND $FF00 Bytes to that end = (first row end - V) × 2 Second part destination = first row end + $0100 Second part bytes in a group = $0200 - first part bytes
Sizes are byte counts, while VRAM destinations are word addresses. This is why the calculation multiplies by 2 to find how many bytes fit before the end of a row.
The routine uses three paths:
.EndLine: adds one final request for the remaining bytes..TwoLinesWithEndLine: adds two requests for the remaining data, each with its own destination and byte count..TwoLines: adds two requests totaling$0200bytes, advances the source and destinations, and repeats for the remaining data.
A direct path precedes the general loop: if the destination starts on a $0100-word boundary and the section contains at least $0200 bytes, the routine uses .EndLine to prepare the entire section as one request.
For example, a $0200-byte section starting at $6020 is split as follows:
| Request | Source | VRAM destination | Bytes |
|---|---|---|---|
| First | Section start | $6020 |
$01C0: 448
|
| Second | Section start + $01C0 |
$6200 |
$0040: 64
|
Together, the two requests copy the section's 512 bytes to the destinations calculated from the row layout. They are not two complete copies of the pose.
Each request uses these tables:
DX_PPU_VRAM_Transfer_SourceDX_PPU_VRAM_Transfer_SourceBNKDX_PPU_VRAM_Transfer_SourceLengthDX_PPU_VRAM_Transfer_Offset
Each added request increments DX_PPU_VRAM_Transfer_Length.
The pose's total byte count was already checked and added to DX_Dynamic_CurrentDataSend by TakeDynamicRequest. This stage splits that upload into transfers; it does not count the full pose again for each request.
DynamicRoutine does not run DMA. It prepares requests for VRAMDMA to process during NMI.
What the sprite keeps
Sprite routines store the usable pose's hash index, not a separate copy of its graphics. The index lets them look up its VRAM block and update its last-use time.
If a new pose cannot be loaded:
- A valid previous pose is kept, and its last-use time is updated to protect it.
- If no valid pose has ever been available, the code must not draw as though the request succeeded.
- Calls supporting fallback to the previous pose may return carry set because a pose is still usable, even though the change has not completed.
Animation and movement remain the sprite's responsibility. The cache reuses or prepares the requested graphics.
Example: two sprites request the same pose
Suppose two visible sprites, A and B, need dynamic pose $0005. Their palettes are ready, the pose is not yet in VRAM, and enough space and data budget remain.
- Sprite A requests the pose. Lookup does not find
$0005in the cache. The system chooses space, registers the pose, and prepares its graphics transfers. A stores the entry index in!LastPoseHashIndex,x. The graphics have not been copied to VRAM yet. - A prepares its drawing. The drawing routine uses the registered entry to calculate tile numbers for that VRAM space and writes positions and properties to the OAM buffers.
- Sprite B requests the same pose. Lookup now finds
$0005registered, even though its transfer is pending. It updates the last-use time without scheduling another upload. B stores the same cache index in its own!LastPoseHashIndex,x. - B prepares its drawing. It writes its own tiles to the OAM buffers, using the same graphics as A with its own position, palette, and flip settings.
- NMI updates video data. The pending pose transfers copy the graphics to VRAM, and the game transfers the OAM buffers. Both sprites use the same uploaded graphics in the next displayed frame.
Only one pose upload is prepared, even though two sprites use it. That upload may require several DMA transfers, depending on the graphics layout in VRAM.
Here, a pose being available during logic means it has a registered entry, allocated space, and an upload prepared for NMI. If its graphics were already in VRAM, they are reused directly without preparing another transfer.
If A's request were rejected, the new pose would not be registered and no upload would be pending. A would use its previous pose if available; otherwise, it would have no pose to draw. B would need to handle its own request and would not find an entry created by A's rejected attempt.
Palette effects: PalettesEffects
The system keeps the original colors so it can change effects, combine them, or restore the unaffected palette. An already affected result must not be treated as the original palette.
Color buffers
| Variable or buffer | Total bytes | Bytes per value | Contents |
|---|---|---|---|
DX_PPU_CGRAM_PaletteCopy |
512 | 2 per color | Base colors in SNES format, with 2 bytes per color. |
DX_PPU_CGRAM_BaseRGBPalette |
768 | 3 per color | Base colors split into red, green, and blue, with 3 bytes per color. |
DX_PPU_CGRAM_BaseHSLPalette |
768 | 3 per color | Base colors split into hue, saturation, and lightness, with 3 bytes per color. |
DX_PPU_CGRAM_PaletteWriteMirror |
512 | 2 per color | SNES-format result, ready for a CGRAM transfer request. Corresponds to the game's palette buffer. |
Per-palette flags
Each bit in these flags corresponds to one palette. BG variables cover 0–7; SP variables cover 8–F.
| Variables | Total bytes | Bytes per value | Meaning |
|---|---|---|---|
DX_PPU_CGRAM_BGBaseLoaded and DX_PPU_CGRAM_SPBaseLoaded |
1 per variable; 2 combined | 1; one bit per palette | Whether the SNES-format color base has been prepared. |
DX_PPU_CGRAM_BGBaseRGBPaletteLoaded and DX_PPU_CGRAM_SPBaseRGBPaletteLoaded |
1 per variable; 2 combined | 1; one bit per palette | Whether the base has been converted to RGB. |
DX_PPU_CGRAM_BGBaseHSLPaletteLoaded and DX_PPU_CGRAM_SPBaseHSLPaletteLoaded |
1 per variable; 2 combined | 1; one bit per palette | Whether the base has been converted to HSL. |
DX_PPU_CGRAM_BGEffectLoaded and DX_PPU_CGRAM_SPEffectLoaded |
1 per variable; 2 combined | 1; one bit per palette | Whether the global effect result is ready. |
These flags avoid repeating conversions and calculations whose results remain valid.
Preparing a base
- Checks whether the palette already has a registered base.
- If not, gets the colors from the game's buffer. An explicit upload, such as
TransferPalette, can supply that base directly. - Checks whether the effect needs RGB or HSL.
- If that conversion is missing, prepares it and marks it as available.
If only the effect changes, the base may remain valid. If the original colors change, upload calls update the base and invalidate conversions and results that must be recalculated.
Effect data
The ID selects data from the tables generated during installation:
Types: calculation to perform.Value1,Value2, andValue3: channel values, or a transition's target palette address.Ratio1,Ratio2, andRatio3: how much of each change to apply.
Channels range from 0 to 31. Ratios range from 0 to 127: 0 keeps the source unchanged, and 127 fully applies the selected change.
Calculations run outside NMI. NMI transfers the resulting colors.
Global effect
| Variable | Total bytes | Bytes per value | Purpose |
|---|---|---|---|
DX_Dynamic_Palettes_GlobalEffectID |
2 | 2 | Selects the global effect. 0 restores the base colors. |
DX_Dynamic_Palettes_GlobalBGEnable |
1 | 1; one bit per palette | Selects palettes 0–7 to receive the effect. |
DX_Dynamic_Palettes_GlobalSPEnable |
1 | 1; one bit per palette | Selects palettes 8–F to receive the effect. |
DX_Dynamic_Palettes_LastGlobalEffectIDPerPal |
32 | 2 | Last global effect ID processed for each palette. |
DX_PPU_ColorEffect_Enable |
1 | 1 | Controls whether the fixed color and color 0,0 are affected. |
For each enabled palette, the manager:
- Checks whether the effect result needs an update.
- Prepares the RGB or HSL base if missing.
- Calculates the effect if the previous result is no longer valid.
- Prepares SNES-format colors and requests their transfer.
- Records the processed ID when the corresponding upload is accepted.
If the effect and base remain unchanged, recalculation is unnecessary. Changing the global ID invalidates previous results, but not necessarily the RGB and HSL bases.
Global ID 0 also requires an update: the system must upload the base colors again to remove the previous effect.
Local effects and concatenation
A resource can have its own effect and also receive the global effect. For example, a sprite can change hue and then darken along with the level.
The order is:
- Prepare the local effect result.
- Use it as the global effect's base through the concatenation call.
- Calculate the global effect on that base.
- Upload the combined result without first displaying the local-only version.
- Use RGB concatenation for RGB effects.
- Use HSL concatenation for HSL effects.
- Resources sharing a CGRAM destination share its colors. To display two different local effects, they need destinations that can keep both results separately.
If the transfer is rejected
- Calculating an effect does not mean it is already in CGRAM.
- If the transfer is rejected, the manager can retry the update.
- Where supported, the previous usable result is kept until it can be replaced.
- Do not upload the unaffected original colors first: they could appear for one frame before the correct result.
How the systems work together
A dynamic sprite brings these tasks together:
- Its logic selects the pose and palette version.
- Palette assignment finds a usable palette and prepares colors if needed.
- The cache protects the loaded pose or prepares a new upload.
- Drawing writes the pose's tiles to the OAM buffers.
- The effect manager prepares pending global changes.
- NMI performs the requested graphics and color transfers.
Sharing graphics, sharing a palette, and sharing a drawing routine are three different things. Dynamic Xtreme can take advantage of each without requiring resources to share the others.