it is, you can see the main thread loop here:
/sm64/src/game/main.c:
Super Mario 64 has an operating system inside to simulate handling multiple threads running at once. The main game begins by creating a thread here:
Code:
create_thread(&gIdleThread, 1, thread1_idle, NULL, gIdleThreadStack + 0x800, 100);
osStartThread(&gIdleThread);
thread1_idle is a method that initializes the hardware:
Code:
static void thread1_idle(UNUSED void *arg)
{
#if VERSION_US
s32 sp24 = osTvType;
#endif
osCreateViManager(OS_PRIORITY_VIMGR);
#if VERSION_US
if (sp24 == TV_TYPE_NTSC)
osViSetMode(&osViModeTable[OS_VI_NTSC_LAN1]);
else
osViSetMode(&osViModeTable[OS_VI_PAL_LAN1]);
#else
osViSetMode(&osViModeTable[OS_VI_NTSC_LAN1]);
#endif
osViBlack(TRUE);
osViSetSpecialFeatures(OS_VI_DITHER_FILTER_ON);
osViSetSpecialFeatures(OS_VI_GAMMA_OFF);
osCreatePiManager(OS_PRIORITY_PIMGR, &gPIMesgQueue, gPIMesgBuf, ARRAY_COUNT(gPIMesgBuf));
create_thread(&gMainThread, 3, thread3_main, NULL, gThread3Stack + 0x2000, 100);
if (D_8032C650 == 0)
osStartThread(&gMainThread);
osSetThreadPri(NULL, 0);
// halt
while (1)
;
}
This sets up the video drawing mode, but the most important part is the creation of a main game thread:
Code:
create_thread(&gMainThread, 3, thread3_main, NULL, gThread3Stack + 0x2000, 100);
if (D_8032C650 == 0)
osStartThread(&gMainThread);
peaking at the method static void thread3_main(UNUSED void *arg) reveals the main system loop:
Code:
while (1)
{
OSMesg msg;
osRecvMesg(&gIntrMesgQueue, &msg, OS_MESG_BLOCK);
switch ((u32)msg)
{
case MESG_VI_VBLANK:
handle_vblank();
break;
case MESG_SP_COMPLETE:
handle_sp_complete();
break;
case MESG_DP_COMPLETE:
handle_dp_complete();
break;
case MESG_START_GFX_SPTASK:
start_gfx_sptask();
break;
case MESG_NMI_REQUEST:
handle_nmi_request();
break;
}
Dummy802461DC();
}
Super Mario 64 doesn't offer any methods of variable timestep in the loop it runs from a cursory glance, it just runs over and over again as fast as it can. This is pretty easily confirmed on a real N64 by using, say, a game shark and spawning too many objects in one level. The game slows down to process them, because each internal tick is taking longer to finish. An operating system that controls all the modules of the engine communicate through a messaging queue, and the loop just runs over and over again waiting for a system to send a message for different events to be handled asynchronously it seems. There are semaphore implementations in the code, so I assume these different subsystems communicating through this messaging system have a blocking scheme going on to prevent something crazy.
Regarding how easy or hard it'd be to futz with the framerate of something like this, after digging through it for a day, I'd imagine your very first step would be to create some sort of fixed timestep for the physics system as those look like they update at a fixed expected rate. A fixed timestep system is one that subdivides real time into "tics" to be processed. How it works is that you sample a time stamp at the beginning of a single full frame of your game, then at the end of that full frame, you sample a second time stamp. Then, you find the difference in time between the two stamps to see how long your frame took to create in real time. From that real time difference, you convert that to see how many fixed intervals have passed and update the physics by that many steps.
Example, say I want my physics to update at 120 hz, i.e. 120 times a second. That means each "tic" of my physics calculations are 1000/120 = 8.333 ms (1000 is used because there are 1000 ms in every second). Say my drawing takes place at 30 hz (i.e. 30 times a second). That means each draw takes 33.3333 ms. Say my physics calculations are simple, every physics "tic" I move a ball 1 pixel to the right. When I draw frame 1, the ball is at pixel 1. By the time I draw the next frame, 33.333 ms have passed in real time. In the time 33.333 ms takes, I could run (33.3333/8.33333) = ~4 physics tics. So, for the next frame, I update my physics 4 times - the ball moves right 1 pixel on tic 1, then 1 pixel on tic 2, then 1 pixel on tic 3, then 1 pixel on tic 4. So in all, it's moved right by 4 pixels from one draw frame to the next.
Doing this decouples time specific functions, it keeps time constant regardless of a larger update interval. This works both ways with little effort. You can check against a time threshold and prevent a tic from happening if too small of a step occurred. Say your physics operate at 120 hz, but you manage to get it drawing at 240 hz. You'd want 1 physics update for every 2 draws in that case, which is mathematically simple to do. Using fixed timestep, you can decouple the time sensitive parts of Mario 64, like it's physics engine, from the stuff you want to operate at a variable interval, like the framerate.
This isn't quite a magic bullet though. If you have a high framerate, but physics or controller polling is still done at, say, a 15 hz interval, then the game will feel sluggish and floaty. To make every system in the game operate independently requires much, much more work. Several systems would have to operate at a variable timestep, which requires much more engineering than implementing fixed timestep.
Also, as I mentioned in an earlier post, this is really, really incomplete. OP kind of jumped the gun here, I'd say a good 30-40% of the code is still indecipherable machine translation. So there could be parts of the engine that are timestep dependent that one might not even see or notice, because it's obfuscated in function by just some random naming symbols. Only the annotated parts of the source code are discernible, the parts that aren't are basically gibberish.
edit: checking out the code for the profiler, every system is run in it's own thread - audio, video, etc. The profiler is already set up for fixed timestep in the way it profiles these systems, so something like I describe for the main game loop is totally doable.