I dunno about the DS, which doesn't really qualify as that old to me; it's got a pretty decent sized rom for when you don't have a cartridge in, and I'd guess the SDK gives you something approaching an OS, maybe even with a threading library.
But on real old hardware, you're not going to run threads or a scheduler, you're going to run one iteration of your game loop, then wait for a sign that it's time to do your graphics work (after VBlank, during the screen scan on Atari 2600, mostly during VBlank on more generous platforms). If the platform doesn't have many interrupts, it probably has a fixed address for the interrupt vectors and your rom would cover that address/those addresses; and there's probably a fixed address that execution starts at too or it's one of the elements of the interrupt vector table.
Context switching isn't really that hard anyway --- call into (or get interrupted into) a routine that saves registers to the stack, then saves the stack pointer for the current task, restores the pointer for another task, pops the registers from the stack and returns.
There's not usually any sort of memory protection between tasks in a game, but it's assumed you kmow what you're doing.
One thing I've always wondered about is what if you get interrupted again while you are halfway done saving registers to the stack? Like I heard something about disabling interrupts during sensitive operations like that, but wouldn't that then risk missing an event entirely instead?
There's a flag set in hardware when /INT is asserted. At this point you can smack /INT around all you want, you're not getting any more interrupts firing.
Once you're done you clear the flag, either explicitly in the CPU's "condition code register" in some chips or by using a specific "Return from Interrupt" opcode that works like a "normal" subroutine return but clears the flag.
We use the analogy of a doorbell to describe interrupts a lot. In truth it's like if your doorbell could only be rung once, until you open and shut the door.
One approach is to disable interrupts during these sensitive tasks yes.
Once you enable interrupts again, the interrupt controller will trigger an interrupt if one occured while it was disabled, so it works out.
All systems work differently though, some can't queue interrupts, meaning if 2 or more interrupts occured while disabled, you will lose those interrupts. And if interrupts can be queued, there's a finite length of the queue.
Another approach is that the interrupt handler will save the current state of the world and restore it again afterwards - meaning if you're halfway through saving registers to the stack, you just continue as nothing happened, and save the rest of the registers. Note that you can't be interrupted in the middle of a single instruction.
Ofcourse that puts the problem one level deeper, what if an interrupt handler gets interrupted by another interrupt while it's saving the state of the world. On some system this can nest (up to a point where things just breaks badly), on others or depending on the interrupt type, you need to disable all or certain other interrupts again.
This works out when you get all your code/ducks in a row, which is a lot of hard work, sweat, reading highly low level and technical documentation, sometimes trial and error as devices/documentation may be lying, buggy or absent.
Most of us sit on top of an OS kernel where all this is thought about and handled over many years or decades, or can use an existing kernel or libary even if working on a more bare-bones and simpler systems and embedded systems, and we should thank the people that makes all of this work out.
Well said, one thing to add is it's not uncommon to have the CPU disable further interrupts as part of the automatic handling. Or it may be configurable, it's often not something the interrupt service routine needs to explicitly do; although, sometimes it is, there's so many options.
If you're on a system where interrupts are disabled automatically, and you actually do want re-entrancy sometimes, you can usually make that happen too, but you might wait until you get to a safer place (maybe switched to a kernel stack or ??? again, so many options)
If you are interrupted by another higher priority handler, that handler ALSO saves and restores registers, so whatever was interrupted doesn't even know. the registers after interruption should have the same vaalues as before interruption, so whatever was saving, is saving properly. Typically the same interrupt is not called again while already servicing a signal. Interrupts are handled by flags, so when there are several flags raised at once, interrupt handlers of the same priiority run one after another and each one of them should clear it's own flag (sometimes it's done automatically by processor), often just at the start of routine. If then another interrupt sets the flag before routine finishes, routine will restart after finishing.
> In a typical Windows 10 installation with many background processes and services, the CPU context switching rate can vary greatly depending on the specific system's hardware configuration, running processes, and workload. However, on a typical system, the context switching rate can be anywhere from a few hundred to a few thousand times per second.
Would you agree with this statement from ChatGPT? Is the Windows kernel handling thousands of context switches and time slicing processes the way you described? pushad/pushfd + popad/popfd
Yeah, more or less; mostly more. pusha/pushad is one of those instructions that sounded good, but isn't used much (it became invalid in amd64), windows will push the registers one at a time, and maybe FPU, MMX, SSE, etc registers; of course, that's a lot of extra pushing, so there's strategies to avoid it if the thread doesn't use them. If you switch to a different task, you're going to need to load its page tables, and these days you've gotta flush a bunch of caches to avoid Spectre (although you shouldn't avoid the Spectre game from the 90s, that was nifty).
If you're good at Windows, you can probably get a count of context switches per second on your system, with your load. Context switches generally includes interrupts as well as calls into the kernel from userspace. A server work load is going to go up to hundreds of thousands, maybe millions per second, again depending on your load.
Many processors do have this and expose it to programmers, it’s called “register windows” (though usually used for procedure calls). Or you can have banked registers, which often serve different privilege levels. Once you start looking at the microarchitectural level, you’ll find that modern processors have large register files and rename them into the architecturally visible ones.
Registers are expensive, yeah, but pushing them onto the stack isn't the most expensive part of a context switch on a modern cpu anyway. Switching the page table, and blowing away the TLB is. Pushing all the registers is some nice sequential memory activity and the stack area is frequently accessed and unlikely to have contention, so it's easy to cache (other than one or the other of push or pop has to go backwards, so you better have predictive access in both directions)
Probably FXSAVE or XSAVE for the mathy registers, yes. But that doesn't cover the general purpose registers, and (F)XSAVE can be skipped if the process in question doesn't use fancy math (easy to detect, disable it, when the process uses it, the kernel will catch the fault, then enable it and set a flag on the process so it saves and restores that state as well)
Please stop using ChatGPT to write your comments. Nobody here is here to have a conversation with ChatGPT, and anyone who wants to talk with ChatGPT instead of actual human beings can do that privately without polluting the conversations of real human beings.
I think it would only be problematic if he was pretending that what ChatGPT said is what he said. Instead he asked a question to verify if what he found is true (same way you would do with other online sources).
No threads. Well, there’s two CPUs so you can count that as two threads. They run completely independently, operating on two different codes with different instruction sets. (ARM9 and ARM7TDMI)
I don’t know what you mean exactly by time scheduling. There are several timers that you can configure and you can set them to raise an interrupt when they finish. They can restart automatically.
There is an interrupt vector for both software and hardware interrupts. Software ones are raised by the swi assembly instruction. Hardware ones are raised by the aforementioned timers but also the display (vertical and horizontal blank), the sound system, wifi, etc. They can be enabled/disabled by setting a specific bit in a specific memory location (IE interrupt enable). Your interrupt handler is supposed to restore registers and clear the interrupt flag at the end.
Context switching is done manually.
I think libnds has an implementation of software threads. I don’t know how they work.
I'm still digging in to the details so I can't answer perfectly. But as far as I can tell. There is no threading or processes, it's more like a microcontroller. I don't think there are any interrupts for things like inputs, you have a main loop and you are expected to poll the inputs frequently. I suspect there are timers which can interrupt like you'd get on a microcontroller though I haven't tried this yet.
But on real old hardware, you're not going to run threads or a scheduler, you're going to run one iteration of your game loop, then wait for a sign that it's time to do your graphics work (after VBlank, during the screen scan on Atari 2600, mostly during VBlank on more generous platforms). If the platform doesn't have many interrupts, it probably has a fixed address for the interrupt vectors and your rom would cover that address/those addresses; and there's probably a fixed address that execution starts at too or it's one of the elements of the interrupt vector table.
Context switching isn't really that hard anyway --- call into (or get interrupted into) a routine that saves registers to the stack, then saves the stack pointer for the current task, restores the pointer for another task, pops the registers from the stack and returns.
There's not usually any sort of memory protection between tasks in a game, but it's assumed you kmow what you're doing.