The wiki has been upgraded to MediaWiki 1.43. The wiki's theme is still being adapted to work with the new version, and may appear broken for the time being. --RanAS

Dynamic Xtreme internal workings

From SnesLab
Jump to: navigation, search
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:

  1. Resets DX_Dynamic_CurrentDataSend to 0.
  2. Increments DX_Timer.
  3. Increments DX_SyncTimer if the action is not frozen and the game is not paused.
  4. Performs graphics transfers to VRAM.
  5. Performs color transfers to CGRAM.
  6. 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:

  1. If ControllerOptimization is enabled, the controller routine runs here instead of during NMI.
  2. 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:

  1. If Kevin's VRAM optimization is enabled, Lunar Magic's update destinations are adjusted before the transfers take place.
  2. Palette update flags are cleared to 0, and protection timers greater than 0 are decremented. This is skipped while paused and in disallowed modes.
  3. 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: DXAllowedGameMode uses a separate table. !Upload and 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:

  1. Resets DX_Dynamic_CurrentDataSend to 0 to count requests from the next logic cycle. Transfers already prepared remain in their lists.
  2. Increments DX_Timer.
  3. Checks $9D and $13D4|!addr. If both are 0, increments DX_SyncTimer.
  4. Performs pending VRAM transfers if GraphicsChange is enabled.
  5. Performs pending CGRAM transfers if PalettesChange is enabled.
  6. If PalettesEffects is 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:

  1. Compare the timers. If SafeFrame equals DX_Timer, that sprite has already processed its pose upload at this timer value. The routine skips a new request.
  2. Record the attempt before processing it. If the values differ, it copies DX_Timer to SafeFrame, then checks whether it can reuse the pose or must request another.
  3. Check the result. A new pose request can succeed or fail. Even if it fails, the value saved in SafeFrame is 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,x is 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:

  1. DX_Timer = $0030. The sprite saves $0030 in its SafeFrame and tries to obtain a new pose. No data budget remains, so the request fails; it keeps the previous pose if one exists.
  2. 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.
  3. When Dynamic Xtreme's NMI runs, DX_Timer advances 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:

  1. Calculates the total byte count if the new upload is added to DX_Dynamic_CurrentDataSend.
  2. Compares it with DX_Dynamic_MaxDataPerFrame. If it exceeds the limit, returns carry clear without adding the request.
  3. Uses DX_PPU_VRAM_Transfer_Length to find the next entry and increments that counter.
  4. Stores the source, bank, byte count, and destination in the tables.
  5. Updates DX_Dynamic_CurrentDataSend with 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:

  1. Reads DX_PPU_VRAM_Transfer_Length. If it is 0, returns because there are no requests.
  2. Calculates the last entry's offset: (Length - 1) × 2.
  3. Reads its source, bank, and byte count into the DMA registers.
  4. Writes the VRAM destination to $2116-$2117 and starts the transfer through $420B.
  5. Moves back 2 bytes to process the previous request, repeating until the first is complete.
  6. Writes 0 to DX_PPU_VRAM_Transfer_Length to 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:

  1. Select the image for the current animation step.
  2. Calculate its source address.
  3. Request a transfer to the animation's VRAM destination.
  4. 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:

  1. Looks for the ID in the hash table.
  2. If already registered, updates its last-use time without adding another upload.
  3. Otherwise, checks the byte count and finds suitable space in the VRAM map.
  4. Selects poses that can be replaced, considering their time since last use and the new pose's space requirements.
  5. Updates the cache tables and map.
  6. DynamicRoutine splits the pose's data into the required transfers.
  7. 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, $91 starts 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:

  1. Starts at the last pending request.
  2. Configures DMA with that request's source and byte count.
  3. Selects the first color through $2121 and performs the copy.
  4. Continues with earlier requests.
  5. Writes $FF to DX_PPU_CGRAM_Transfer_Length when 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:

  1. Reuse an assignment with the same ID and compatible effect setting.
  2. If none exists, use a free entry.
  3. If none is free, replace an entry that is not set aside and whose timer is 0.
  4. 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

  1. Finds the pose's routine and data through GraphicRoutineIDs, GraphicRoutine, and TableOffset.
  2. Gets the tile count from NumberOfTilesMinus1.
  3. Calculates each tile's position from the sprite position, displacements, and applicable flips.
  4. Checks whether the tile is visible and OAM space remains.
  5. 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:

  1. Saves the requested ID and calculates its hash.
  2. If DX_Dynamic_Pose_Length is 0, no poses are registered and the search ends.
  3. Checks DX_Dynamic_Pose_HashSize[hash]. If it is 0, no registered pose has that hash, so the search also ends.
  4. Starts at the entry whose index matches the hash.
  5. Reads that entry's ID. If it matches the requested ID, the pose has been found.
  6. 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:

  1. Starts with the registered pose count for the requested hash.
  2. Visits positions in the sequence until it finds the requested ID or has checked that many poses with the same hash.
  3. Each time it finds a pose with the same hash but a different ID, decrements the temporary counter.
  4. Free positions and poses with other hashes do not decrement it; the search continues.
  5. 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:

  1. Gets the matching entry's index.
  2. Writes the current DX_Timer value to that entry's DX_Dynamic_Pose_TimeLastUse.
  3. 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:

  1. Reads PoseSize and calculates DX_Dynamic_CurrentDataSend + PoseSize.
  2. If the result exceeds DX_Dynamic_MaxDataPerFrame, returns failure without changing existing poses.
  3. Reads Pose16x16Blocks to find the required block count.
  4. Calls VRAMMap_GetBestSlot to find suitable space.
  5. If no usable space exists, returns failure.
  6. Removes poses occupying the chosen space from the map and hash table.
  7. Finds a free hash table entry and registers the new pose.
  8. Updates the VRAM map and frame data counter.
  9. Calls DynamicRoutine to 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_FindFreeSpace looks for an entry whose ID is $FFFF, using the same sequence of 15-entry steps.
  • DynamicPoseHashmap_Add stores the ID and last-use time, increments DX_Dynamic_Pose_Length, and increments the corresponding hash counter.
  • DynamicPoseHashmap_Remove writes $FFFF to 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:

  1. Read the pose ID at DX_Dynamic_Pose_ID+$0E. The offset is $0E because each ID occupies 2 bytes and $07 × 2 = $0E.
  2. Use that ID to read Pose16x16Blocks, which contains 6 in this example.
  3. Read DX_Dynamic_Pose_TimeLastUse+$0E to 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:

  1. Reads DX_Dynamic_Tile_Offset at the current position.
  2. Gets the section's start with Offset AND $7F.
  3. Evaluates that section and spaces it can form with adjacent sections.
  4. Moves to section start - 1, the previous section's last position.
  5. 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

  1. The candidate must have at least the requested block count.
  2. The highest score wins: free space or the longest time since last use.
  3. If scores match, the smaller candidate wins, provided the pose fits.
  4. 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:

  1. VRAMMap_RemovePosesInSpace traverses its regions and removes poses being replaced from the hash table.
  2. VRAMMap_RemoveSpace marks the chosen space as one free region by updating its endpoints.
  3. A free hash entry is found, and the starting block is stored in DX_Dynamic_Pose_Offset.
  4. DynamicPoseHashmap_Add registers the pose.
  5. VRAMMap_AddPoseInSpace writes the pose's hash index and region start at the first and last blocks.
  6. 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

  1. Gets the source from PoseResource.
  2. Processes the first section's bytes through DynamicRoutineLine.
  3. If the second section's size is 0, returns.
  4. Otherwise, advances the source by the first section's byte count.
  5. Adds PoseSecondLineOffset to 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 $0200 bytes, 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_Source
  • DX_PPU_VRAM_Transfer_SourceBNK
  • DX_PPU_VRAM_Transfer_SourceLength
  • DX_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.

  1. Sprite A requests the pose. Lookup does not find $0005 in 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.
  2. 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.
  3. Sprite B requests the same pose. Lookup now finds $0005 registered, 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.
  4. 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.
  5. 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

  1. Checks whether the palette already has a registered base.
  2. If not, gets the colors from the game's buffer. An explicit upload, such as TransferPalette, can supply that base directly.
  3. Checks whether the effect needs RGB or HSL.
  4. 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, and Value3: channel values, or a transition's target palette address.
  • Ratio1, Ratio2, and Ratio3: 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:

  1. Checks whether the effect result needs an update.
  2. Prepares the RGB or HSL base if missing.
  3. Calculates the effect if the previous result is no longer valid.
  4. Prepares SNES-format colors and requests their transfer.
  5. 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:

  1. Prepare the local effect result.
  2. Use it as the global effect's base through the concatenation call.
  3. Calculate the global effect on that base.
  4. 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:

  1. Its logic selects the pose and palette version.
  2. Palette assignment finds a usable palette and prepares colors if needed.
  3. The cache protects the loaded pose or prepares a new upload.
  4. Drawing writes the pose's tiles to the OAM buffers.
  5. The effect manager prepares pending global changes.
  6. 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.