Our first challenge with PC compatibility is one of speed. This one won't be short -- I want to go in-depth about why some games and software might run too fast for us on certain kinds of hardware. This is more of a history lesson than a post about actually buying or using hardware, because I want to get some context for why PC compatibility isn't quite as easy a question as multiple generations of hardware might make it seem.
When the IBM PC came along, the ecosystem of "personal computers" and computing hardware in general was vastly different from how it is now. Between the way the market was constructed and the way that hardware was limited at the time, software wasn't designed with long-term use in mind, or at least not with what we'd think of in terms of interoperability or portability. Software compatibility was low, because most hardware was closed off and incompatible.
From here on I'll be referring to the IBM PC, as in "the personal computer model designed and sold by IBM", and its broadly-compatible inheritors and clones, as simply "the PC". That was its brand name, the "IBM Personal Computer". Pretty basic, and without the IBM branding would have been generic and ambigous; the term "personal computer" was already in use to describe any computer that you yourself owned that you could have in your house, as opposed to a large server type of machine that would service multiple users through a remote connection from a basic terminal (think of today's "cloud computing" as the modern equivalent). These had existed for years before IBM sold one, but the term has become synonymous not with any computing device but with specific hardware derived from its architectural decisions and, ultimately, tied to its original architecture. If I am referring to a PC that is not derived from this IBM architecture, I will refer to it by its brand name, like the Apple II or the Commodore 64, or an abbreviation thereof. If I refer to a PC rather than the PC, I may not be referring to the IBM architecture but to the notion of a computer that you would be able to own, in your home.
Of course, this clarification of terminology comes with a specific point about the hardware market -- you could already have a personal computer by the time that the PC was out. There was, however, no notion of a standard architecture. Machines were all, broadly, incompatible with each other, systems usually only came with very basic operating systems, and most had very limited abilities for expansion. When you got a computer it was almost always in a form factor of plastic (typically black or beige) with a mounted keyboard. Some had an expansion slot. Some had built-in storage support like a tape deck or floppy drives. Lots of them had different CPUs, and they basically all had bespoke solutions for graphics, and many did for audio as well. In general, PCs were a lot like video game consoles that had keyboards; indeed, several consoles and computers were nearly the same architecture or the exact same architecture but with bundled input and output peripherals. The Atari 8-bit line like the 400 were basically indistinguishable from the 5200 from a specs standpoint; the Coleco Adam was just a ColecoVision but with, yes, a keyboard and tape deck; the SC-3000 was a computer-ified version of the SG-1000 (itself based heavily on the ColecoVision). Like with consoles, the idea of there being long-term support between systems was not guaranteed. If you got a new computer, you almost certainly needed new software to go along with it. Commodore's PET was not compatible with the suceeding Commodore 64, nor the Sinclair ZX81 with the ZX Spectrum, though all these consoles used similar flavors of BASIC that meant that programs coded in it might be compatible between systems. Most commercial software wasn't, though (assembly code was significantly more efficient, which was tied to the specific hardware arrangement of a given system). Ultimately, software was designed with specific hardware in mind -- there was absolutely no concept of "write once, run anywhere", nor was there any expectation of forward compatibility in software.
That's not to say software was necessarily dead with new hardware. Ways to emulate old software on new machines did exist, so people could run, say, PET software on the 64, but the process of emulation introduced overhead that could mean it would run worse. Operating systems that were designed to work on multiple systems did exist, like CP/M, but they weren't designed as general-purpose and you wouldn't be using it to play games. CP/M was meant mainly for interoperability for business software, and wasn't always totally hardware native either.
Indeed, the Apple II, maybe the most notable counterexample to this kind of hardware design due to including multiple hardware expansion slots, had its struggles in this regard. To run CP/M on an Apple II, a card came out with a separate CPU, a Z80, to support it. The CPU wasn't there to speed up computation, but to make that software run at all. Indeed, while the Apple II's base model was pretty, well, basic, the expansions that it did have often weren't used by software made for it, because that meant that you could only sell to people who had the expansions in the first place, which was necessarily a smaller community than those with the base hardware. But, hey, a compatibility expansion like that wasn't new there, either. Coleco had done something similar with an Atari 2600 hardware clone expansion for the ColecoVision, and wound up in a lawsuit over it (which was surely a contributing factor for why this sort of interoperability wasn't expected).
Of course the Apple II was unusual in a different way -- it had a direct successor in the Apple IIGS that was fully backward-compatible with the original system and had new features! The problem is, it was intentionally designed to run worse than it could have by using an underclocked CPU, to prevent it competing with the Macintosh, which was designed with so little interest in compatibility with the Apple II that it had a completely different CPU by a different manufacturer. The Commodore Amiga developed in a similar direction to the Mac, completely incompatible with the C64 -- indeed, it was much closer in line with the Mac due to both switching from the 6502 to the 68000 CPU in the process of moving from 8-bit to 16-bit architecture, mainly because it was a fairly efficient CPU that had a very intuitive assembly language, but (because it was manufactured by a completely different company, understandably) had no compatibility with the previous system's CPU. This lack of forward-compatibility meant that the old Apple II and C64 were manufactured in various forms for a surprisingly long time. The last Apple II models (a few revisions of hardware removed, changing peripherals but retaining the basic CPU and such) were made in 1993, and for the C64 it was 1994 (probably because the consolized Amiga CD32 sold so terribly it led to bankruptcy for the company around that time, and could otherwise have stayed in the market even longer).
This was how software design was envisioned -- your hardware was pretty static, and it would be around for potentially a while, without changing, and upgrades meant new software entirely. You didn't write code thinking about how people 30 years on would write it -- you knew, or at least presumed, that they wouldn't, any more than you'd expect to walk into someone's house today and see them checking GMail on an Apple II today. If it ran, it ran, but if it didn't, it wouldn't. You tested on that static hardware -- there was nothing else out there. "What if CPUs get a lot faster" wasn't a relevant question for the thing you were coding now; if you had to put it on a newer CPU, it was going to be a different program.
But the thing about the CPU that the PC used, the Intel 8088? Well, it was a slightly reconfigured version of the Intel 8086, with its only real difference being that it used an 8-bit rather than 16-bit bus. This meant that code that was designed for one could probably run on the other, and yes, PC clone hardware variants existed that used each within a few years, though the 8088 was more common especially on IBM's own hardware. The updated PC-AT (also by IBM) released only a few years after the original PC, in 1984, included a improvement in the 80286 processor, which was arguably the real start of what is now called either the "i86" or "x86" architecture, depending on who you ask. The processor was compatible but overall more powerful, and with the expansion slots that the PC motherboard had meant that every part of the system could be more powerful while still retaining compatibility with old software. This was a huge change, and while compatible clones of the PC existed like Compaq's portable line ensured that the IBM PC's general architecture had more market penetration, arguably the AT solidified the notion that the architecture in question was going to continue to be the future of computers (even if IBM was not necessarily the one in charge of it -- see, for example, the fates of the PCjr and the Tandy 1000, which is definitely a story for another time).
The funny thing is that the PC wasn't really designed with much interest in being a gaming machine, arguably less so than the Apple II was, even, due to its higher-res color graphics mode at 320x240 having fixed palettes of only 4 colors each that were somewhat gaudy; the hardware that does this (one of the pieces of hardware a PC's expansion bus would have plugged into it) is called, per the link above, CGA. In contrast, at the same resolution, the already-at-market Apple II had the ability to use 6 different colors in two palettes of 4 for sets of 7 pixels in a row (both palettes had duplicated entries for white and black, with one having green and purple and the other having orange and blue, similar but different from the two color modes the PC supported). Yes, gaming on the PC was inevitable, but compared to nearly contemporary systems like the C64 (which came out a year after the original PC), gaming didn't seem to be the focus of the system, and the aesthetics of what the system produced were, ultimately, limited.
There's a notion of "racing the beam" that describes how games were often programmed, especially for the Atari 2600, which lacked any notable independent graphics hardware and required the CPU to focus almost exclusively on drawing the screen during display cycles on the TV. That's not quite accurate for how the PC worked, but there is a specific key point that both systems had in common for graphics coding: if you wanted to do any sort of real-time graphics, you would have a hard time actually synchronizing them to the monitor. You needed to set the graphics ram to what you wanted to display, and you had to make sure that the display and it weren't competing for use of the RAM.
The PC did have a hardware interrupt to synchronize display information with an output TV, which was generated by a timing chip, the Intel 8253 which was meant primarily to provide timing for synchronizing the machine with a display; the chip had a fixed cycling count that produced an interrupt designed to be a factor of the color signal of a television. It wasn't a very good option to use as a state timer within early PCs, though. The PC had to read out its value, which took multiple operations. The clock value was two-bytes and, because its timing was independent of CPU cycles, needed to be sent a command to fix its readable value (this only fixed what the signal was that it put from specific output lines; the state of the internal timer, which generated the interrupts, would continue to tick down while this was occurring), which was two bytes. These bytes needed to be read independently, and so required separate CPU instructions to perform. This was not very efficient, and early DOS games usually did not try to synchronize their behavior to it -- and, again, there was not that much of an expectation of software forward-compatibility in this era. Manually counting cycles was a more common order of the day rather than trying to sync operations to display intervals, because you wanted a game that was both playable now and (arguably) as good as it could be, rather than simplifying its logic in order to perform robust typing.
Now, when the timer reached zero, it would alert the CPU through a process called an interrupt, which meant that the CPU would be, well, interrupted and have its state set to a specific value to handle the change. In this case it was fixed to interrupt 0. While using interrupts to synchronize game logic is something that was known for a while in games programming, it wasn't very common on PCs. The big problem is that the interrupt, as actually exposed to the CPU and thus detectable by the software, doesn't trigger in time with the actual framerate of the display, but reduced to only about 18.2 Hz. For a responsive game, this is probably going to be a lot lower than what you'd want, as that's smaller than the expected 60Hz framerate by a factor of about 3. You'd still want to do some manual checking to actually see when a new frame is set, or you'd still have to do cycle counting to figure out when the frames actually are -- so it doesn't actually solve the problem.
Here's a video that demonstrates code to draw CGA graphics in a way that uses the interrupt handler to keep code synchronized. While it uses a lot of assembly, it might be helpful in understanding what this might look like from the perspective of writing software. Note that even the guy making it points out (at least in the comments) that interrupt handling is not a trivial operation for a DOS program.
Now, the lack of interrupt handling to time code is maybe a little surprising to people who come at this from an understanding of console game programming, as using display interrupts, like the interrupt generated by the graphics hardware when it's drawn a full frame of data to a TV or monitor, usually called the VBlank (for vertical blank) interval. This was quite common to synchronize game logic on systems that had independent display circuitry (say, the NES), where the VBlank period would be the time when the console was most free to process its system logic. (In comparison, the time while the display was being controlled by the graphics hardware would be used to handle things like sprite data that might need to be updated within the frame, if there was more intended to be on screen than the system could display). Of course, these consoles didn't have anything like an operating system -- everything ran off the cartridge, and the interrupt handlers were all defined by that software, so these interrupts could immediately trigger these kinds of logic changes. This meant fewer layers of indirection in the code to handle those interrupts, so more game logic could potentially happen per-frame.
So ultimately, a lot of forces conspired for early PC games to ignore trying to use any sort of hardware state counter or timing to manage code execution that needed to run at a specific rate, instead using the known CPU cycle counts to try to determine what they could and couldn't do feasibly in a game. The problem is, though, what happens if your game was designed to run with a certain number of cycles per frame of game data, and the number of cycles per second your computer's processor can do goes up? Games obviously aren't like business software -- game code reacts to a player's actions, and then the player reacts to what that code does, and so on until it ends, and there are limits to how quickly a human can react in this cycle without being overwhelmed. This older era of PC games featured many titles that would regularly become unplayable as hardware got more powerful., making the attitudes of their design almost self-fulfilling in a way; they didn't last, and very little software of the time might be regarded as anything but completely arcane by modern standards, relics of a nearly-forgotten era of computing.
Of course, the issues with interrupt handling tying up CPU cycles was increasingly less of an issue as the hardware got more powerful, and the general trend is for games to be better-behaved in the presence of hardware variations like PC speed. The kinds of calculations that needed to be done per frame didn't go up nearly as much as the performance of CPUs did over the decade, and so many games did latch the timing circuit described above in order to ensure future compatibility. Most games, as a result, aren't that timing sensitive. Commander Keen IV, certainly, runs as well on a Pentium II as it would on a 486. King's Quest VI's time-based puzzles do not require superhuman reaction speed to pull off on a machine from the Windows XP era. Software, broadly, works, and upping the speed of your computer almost always helps, and almost never hurts, because games that can handle newer hardware can keep getting sold. As a high-level explanation for what this sort of logic might look like: do your per-frame state calculations for your game, check the timer, use that to figure out however many cycles to wait until the next frame starts, wait that long, and then start again.
Exceptions to the rule of more robust software still existed in a lot of places. After all, even if programmers knew about how to properly time software, they still had to use several CPU cycles to get its state. If developers were writing software whose performance was at the upper end of what CPUs at the time could reasonably do, whatever time that was, they might skip it entirely. Wing Commander, which came out in 1990, is an excellent example of this sort of issue -- its framerate is unlocked, but the framerate is tied to the game's logic, so on a more powerful PC, it becomes too fast; Wing Commander was no DOS gaming also-ran either, as it was one of the highest-profile releases of its time. It is extremely not future proof, and requires specific configurations to run correctly (and, unsurprisingly, not anywhere near a consistent 60FPS when this is correctly set up).
These aren't the only examples. Anything compiled with, say, Borland's Pascal tools had a sort of bug in their delay timer code that didn't run correctly on faster machines. The compiled code times delays by...yep, counting cycles within a certain range of time. Above a certain clock speed (around 200Mhz, which some Pentium IIs could do), this could lead to counting so many cycles that the counter would overflow, breaking the program, but rather than making it run too fast, it simply didn't run at all instead. Fortunately, there is a patch (available here) that can let these run without this restriction -- being the result of an oversight in the standard libraries means that compiled code is pretty consistent to patch, thankfully.
And, yeah, when you add in Windows to the mix there were even more potential inconsistencies! While by the time Windows 3.1 was out, PC gaming was definitely an established force in the market and could regularly produce experiences on par with or better than consoles at the time, this was almost always done externally to the OS. Windows, however, was not designed to support that style of gaming, and so games made to run for Windows tended to be slower, with simpler graphics, limited scrolling, and usually less sensitive to time -- think card games, board games, and puzzle games. Trying to play more ambitious stuff in the Windows environment (i.e., not just direct through DOS) was challenging and usually required doing things with the available tools that they weren't really designed to accommodate, and usually involved "hacky" or cheating solutions, especially if they were using more simple tools like Microsoft Basic to write their programs. Windows games that tried to do things like that in the early days are very CPU sensitive and tend to be a compatibility nightmare, the absolute hard mode of software.
What's less fun is when your game normally would run fine, but doesn't manage to detect hardware correctly because of how fast the system is. Sound detection in a lot of games I've played is a good example of how this can go terribly wrong -- autodetection or initialization involves the CPU sending some commands to the sound card, then waiting for the sound card to have the expected status. On a faster CPU, this can take longer than the expected number of cycles to wait, so the sound card doesn't produce the right status. Despite the fact that the sound hardware is there and would work just fine if the program weren't so hasty, it assumes that actually there is no sound card and so produces an effectively silent game. Oops!
The good news is, that at least when the limiting factor is just CPU speed, we actually have a lot of potential ameliorations in hardware, both then and now. In fact, one of the reasons I'm starting with this topic is that it's probably the easiest thing to fix, which is potentially good, because it would mean that a single piece of hardware could cover a lot of potential use cases. In the next part of this series I actually want to talk about how we can slow down a CPU to make software more playable, and what the limitations of some of those solutions are.