Splitting Assets from Code

From N64brew Wiki
Jump to navigation Jump to search

This first page will cover the important aspect of keeping your game assets separate from the game code (where assets are things like textures, models, sounds, etc...). Depending on how you've structured your project, or more importantly how far you are into it currently, this can be a relatively simple or relatively difficult task.

Start by looking at the original sample, before any modifications. It is a simple ROM with two rooms that can be switched between by pressing the A button, with a texture that is displayed in the middle.

If you take a look at the makefile, you will see that our textures (spr_bear and spr_burger) are being compiled as C code and then being linked into the codesegment. It's not a big concern for this small project, but due to the 1MB IPL limit, this will become a problem as the project gets bigger. The idea here is that we want to have the assets elsewhere in the ROM, and we load them from the cartridge only as we need them.

So first and foremost, lets move our assets over to ROM.

Moving our assets to ROM

For simplification reasons, we're going to convert our textures to binary data, instead of keeping their current form as C arrays. You should be able to do this for pretty much any sort of assets your game will need, such as static display lists. The process of actually converting your data to binary form will not be covered here, as there are plenty of tools to do that for you already.

Once your assets are in a binary format, you must remove the original code files from your makefile (as they're redundant) as well as any #include's relating to them. You can leave the array pointers in the code for the time being, as they'll be substituted later.

The actual process of putting your assets in ROM depends on your SDK setup:

Your Spec file, before adding the assets to it, would look something like this:

beginseg
    name    "code"
    flags   BOOT OBJECT
    entry   nuBoot
    address NU_SPEC_BOOT_ADDR
    stack   NU_SPEC_BOOT_STACK
    include "codesegment.o"
    // Microcode includes go here (omitted for simplicity reasons)
endseg

// Wave's aren't used for anything in the PC SDK, they're just for visual reference
beginwave
    name    "original"
    include "code"
endwave

Adding in new raw assets is as simple as creating a new segment and specifying the RAW flag. For instance, having the data from spr_bear.c converted into binary form (with the name spr_bear.bin) and linking it to our ROM is as simple as:

beginseg
    name    "spr_bear"     // This name is important, and should be unique
    flags   RAW            // Specify that this segment is raw data (and not code)
    after   "code"         // Specify to put this data in ROM, right after our code segment (Although you can omit this line if you want)
    include "spr_bear.bin" // The file to link
endseg

Do this for all the assets, and you're almost done. The next step is to open a C header file (or better yet, create a new one) and to create some extern calls for your new segments:

extern u8 _spr_bearSegmentRomStart[];
extern u8 _spr_bearSegmentRomEnd[];

Remember that segment name I told you that was important and had to be unique? Whatever you set your segment name to, it needs to match the extern's. Meaning, if you called your segment NAME, then you would need to define the extern's as _NAMESegmentRomStart and _NAMESegmentRomEnd respectively.

If you want to know more about Spec files, the online manuals do not contain a lot of information about them. Instead, it is highly recommended that you check out the Specfile Format chapter of the N64 EXEGCC Compiler User Guide for more information.

TODO

Now that our assets are in ROM, we need to DMA them to be able to use them in our game.

Loading assets from ROM

Having the segment addresses for our data in ROM, it is relatively simple to DMA them from ROM. We know the starting address of our data thanks to the SegmentRomStart, and we can infer the size of the data by subtracting the start address from the end address.

With Nusys, the task is relatively trivial:

u32 size = _spr_bearSegmentRomEnd - _spr_bearSegmentRomStart;
nuPiReadRom((u32)_spr_bearSegmentRomStart, (void*)buffer, size);

If you're not using Nusys, then there's a bit more overhead involved:

OSIoMesg	dmaIoMesgBuf; // The message buffer
OSMesgQueue dmaMesgQ;     // The message queue
// This code already assumes a message queue exists and has been created with 'osCreateMesgQueue'
// These variables are just here for reference

u32 size = _spr_bearSegmentRomEnd - _spr_bearSegmentRomStart;

// Invalidate data cache for the buffer to prevent memory from being destroyed
osInvalDCache((void*)buffer, size);

// Start the DMA
osPiStartDma(&dmaIoMesgBuf, OS_MESG_PRI_NORMAL, OS_READ, (u32)_spr_bearSegmentRomStart, buffer, size, &dmaMesgQ);

// Wait for the DMA to finish
osRecvMesg(&dmaMesgQ, NULL, OS_MESG_BLOCK);

Please note that the code on this wiki page does not replace the manuals. Data DMA has very strict requirements in terms of maximum size and buffer alignment, therefore you should look up the manual pages for nuPiReadRom or osPiStartDma to make sure your DMA operation will succeed.

You're probably wondering from the above code what buffer is. You probably understand that the data needs to go somewhere (like a variable in your code), but there's two different approaches to having data buffers:

Having a buffer in the codesegment

The easiest method is to simply just have a global variable which will work as a "cache":

u8 buffer[4096]; // The exact maximum size of a texture in TMEM (4Kilobytes).

The idea is, before you start rendering the level, you load any textures you need into your global cache (which can be any size you want, not just 4096 bytes). Then, when the data isn't needed anymore, you mark that part of the buffer as "empty" and you can overwrite it with new data.

You're probably wondering: "Hold on, this global buffer variable is part of the code... Won't it be subject to the exact same 1MB restriction we had before?". The answer is no, because initialized global variables are not loaded from ROM (as they're dynamically "created" when your code loads). The downside to this method is that you might not always have full control over where the data is initialized to in memory. The alternative would be:

Having a buffer somewhere in RAM

Our game has about 4MB of RAM to work with (8 if the Expansion Pak is available), and roughly 1.5 to 2MB would be occupied by the framebuffers, Z-Buffer, and the code itself. So why don't we instead make use of those 2 to 2.5MB we have free to store our assets there?

All we have to do is create a new C file, and in it we place a global buffer just like we did in the previous section. Except this time, we're not going to link this data to our codesegment, rather we'll tell the N64 to reserve this part of RAM for our buffer. We'll need to modify the makefile somewhat, as we'll need to compile our C file into an object file. Lets assume our buffer is in a C file called texbuf.c.

Your makefile should already have a dedicated CODEFILES variable where you put all your C files, and then it gets compiled into one big codesegment.o object file via CODEOBJECTS = $(CODEFILES:.c=.o) and then gcc -o $(CODESEGMENT) -r $(CODEOBJECTS). The idea now is that you create a new variable, such as DATAFILES, and you place your buffer files here.

DATAFILES   = texbuf.c
DATAOBJECTS = $(DATAFILES:.c=.o)

Now, you'll want to place the DATAOBJECTS in your makefile where it will compile the .o's, but not link them to the codesegment. Typically, this will be in the target section (such as $(TARGETS): or default:). Example:

OBJECTS = $(CODESEGMENT) $(DATAFILES:.c=.o)

$(TARGETS): $(OBJECTS)
    $(MAKEROM) spec $(MAKEROMFLAGS) -I$(NUSYSINC) -r $(TARGETS) -e $(APP)
    makemask $(TARGETS)

Since we're not linking the object file in our makefile, we'll need to do that manually afterwards:

In your spec file, all you need to do is add a new segment, but give it the OBJECT flag instead:

beginseg
    name    "texbuf"
    flags   OBJECT
    after   "code" // You can use 'address' if you want to specify an exact RAM address to put the buffer at
    include "texbuf.o"
endseg
You can use #include in your spec file to include a header file with a list of address macros if you want to, and use that macro value in place of raw address numbers or the after keyword.

TODO

Once it's linked, you're all set! You just need to ensure your array is extern'd somewhere and you can use it without any other changes to your code.

A useful trick, you can extern the codesegment as well:

extern u8 _codeSegmentRomStart[];
extern u8 _codeSegmentRomEnd[];

You can use this, for instance, to load your assets right after the code segment by loading them into the address at _codeSegmentRomEnd. You don't need to create an object file with your buffer as you can just write to any RAM address you seem fit (for instance, you can just define u8* buffer = 0x80000400 globally, and then treat it as a array/buffer) but it sure helps as the compiler/linker can potentially catch buffer overflows and/or segment overlapping.

You can see an implementation of the buffer method described in this section here.

Managing large projects

After going through all of this code, you probably still have a few wiggling doubts: "How do I manage the loading of data in large projects? How can I tell what assets I have loaded, and what needs to be loaded? How do I tell if my caches are full, thus unable to read more data from the cart?". These questions are answered in the next chapter of the Code segmentation guide, which covers file systems.