Table of Contents
Memory management
The WonderSwan poses unique challenges for implementing a robust memory management system:
- The unified memory architecture places display and audio data in shared RAM with the CPU, however its restrictions lead to harsh alignment requirements, and thus the need for non-contiguous memory allocation in RAM.
- The combined segmentation and banking benefits from a ROM layouting scheme which takes it into account.
Addressing RAM and ROM in C
In a C environment, by default, near pointers are utilized - they're smaller and faster to use, but they can only point to the program's default data segment - for all wswan
targets, this is the console's internal RAM (IRAM).
To point to data in ROM, a far pointer is required. These are explicitly declared by using the __far
modifier:
// The variable below will be stored in IRAM, even if it's declared as read-only (const).
static const uint16_t table_iram[] = {0, 1, 2, 3};
// The variable below will now be stored in ROM instead of IRAM, as it is read-only and far.
static const uint16_t __far table[] = {0, 1, 2, 3};
// As will this one, except it's not static, so you can reference it in a .h file...
const uint16_t __far global_table[] = {0, 1, 2, 3};
// ... like so.
extern const uint16_t __far global_table[];
void process_table_iram(uint16_t *tbl); // This function can accept pointers to IRAM only.
void process_table(uint16_t __far *tbl); // This function can accept pointers to both IRAM and ROM.
However, certain subtargets model things differently:
- On
wswan/bootfriend
, all of the program's code and data is stored in RAM. - On
wwitch
, the default data segment actually points to SRAM - on this platform, IRAM only contains the stack.
For this reason, Wonderful provides helper modifiers which abstract away the differences; these are useful for developing libraries usable across all subtargets:
__wf_rom
- points to read-only program code/data on every subtarget;__wf_iram
- points to IRAM on every subtarget.
Addressing RAM and ROM in assembly
The linker places code and data in RAM and ROM using sections. These sections are used by default by the C compiler:
.fartext
- far code, last 768 KiB of ROM.farrodata
- far read-only data, last 768 KiB of ROM.text
- code, RAM.rodata
,.data
- data, RAM.bss
- zero-filled data, RAM
Note that these act as prefixes - .text.a
and .text.b
will both be placed in RAM. This matters in particular for far code/data - .fartext.a
and .fartext.b
will both be placed in the last 768 KIB of ROM, but are allowed to use different 64 KiB segments to do so.
To address RAM and ROM in assembly, one can use these very same sections manually.
.section .fartext.my_rom_code_segment, "ax"
.section .farrodata.my_rom_data_segment, "a"
.section .text.my_ram_code, "ax"
.section .data.my_ram_data, "aw"
The quoted component refers to section flags. The most useful ones are:
a
- section is allocated in RAM/ROMw
- section is writablex
- section is executableR
- section is retained even if not used by any code
All section flags are listed in the assembler manual.
Advanced section names
What if you want to place your code or data in a specific area of RAM? On the WonderSwan, especially the Color model, there are many restrictions as to where or with what alignment certain types of data (screen tables, tiles, audio wavetables) may be placed. Alternatively, what if you want to place your code/data in a manually bankable area of ROM?
To make this possible, Wonderful's linking approach has been extended to allow special section name patterns to be used. For example:
__attribute__((section(".iramx_2bpp_2000.tiles")))
ws_tile_t tile_2bpp[512];
The above block of code will allocate 512 tiles worth of memory in:
* internal console RAM (.iram
),
- without zeroing the content on startup (
x
), - with constraints expected of 2BPP tile data - the memory area
0x2000
-0x5FFF
, aligned to a multiple of 16 bytes (2bpp
), - at location 0x2000 in memory (
2000
).
The following prefixes exist:
Prefix | Optional arguments | Description |
---|---|---|
.iram[x] | offset | Mono internal RAM (0x0000 - 0x3FFF) [zeroed on startup?] |
.iramc[x] | offset | Mono/Color internal RAM (0x0000 - 0xFFFF) [zeroed on startup?] |
.iramC[x] | offset | Color internal RAM (0x4000 - 0xFFFF) [zeroed on startup?] |
.iram[…]_screen | offset | Screen data (aligned to 0x800 bytes) |
.iram[…]_sprite | offset | Sprite table (aligned to 0x200 bytes) |
.iram[…]_2bpp | offset | 2bpp tile data (aligned to one tile) |
.iram[…]_4bpp | offset | 4bpp tile data (aligned to one tile) |
.iram[…]_wave | offset | Audio wavetable data (aligned to 0x40 bytes) |
.iram[…]_palette | offset | Color palette data (aligned to 0x20 bytes) |
.rom0 | bank index, offset | ROM bank 0 |
.rom1 | bank index, offset | ROM bank 1 |
.romL | bank index, offset | ROM linear bank (uniquely uses 20-bit offset) |
with the following optional arguments:
- bank index - the index of a bank, padded to the top of ROM.
- offset - the absolute offset within a bank or memory location.
Note that the linker requires distinct section names to place things separately: in other banks, memory locations, or for garbage collection. One can use .
after the prefix to specify an unique name.
Controlling placement in wf-process
wf-process supports an easy syntax for moving data to a specific ROM bank, index, or offset:
process.emit_symbol("gfx_title_screen", tilemap_color, {
["bank"] = 0, -- optional; or 1, or "L"
["bank_index"] = "F", -- optional; using a string is preferred over a number
["offset"] = 0x1000 -- optional.
})
This approach also ensures that sections are granted unique names by default.
Accessing data across ROM banks
A given 20-bit linear address to memory contains the bank type information (highest 4 bits) and the offset (lowest 16 bits), but it does not contain the bank index itself. For this purpose, the Wonderful linker was extended with support for additional symbols;
__attribute__((section(".rom0.gfx_font")))
const uint8_t __wf_rom gfx_font[1024] = { ... }; // array "gfx_font"
extern const void *__bank_gfx_font; // address = bank for "gfx_font"
</code C>
To access data in a separate bank, the following method is recommended:
<code C>
void load_font(void) {
// Change bank, saving the previous bank value.
// Assumes "gfx_font" exists at ROM bank 0
ws_bank_t previous_bank = ws_bank_rom0_save(&__bank_gfx_font);
// Load font data from ROM to IRAM.
memcpy(MEM_TILE(0), gfx_font, sizeof(gfx_font));
// Restore the previous bank value.
ws_bank_rom0_restore(previous_bank);
}
Observe the save/restore pattern. As the parent function may already be using ROM bank 0 for something else, it's a good habit to ensure at function exit that the ROM bank remains unchanged from what it was at function entry:
bool load_graphics(void) {
ws_bank_rom0_set(__bank_gfx_title_screen);
// This function changes ROM bank 0!
load_font();
// If load_font() did not restore the previous bank index.
// the code below will copy unexpected data.
memcpy(screen_1, gfx_title_screen, sizeof(gfx_title_screen));
// load_graphics() did not restore the previous bank index either.
// If the function calling load_graphics() relies on said index,
// it too will be handling unexpected data.
return true;
}
Note that ws_bank_rom0_*
can accept both numeric values and const void*
pointers; in the latter case, the address of the pointer will be used.
In addition, when using wf_process
, a `gfx_font_bank` define is provided for convenience - it equals `&__bank_gfx_font`.