USF Case Study 1 v0.1 I herein attempt to describe the ripping procedure for Killer Instinct: Gold. STEP 1: Get Equipped You'll need a Killer Instinct Gold ROM. I used the US version 1.2 for this demonstration, if you use a different version the offsets will be different. You'll need IDA Pro. I'll be using 4.3.0.740a. It is not terribly hard to find online, look for the filename IDA430A.RAR. Other versions found online lack support for MIPS so I recommend you go with this one. Extract all the files to one directory. Pick up the N64 sig files from my web site (http://www.halleyscometsoftware.com/usf/n64-psyq-ida-sigs.rar), extract them to the sig\MIPS directory. You also want to get the Project 64 Save State Loader module (http://www.halleyscometsoftware.com/usf/IDA_PJ64ss.zip), extract it to the main IDA directory. You'll need the Project 64 v1.4 source code (http://www.pj64.net/files/Pj64%20Build%2052%202001-12-22.zip). You'll need Microsoft Visual C++ (or some other way of building Project 64). I'm sure you can find it online, but this is one of the rare pieces of software I actually found worth purchasing. For the purposes of this demonstration I'll be using MSVC6, standard edition. You'll need various other bits and bobs that I should have online by the time you read this. Something helpful when dealing with libultra is the N64 function documentation, which you can find if you look hard enough (or ask the right people). STEP 2: Set up PJ64 I'm starting with the project as it comes out of the .zip and no registry entries (delete HKEY_CURRENT_USER\Software\N64 Emulation\Project64 Version 1.4 if it exists, unless you know you have the right settings). Open Project64.dsw in MSVC. You might expect it to compile right out of the box, but no such luck. Open pif.c (under Source Files->CPU Source) and comment out any line containing LogOptions (there are 4). Then you should be able to build with F7 or run with Ctrl-F5. Get the set of plugins I have online at http://halleyscometsoftware.com/usf/pj64plugin.zip. Extract these to Release_External/plugin (you'll have to create the plugin directory). Run the executable. You'll get an error stating it can't load the GFX dll. Go to Optons->Settings... and select Jabo's Direct3D6 1.5.1 for the graphics plugin. Also set the CPu core style to Interpreter and the default memory size to 4 MB and uncheck "Always overwrite default settings with ones from ini" and "Automatically compress instant saves". Now you should have a functioning version of PJ64 and the source thereto, suitable for hacking. STEP 3: Analyze the ROM Now you could just open the ROM in IDA but that won't work too well in this case. We'll instead head to step 4: STEP 4: Analyze a RAM dump By analyzing a RAM dump rather than the ROM we'll have access to all the code as it is when the game is running. IDA can't figure all that out from the ROM. STEP 4A: Locate the song select function Start up Project 64 and load Killer Instinct Gold. As soon as the N64 logo comes up select System->Save As... and save a save state somewhere convenient. You can then close out of PJ64. Open IDA Pro (idag.exe) and select the option to disassemble a new file (if this is your first time using the program you'll have to click through some legal stuff). Open the save state you made (you'll have to select "all files" in the type box as the .pj extension isn't recognized). You should be presented with options as to how to load the file, the topmost of which should be Project 64 v1.4 Save State (pj64ss.ldw). Ensure this is selected and click OK. IDA will then run it's analysis, loading the N64 sig files and recognizing function names. In the upper right you'll see a box called "Names window" fill up with names as the processing continues. Sort this by name (by clicking the "Name" bar) and look for the appearance of a function called alSeqpSetSeq. This function tells the sequence player what sequence to play, so it should lead us to the music routines. Double click that name and the large window to the left will jump to the location of that function. The address should read "80039640". Now we want to find under what circumstances this function is called. Switch to MSVC and the Project64 source. Open the Interpreter CPU.c file and find the ExecuteInterpreterOpCode function. This will be our base of operations. It is called for every instruction that is run. We want to see where alSeqpSetSet is being called from, so we'll add a bit of code to the beginning of this function to let us know when we've hit alSeqpSetSeq and where it was called from. Add the following line to the beginning of ExecuteInterpreterOpCode: if (PROGRAM_COUNTER==0x80039640) DisplayError("alSeqpSetSeq, ra=%08x",GPR[31].UW[0]); PROGRAM_COUNTER is a variable containing the currently executing instruction. DisplayError is a nice function in Project64 that displays a MessageBox and provides printf functionality. GPR[31].UW[0] is the low word (4 bytes) of the ra register (ra is General Purpose Register #31). The jal (jump and link) instruction is the most common method of calling a function. When the CPU jumps with jal it stores the address of the instruction two instructions later in ra, so the called function will know where to return to. This will tell us where alSeqpSetSeq was called from. Compile Project64 and run KIG. You'll notice that right when execution starts a message box pops up with our message in it, reporting the return address 8002a854. If you click OK and wait a few seconds the exact same message box pops up again, with the same return address listed. Clicking OK again then allows the KIG intro to proceed. The alSeqpSetSeq function is probably called the first time to initalize the sequence player, it is probably given a silence sequence so it does nothing. The second time is most likely where it is called with the intro music. Since we see that in both instances it is called from the same address, 8002a854 (actually 8002a854-8 is where the function call takes place, 8002a854 is the return address after it) the calling function is most likely some type of higher level song selection function for the game. If you wait through the N64 and Rare logos the message box will pop up again, once more with the same return address. This address is where we should begin our analysis. Let's go to that address in IDA (press "g" while in the disassembly window (IDA View-A) and type it in). We'll end up in a bit of unexplored code, pretty much just a list of values. We know there is code here, however, PJ64 just told us that execution returns here after alSeqpSetSeq. Press "c". The code past that address in analyzed, it appears to be the end of the function (I'll explain that appearance below). What we'd really like is to get a view of the function that we're currently in the middle of. Scroll up a few lines to 8002A84C and press "c" again. Here we see "jal alSeqpSetSeq", the instruction that allowed us to find this particular bit of the code. I'm not sure why it is labeled with "# doubtful name", but it seems to be an accurate identification. If we keep scrolling up and pressing "c" we reveal more and more of the function. We see "jal alCSeqNew", which initializes a Compressed Sequence structure. We'll be taking a look at that to determine how different songs are specified. If we continue exploring backwards through the code, we get to the beginning of the function, 8002A790. How do we know? Well, the code there looks like this: RAM:8002A790 lhu $t6, word_80048028 RAM:8002A798 addiu $sp, -0x28 RAM:8002A79C sw $s1, 0x1C($sp) RAM:8002A7A0 sw $s0, 0x18($sp) RAM:8002A7A4 move $s1, $a0 RAM:8002A7A8 sw $ra, 0x24($sp) The 2nd instruction moves the stack pointer back by 0x28, which suggests that we're in a new context (the local context of the function). The 6th instruction saves the return address, since ra will most likely be modified within the body of the function. These two elements are a dead giveaway. Additionally, if we explore up a few lines, we find: RAM:8002A780 lw $ra, 0x14($sp) RAM:8002A784 addiu $sp, 0x18 RAM:8002A788 jr $ra RAM:8002A78C nop This loads the return address, increments the stack pointer (returning us to the calling function's stack context), and jumps to the return address. This is clearly the end of the previous function in memory. Note that the "nop" is really the last instruction, since it is in the delay slot of the jr instruction (jump and branch instructions on a MIPS processor take extra cycles to execute, so while the jump desination is being loaded the next instruction after the jump is executed. This instruction is said to be in the delay slot)). To let IDA know we've found a function at 8002A790 select that address and press "p" (for "procedure"). A bit of extra analysis is performed and a list of the variables used by our function is shown. There is also a label created called sub_8002A790. Let's give this a more useful name, say "MusicFunction". Select the name sub_8002A790 and press "n" to enter a custom name. So now we've identified a function which is responsible for loading a sequence. Good. Scroll back down to 8002A840 where alCSeqNew is called. This is really what I should have had you trace instead of alSeqpSetSet, but the latter led us here anyway. Let's look at what parameters are used to call alCSeqNew (as we're trying to figure out how we can get it to load different songs). MIPS compilers put the parameters for functions in the a0, a1, a2, and a3 registers ("a" for "argument"). The order corresponds to the first, second, third, and fourth parameters in the function as seen in C. I'm going to cheat and take a look at the N64 Function Manual to see what the parameters of alCSeqNew are: void alCSeqNew(ALCSeq *seq, u8 *ptr); The first parameter is an emtpy structure we'll be initializing (this will later be passed on to alSeqpSetSeq so it can play it), the second parameter is a pointer to the raw compressed sequence. The latter, which will be stored in a1 when alCSeqNew is called, is what we're looking for. We need to find out how different sequences are loaded there. Let's look at the code surrounding the call: RAM:8002A838 lw $a1, dword_80066920 RAM:8002A840 jal alCSeqNew RAM:8002A844 move $a0, $s0 Here we see that a1 is loaded from a variable, dword_80066920. Let's give it a name for easier reference, press "n" while it is selected and call it "SeqBuf" (for "Sequence Buffer") Let's see how SeqBuf is used by MusicFunction. Select the name SeqBuf by clicking it once, and it becomes highlighted. If you scroll up or down it remains highlighted, as do any other instances of it. If you scroll up a few lines you see it again at 8002A7DC, where a1 is again loaded from it. The address within SeqBuf is being used in another function call. This is a non-library function so we can't tell what it does just from it's name (sub_80000740), so we'll take a look at it (first give it a more descriptive name, like "LoadSeq"). Double click on LoadSeq to go to the code. Here I'm going to give you the benefit of my experience, rather than pretend I'm doing this for the first time. This is the function that loads the sequence from the ROM, with a prototype like: void LoadSeq(u8 * ROMaddress, u8 * RAMaddress, s32 nbytes); How do I know this? I've seen it before and figured it out. LoadSeq is structured like this: 1. A call to osInvalDCache, which invalidates the data cache. The parameters are: void osInvalDCache(void *vaddr, s32 nbytes); This must be done because the PI (N64's Peripheral Interface, used to transfer data to and from the cartridge) will load from the ROM directly to RAM. The N64's CPU might have previously read the RAM address and have saved a copy in cache, and if we try to read from that address again we might get the outdated copy in cache rather than the new one in RAM that we just loaded from ROM. Invalidating the cache tells the CPU that the cached values are no longer accurate, so next time we try to read from that address it will actually go straight to RAM. You'll notice that LoadSeq's second parameter, a1 (which was loaded with SeqBuf for the call to LoadSeq) is moved to a0 via the instruction at 80000750, so that it becomes the first parameter of osInvalDCache, the address of cache to invalidate. LoadSeq's third parameter, a2, is moved to a1 in order to become osInvalDCache's second parameter, the size of the region to invalidate. Thus we also know that LoadSeq's third parameter is the size of the data we're reading. 2. sub_80002F00 actually does the loading (I don't feel like explaining this whole thing out, it involves the OS functions, in fact this is probably just an unrecognized library function) 3. Then we wait to recieve a message reporting that the transfer has completed. (the call to osRecvMesg has a2=1, which indicates it is a blocking call (which means that until a message is recieved the thread waits)) The upshot is that the first parameter of LoadSeq is the address of the sequence in ROM. Now typically these are laid out end to end, and at the beginning of the bank of sequences there's a list of the address and size of each. This is copied to RAM before any sequences are read. Somewhere before the call to LoadSeq should be some code looking into this table. We see at 8002A7D8 that a0 is loaded from t9+4. As I mentioned, the table of sequences contains lengths and offsets. t9 contains the location of the entry into the table, and +4 causes the load to be from the offset portion of the table. We then look up to see how t9 is loaded. At 8002A7D4 we see that t9=t7+t8. t7 is loaded from dword_80066B18 at 8002A7B8 and it is the base of the table (the address of the first entry). t8 is calculated from a0 (the first parameter of MusicFunction) times 8 (a bitwise shift left of 3 is equivalent to multiplication by 2^3=8). 8 is the size of each entry in the table, thus the first parameter of MusicFunction is the index into the sequence table of which sequence to load. That, at least, is how it appears. Let's return to Project64 and test the theory that 8002A790 (MusicFunction) is the function used to select a song to play. Delete the line you had added before to trap the call to alSeqpSetSeq and add a new line: if (PROGRAM_COUNTER==0x8002A790) DisplayError("a0=%08x",GPR[4].UW[0]); This will cause a message box to appear when MusicFunction is called, and it will display the value of a0 (the first parameter), which we think is the song number. GPR[4] indicates a0, the General Purpose Register #4. When you run the ROM we see the box pops up quickly with a0=00000011, then a little later again it says a0=00000011 and the intro music actually starts. When the KI logo appears the box pops up again with a0=00000028. Let's test if this value controls the song number by setting a0 to 0x28 whenever MusicFunction is called. If we get the KI logo music during the intro we'll know it worked. Change the line you had put in before to: if (PROGRAM_COUNTER==0x8002A790) GPR[4].UW[0]=0x28; Compile, run PJ64, and start the ROM, and you'll see that this is indeed the case. We have located the function responsible for setting the song number. You could set a0 to any value and play a song from the sequence bank. Fortunately this was quite an easy game. It used the audio library strictly and the calls to alSeqpSetSeq and alCSeqNew were right in the song selection function. In other games you might see many levels of functions, queues, and temporary variables used to signal the intent to play a song across the program. And when the audio library isn't used intuition is about all you have to rely on. STEP 4B: Eliminating Unneeded Code Now that we've determined how to get the game playing any song, we want to eliminate all portions of the program not needed for music. The brunt of this is done by Project64 USF, a hacked version of PJ64 which traces all data accesses. Some of the work must be done manually, though, because clearly the game in an unmodified state does a whole lot besides music. My method for working on this has varied as time went on. At one time I would rewrite sections of code that run early in the game's execution in order to make the music play right away and not go on to anything else. Since then I've become lazier. I try to let the game run normally until it hits the song select function, then immediately eliminate any unneeded threads that may be running. First, a word on threads. A thread is an operation system abstration, a function that is sharing the CPU with other threads running, conceptually, simultaneously. Typically in the N64's OS we find that control is handed off by one thread to another via osRecvMesg, as mentioned above. When called in blocking mode it instructs the CPU to run another thread until a message is available. Something I tried with Super Mario 64 (and which hasn't worked since) was to make the audio thread (SM64 was very simply structured and had but three threads: audio, video, and logic) perform a nonblocking call to osRecvMesg, in which it will simply wait until a message is recieved, rather than pass control to another thread, thus the audio thread has complete control over the processor. Of course the OS itself still has some control, it's exception handler is what sends the audio thread the message it is waiting for, reporting that the AI has finished playing a buffer or that the RSP has finished rendering some sound. Anyway, that method no longer works so well, most games have several threads involved in audio. What I most often do now is attempt to "kill" threads that aren't related to audio in order to halt the generation of graphics and the overall progress through the game logic (i.e. you don't want it reading the controller or responding to button presses, or moving on to another part of the intro). Threads are usually structured as infinite loops, so my method for killing them involved removing the final command which returns execution to the top of the loop. The main function of a thread is, in fact, a function, though execution is never intended to reach the end there are usually instructions there for a return to the OS which will remove the thread from the OS's queue. So let's find the threads in KIG and kill some until we've accomplished the goal. In IDA look for a function in the names box called osCreateThread. The offset should be 800027C0. osCreateThread is what's used to first create the thread structure to tell the OS about the thread. It is not actually started until a later call to osStartThread, but osCreateThread is the only one called with the thread's starting address so that's what we're looking for. To trap the calls to osCreateThread we'll add the following line to ExecuteInterpreterOpcode in PJ64 (commenting out the MusicFunction line we added before, we'll have need of that later): if (PROGRAM_COUNTER==0x800027C0) DisplayError("thread=%08x",GPR[6].UW[0]); GPR[6] is a2, the 6th General Purpose Register, and the third parameter of osCreateThread, which is the pointer to the beginning of the thread's function. Give this a run. As the message boxes appear with thread addresses jot these numbers down somewhere. They should be: 800004ac: Thread1 80004c50: Thread2 80000520: Thread3 80003374: Thread4 80001f20: Thread5 8002ad74: Thread6 801ccc8c: Thread7 I've added a little name to each, this helps to refer to them quickly. Go to each of those addresses in IDA, hit "p" to analyze a function there, and name each as I have named them. Now, for the killing. We shouldn't bother with Thread1, it's only job is to create other threads, you can see the endless, yet empty, loop at the end: RAM:80000508 b loc_80000508 RAM:8000050C nop Thread2, however, might do something we want to eliminate. Go to it and scroll down to the bottom, where it says "# End of function Thread2". The last instruction is a branch back to the top of the function. RAM:80004D98 b loc_80004C70 RAM:80004D9C nop Analyze the unexplored code after that and you'll see the end of the function: RAM:80004DB0 lw $ra, 0x1C($sp) RAM:80004DB4 lw $s0, 0x18($sp) RAM:80004DB8 addiu $sp, 0x38 RAM:80004DBC jr $ra RAM:80004DC0 nop We want to get to this point so Thread2 will be exited. How? Simply by writing 0x00000000 to 0x80004D98 to eliminate the loop (writing zero to the address where the "b" (branch) instruction was replaces it with a "nop" (no operation), instead falling straight through to the end of the function. We want to do this only at the opportune time, though, after MusicFunction is called. Get rid of the osCreateThread trap in Project64 and change the MusicFunction line to something more like this: if (PROGRAM_COUNTER==0x8002A790) { ((DWORD*)N64MEM)[0x4D98/4]=0; // kill Thread2 } Here we reduce the address 0x80004D98 to 0x4D98 as it refers to an address 0x4D98 bytes into RAM (the N64MEM array is RAM, the 8 at the very beginning of the address specifies that this is a cached address). We have to cast N64MEM as an array of DWORDS (4 bytes), and then divide our adress by 4 to refer to the right 4 byte section. The comment is important because often we'll have a whole list of commands like this and it'll quickly become confusing. You can try to run this... but you'll see that the program just sits there, black screen, no sound. Select "Options->Show CPU usage %" to show a running report of the activity. Alist is the audio list, and we see about 2% of CPU time is being spent on that, so some blank audio lists are probably being sent. This indicates that the audio system is up and running, it just isn't playing anything yet. Dlist is the display list, which is occupying 0% of the CPU, which indicates no graphics are being rendered (sometimes even a black screen uses CPU so this is a good sign). Let's remember what happened when we were looking at the calls to MusicFunction before. Recal that there were two calls with a0=0x11, and only the second one seemed to actually play anything. Perhaps we've killed Thread2 too early and stopped the program's execution before the audio code is fully initialized. Let's try instead to kill Thread2 after the second call to MusicFunction. We'll do this by adding a variable to track how many times it's been called. For instance, I changed the beginning of ExecuteInterpreterOpcode to this: static int runyet=0; if (PROGRAM_COUNTER==0x8002A790) { if (runyet) ((DWORD*)N64MEM)[0x4D98/4]=0; // kill Thread2 runyet=1; } The second time MusicFunction is called runyet will have been set to 1 and Thread2 will be killed. This is, by the way, the benefit of using an emulator's source code for debugging, it give you nearly infinite flexibility. Let's run this and see what happens. Drat, the N64 and Rare logos run anyway, and the sound is all messed up. Though it does look like, when "PRESENTS" shows up, execution halts. I think it'd probably be best to ignore Thread2 for now and look for another possibility. We should keep the runyet bit, though. Let's look at Thread3. Hmm... it seems to be a normally ending function, so it's probably only called once (and with the call to LoadSeq we see in the middle it must be loading something from ROM). Let's look at Thread4. We see that it contains a call to __osViSwapContext, this might indicate that we're dealing with a graphics-related thread, a good candidate for elimination. Notice that the end of the loop is at 8000350C, with a branch back to 800033BC, but also notice that when you highlight loc_800033BC there is also a branch a few lines up at 800034FC. Both of these will take execution back to the top of the loop, so we'd be best off removing them both. I change the code in ExecuteInterpreterOpcode to: static int runyet=0; if (PROGRAM_COUNTER==0x8002A790) { if (runyet) { //((DWORD*)N64MEM)[0x4D98/4]=0; // kill Thread2 (sound corruption?) ((DWORD*)N64MEM)[0x350c/4]=0; // kill Thread4 ((DWORD*)N64MEM)[0x34fc/4]=0; // also kill Thread4 } runyet=1; } Run this and... nope. Alist % goes to zero and the game just hangs. Let's look at Thread5. Looking through it I see a lot of osSpTask functions, this probably is responsible for relaying commands to the RSP. Let's not mess with it for now. Let's look at Thread6. We see that this is also a normally-ending function, but there is a call to alClose that will most likely never be executed, as this is the command that shuts down the audio library. The branch before it must always be taken. Also, this tells us that this is most likely the audio thread, so again we should leave it alone. Let's look at Thread7. Ooh, this one's a little tricky. If you scroll down to 801CCD74 you see "jr $t8", followed by unexplored code. This is indicative of a jump table, usually the compiled form of a "switch" statement in C. Depending on some condition the jump will take us to various bits of code in that unexplored region. Select 801CCD7C, the first unexplored byte, and press "c" to analyze it. There is a single function call, then a jump past all the rest of the unexplored code to nearly the end of the function. This is the equivalent of a "case" within a "switch" with a "break" statement, skipping straight to the end of the "switch". We can continue analyzing the rest of the unexplored code there and find similar things. All of them, though, branch at the end to 801CCE38 (or 801CCE34 for a few), which is where we also find the true end of the function at 801CCE48. Let's kill this and see what happens. static int runyet=0; if (PROGRAM_COUNTER==0x8002A790) { if (runyet) { //((DWORD*)N64MEM)[0x4D98/4]=0; // kill Thread2 (sound corruption?) //((DWORD*)N64MEM)[0x350c/4]=0; // kill Thread4 //((DWORD*)N64MEM)[0x34fc/4]=0; // also kill Thread4 ((DWORD*)N64MEM)[0x1cce48/4]=0; // kill Thread7 } runyet=1; } Hmm, absolutely nothing happens. Rather, the game seems to run unimpeded. You will find, though, that if you try to start a game it will stop at the Vs. screen, so we're probably dealing with a fairly high-level logic thread. How disappointing. I am not, however, out of ideas. Let's look again at Thread5, which appears to be responsible for actually handing off alists and dlists to the RSP. I didn't want to mess with it before because I'd hoped that we could find a simple thread-killing solution, and killing Thread5 would disable audio as well (assuming, as I do, that all RSP access is done through it). We'll need a slightly more sophisticated approach. A glance through the code reveals chunks of code with small numbers of library functions in each. It is my guess that each of these has a single, well-defined role to play. If we can locate the bits that relate to graphics we can disable them from rendering and also probably stop the overall game execution (since it will be waiting for a frame to finish rendering). A quick glance at the chunks involving osSpTaskStartGo and friends doesn't seem to relate any of them specifically to graphics, though. What I do see is osViSwapBuffer occasionally. This is the function required to replace the currently displayed buffer with a newly rendered one. This arrangement, called double buffering, ensures that the image is not being drawn as it is being displayed, but rather one frame is drawn to memory while another is being displayed, then they are swapped. So these bits of code are most likely used when the rendering process has completed. The two bits like this are at 800021AC and 8000222C, and both also contain a call to osSendMesg. Here's our chance: my guess is that the calls to osSendMesg is used to let the graphics code (wherever it may be) know that the frame has finished rendering (it's probably waiting with a blocking osRecvMesg). If we remove these calls we may be able to freeze the graphics code and also the overall game logic. The calls are at 800021E8 and 80002268, so my code to remove them is: static int runyet=0; if (PROGRAM_COUNTER==0x8002A790) { if (runyet) { //((DWORD*)N64MEM)[0x4D98/4]=0; // kill Thread2 (sound corruption?) //((DWORD*)N64MEM)[0x350c/4]=0; // kill Thread4 //((DWORD*)N64MEM)[0x34fc/4]=0; // also kill Thread4 //((DWORD*)N64MEM)[0x1cce48/4]=0; // kill Thread7 // disable gfx messages ((DWORD*)N64MEM)[0x21E8/4]=0; ((DWORD*)N64MEM)[0x2268/4]=0; // play title song GPR[4].UW[0]=0x28; } runyet=1; } I added in the play title song bit so that we can hear something interesting (0x11, which plays during the N64 and Rare logos, is short and tuneless). Run this and sure enough everything freezes up but the music. The N64 logo can be seen coming on screen but it was only rendered once or twice before the graphics were abandoned. Dlist CPU usage is at 0%. Something important to listen for is if the music cuts off, which indicates that the main game is still proceeding to some extent in the background (or possibly that things crashed). This doesn't happen in our current scenario so we should be safe. This is a bit sloppy, I'll admit, but you can't argue that it accomplishes the goal. When I'm ripping I care more about getting the damned thing accomplished quickly, before I lose interest, than hacking everything perfectly into shape. We'll accept this as is. To move on to the next step we'll need to create a save state exactly at the start of MusicFunction, after we've applied our patches but before anything specific to the song has happened. That way we can use this save state as the basis for ripping each and every song. The code to do so looks like this: static int runyet=0; if (PROGRAM_COUNTER==0x8002A790) { if (runyet) { //((DWORD*)N64MEM)[0x4D98/4]=0; // kill Thread2 (sound corruption?) //((DWORD*)N64MEM)[0x350c/4]=0; // kill Thread4 //((DWORD*)N64MEM)[0x34fc/4]=0; // also kill Thread4 //((DWORD*)N64MEM)[0x1cce48/4]=0; // kill Thread7 // disable gfx messages ((DWORD*)N64MEM)[0x21E8/4]=0; ((DWORD*)N64MEM)[0x2268/4]=0; sprintf(SaveAsFileName,"KIG_USF"); Machine_SaveState(); } runyet=1; } The save state "KIG_USF.pj" will probably be created in the same directory as the PJ64 source code. Make note of its location. STEP 5: Process with PJ64 USF PJ64 USF is the monstrosity I have created to pare down ROMs and save states for use in USFs. When set up correctly it will trace every data access the game makes and decide what data to keep and what to throw out. It generates "sparse" files, they contain only the data actually used and any other data, which is assumed to have a value of zero, is not stored in the file. These can be converted directly to USFs. First get and extract the archive containing PJ64 USF (http://halleyscometsoftware.com/usf/pj64usf3.zip). Apply the pj64usf.reg and pj64usfrsp.reg registry patches. Start Project64.exe, open the Killer Instinct Gold ROM. Choose "System->Batch Rip" from the menu. First we'll do a test to see if our save state can actually be made into a useful USF. Select the save state you want to use (the "KIG_USF.pj" thing we made before) and make sure the output directory points to somewhere that actually exists. Trace Begin Offset is, well, where the emulator will begin tracing. That's also where it will create it's initial save state. We want this to be fairly close to the beginning of our MusicFunction, but not at the very first instruction, as that is where we instructed PJ64 to create KIG_USF.pj, but it didn't actually get around to it until the next instruction. Let's use a nice, fairly safe value like 8002A79C, two instructions past the start of MusicFunction. Note that all the numeric values in the Batch USF Ripper dialog box are hexadecimal, don't enter 0x before them. For the song values we'll enter a single value that we know works twice, 0x28 (enter it as 28). If you enter different values the batch ripper will attempt once for the entire range, inclusive of the values you entered. Also, uncheck the "Reset on new data read" box, as that would make the ripping proceed until the song finishes or loops, and we want to quickly verify that the system works at all. When this is checked, every time a new piece of data is read the alist count is reset, so that the tracing doesn't finish until no new data has been read in a while, generally indicating that the song is either over or has looped. Occasionally this value is not set high enough and some songs will crash before they finish as a result, simply rerip the problem tracks with a higher alist value, such as 2000. The "Song Value Offset" field doesn't do anything in this particular version (it's intended for rips where the song value is in RAM, rather than in a0). The music should start playing a bit slowly, as a lot of extra processing is being done. You should see an alist and dlist count in the status bar of the PJ64 USF window, dlists should stop at 2 and alists should go up to 1000. When alists hits 1000 playback will freeze and the sparese ROM and save state will be created in the directory you specified: sparse28.rom, which should be about 56 KB, and sparse28.ram, 149 KB. Exit PJ64 USF (you should do this after every action, it isn't very well written and the only way to make sure it's reset is to restart it). Now we'll convert these files to a usable USF. Get the usftools.zip archive (http://halleyscometsoftware.com/usf/usftools.zip) and extract all the programs to a directory you'll be working in (preferably the same one as the sparse files were genereated in). Run the following command in that directory from an Command Prompt window: rom2usf sparse28.rom sparse28.ram sparse28.usf Now you should have a file called sparse28.usf, open it up in Winamp and enjoy! At about 18 seconds it will crash and fail, this is how long we traced it for in PJ64 USF and it doesn't have any more data than what was used in that time. Now we can go back and rip the whole soundtrack. You'll need to choose good starting and ending song values, 0 is usually good to start but the end depends on how many songs there are in the game. I happen to know that Killer Instinct Gold has 48 tracks (0x2f therefore being the highest song number), but if you didn't you could simply try playing higher numbers until the ROM either crashes (if the programmers didn't check the input to the song select function) or you run into a whole lot of silent tracks. In KIG the game crashes with a value 0x30 or above. So load Killer Instinct Gold V1.2, set up the Batch Rip Dialog with these values: Using this savestate: C:\Pj64\KIG_USF.pj (assuming that's your path) Trace begin offset: 8002A79C Sparse output directory: c:\ (assuming that's where you want the output files to go) Song Values 0 to 2f Spend ___ alists ripping each song: 1000 (usually a safe value) Reset on new data read *checked* Hit the Rip! button and (here it comes...) let 'er rip! Then just wait... the uneditable text box next to the Song Values inputs tells you what song it's currently ripping. You can, if you want, use two (or more) different computers to rip the same game, just set the song values to nonoverlapping ranges that cover the whole range. Distributed processing at it's best. * * * In about 1/2 hour or so you should be left with 48 .rom file totalling 1.77 MB and 48 .ram files totalling 6.85 MB. STEP 6: Create miniUSF/USFlib set At this point you could theoretically use rom2usf on all the individual files and create 48 USFs, but that would take up a whole lot more space than is needed. We'll use recon.exe to combine all common data. First I recommend creating a subdirectory just for the files, call it kig or something, and put the sparse files in it. Then, from within that directory (which must also contains the programs from usftools.zip), run these commands: for %1 in (*.rom) do recon %1 ..\kig.romlib l * This combines all the .rom files into kig.romlib for %1 in (*.ram) do recon %1 ..\kig.ramlib l * This combines all the .ram files into kig.ramlib for %1 in (*.ram) do recon %1 %1.miniram ..\kig.ramlib c * Compares each .ram and kig.ramlib and makes a .miniram file containing only the differences for %1 in (*.miniram) do rom2usf NULL %1 %1.miniusf * Create a .miniusf file for each song (no ROM is needed because the ROM is common to all the files, and so is only in kig.romlib) rom2usf ..\kig.romlib ..\kig.ramlib kig.usflib * Make the .usflib containing all common elements (RAM and ROM) psfpoint -_lib=kig.usflib *.miniusf * Add a tag to the .miniusf files indicating that they must refer to kig.usflib. Then you should have a working set of .miniusfs. You can delete the .miniram, .ramlib and .romlib files, but hang on to the .rom and .ram originals in case you have to rerip certain tracks, then you can regenerate the .miniusf/.usflib set without having to rerip all the tracks. If you want to put those commands in a batch file you'll need to replace every instance of %1 with %%1. --- And there you have it. I hope someone will be able to follow this and rip some other games. -hcs 7/23/05 halleyscometsoftware@hotmail.com 0.0 - 0.1 - typos, clarification