Segments and the spec file
The spec file is a part of the compilation process, much like the Makefile. In fact, it is summoned during the compilation process to define several major elements of the ROM, including:
- Code Segments
- Audio segements
- More segments
The boot function is the function that runs upon switching on the power.
Once they are defined, they can be called during gameplay should the need arise.
The official releases for the Nintendo 64 ranged from 4-64MB, so you can tell that this was a major component to making a large game possible. To give a bit of a comparison of two extremes, Dr Mario 64 (which was a fairly simple game for the console) was 4MB, while Conker’s Bad Fur Day and Resident Evil 2 stand at a massive 64MB because of all the graphic detail, voices, music and full-motion video. Of course, large-capacity cartridges were expensive, so there was pressure for developers to keep sizes as small as possible.
The reason why the spec file exists is that the N64 has 4MB of RDRAM. This means that the maximum amount of code that can be loaded from the cartridge is 4MB; so if the game’s ROM is larger than that, it needs to be loaded in segments through the PI.
A picture is worth a thousand words, so here is how it would work for a game:
The spec file is written in a C-esque format, though most of the C code is written in an external file and then included by using something like this:
#include "header.h" #include
It is important to include nusys.h if you want to use any NuSystem functionality, which will help simplify the boot portion of the program. After that, you will have a whole bunch of segment definitions. The basic structure of a segment is like this:
beginseg name "segmentName" flags FLAGS include "file.ext" endseg
There are many more settings for a segment besides this, which we will get to later.
After all the segments have been defined, they are joined into a wave. The wave creates an ELF object file. This file is used for debugging, so it is only optional. It has a syntax that looks like this:
beginwave name "waveName" include "segmentName" include "segmentName2" include "segmentName3" endwave
It really is that simple. All you need to do is include all the settings for each segment between the begin and end markers. The syntax overall is fairly simple. You can even add definitions, conditionals and comments like you would in regular C code if you need to.
A detailed view on segments
The basic structure of the spec file is quite simple, where it gets a bit more complicated is defining each segment correctly.
Attributes of a segment
These are all of the properties that can be assigned to a segment. You’re not going to need all of these in every segment, but it’s good to know what they all do:
beginseg name "segmentName" flags BOOT OBJECT RAW entry bootFunc stack int address int number int after int include "segment.o" maxsize int align int endseg
name is the name or label of the segment. It is used in the C code you write to call upon a particular segment later on. Required.
flags are the settings for that segment. There are three options.
BOOT designates the boot segment, as in this the segment that will run when you start up the console; it must be the first segment in the list.
OBJECT denotes that it includes object (.o) files created by the Makefile.
RAW is all other file types, mostly used for segments that refer to audio files.
entry is the boot function. This function must be included in the boot segment, and is the first function to run in the entire program. If using NuSystem, this is set to
nuBoot and the boot function in the code is set to
stack is the stack address of the boot code. This is typically set to the stack size (0x2000) plus an array of 128 64-bit integers. In NuSystem, it is defined by the variable
after are the memory location for OBJECT-type segments. address-type is meant for the CPU, number-type are for the RSP and after-type are meant to place segments right after each other to reduce fragmentation. From what I’ve seen, address-type are typically 32-bit integers, starting with 0x80000000. number-type tend to be small numbers, typically values within 1-4. I have no idea how the exact value for addresses and number are assigned. Perhaps the debugger prints them out, but they seem to be static.
include is fairly simple – it makes the segment include a file. This can be used by OBJECT-type segments to include C code and some of the N64’s microcodes as well; and RAW-type segments typically include only one file of another type. All segments must have at least one of these.
maxsize can be used to specify the maximum size of a segment in bytes.
align can change the alignment of memory. It is set by default to 16 bytes (128 bits).
The boot segment is the first segment to be run in the game program. If you look at the animation at the top of the guide, you’ll see that there is one segment that is always active. This is the general code segment, which often includes the boot function.
The boot segment is the first segment defined in the spec file, and has some unique properties when compared to other ones:
- Like all segments, it has a
flagsare always BOOT OBJECT.
- It always has an
entryline, denoting the boot function.
- Always has a
- It uses
includefor one object file based on the program’s code; the one with the boot function.
- It can
includemultiple lines of microcode as well, depending on which ones are in use.
This example from simple shows how boot segment looks like:
beginseg name "code" flags BOOT OBJECT entry boot stack bootStack + STACKSIZEBYTES include "codesegment.o" include "$(ROOT)/usr/lib/PR/rspboot.o" include "$(ROOT)/usr/lib/PR/gspFast3D.o" include "$(ROOT)/usr/lib/PR/gspFast3D.dram.o" include "$(ROOT)/usr/lib/PR/aspMain.o" endseg
And here is a similar one from nu0, this time showing how it is set up using NuSystem:
beginseg name "code" flags BOOT OBJECT entry nuBoot address NU_SPEC_BOOT_ADDR stack NU_SPEC_BOOT_STACK include "codesegment.o" include "$(ROOT)/usr/lib/PR/rspboot.o" include "$(ROOT)/usr/lib/PR/gspF3DEX2.fifo.o" include "$(ROOT)/usr/lib/PR/gspL3DEX2.fifo.o" include "$(ROOT)/usr/lib/PR/gspF3DEX2.Rej.fifo.o" include "$(ROOT)/usr/lib/PR/gspF3DEX2.NoN.fifo.o" include "$(ROOT)/usr/lib/PR/gspF3DLX2.Rej.fifo.o" include "$(ROOT)/usr/lib/PR/gspS2DEX2.fifo.o" endseg
There are two other types of segments that can be defined, and they are much simpler to understand.
First off, you have OBJECT segments that are not part of the original boot segment. These can be used for a few things, for example:
- Storing large amounts of models/textures that aren’t in repeat use. For example, one level of a game would reuse enemy texture and models that aren’t used in the rest of the game.
- Logic segments for AI and other functions that are used in certain parts of the game, but not others. Eg, a boss’s AI, or the physics engine for swimming that is used in water levels, but not in dry ones.
- When you want to exclude the boot function from the engine, so as to save some space in RAM.
beginseg name "gfx" flags OBJECT after code include "gfx.o" endseg
The above code creates an OBJECT segment called gfx based on a file gfx.o, and places it right after the segment called code. Simple, really.
The last type of segment is one that contains non-code information, mostly used for music since graphics are stored in the code segments. Here is what it would look like:
beginseg name "seq" flags RAW include "sequence.seq" endseg
This creates a RAW segment named seq, which contains the music sequence file sequence.seq.
All that’s left to do now is to learn how to load and clear segments from the RDRAM. This is quite a lot, so I will leave it for another page in the future.