12

When the IBM PC was released, it did not take long for people to figure out that there would be a big market for compatible machines.

The first wave relied on MS-DOS as the compatibility layer. The theory was that applications wouldn't hit the hardware directly, they would use DOS calls instead, so you could sell slightly incompatible hardware as long as you had an x86 CPU and a version of DOS tweaked for your machine.

The problem was that the DOS display code was weirdly slow. Seriously: the first time I wrote a program on a 286 PC compatible, in C, with (normal 80x25 text) full-screen display, was in the late eighties. I knew in theory you were supposed to go via DOS, so I tried that. To this day I have no idea how they managed to make it that slow without deliberately putting delay loops in the code. Second attempt via BIOS: faster but still much too slow. Third attempt straight to hardware: screen updated instantly. So I shrugged and kept doing it that way. So did everyone else, which is what killed off the semi-compatible DOS machines. The second wave understood the need to be one hundred percent compatible at the hardware level.

But was there anything other than video that was a source of hardware compatibility issues in the first wave? Or put another way: after video, what was the second most common source of compatibility issues for the semi-compatible DOS machines of the first wave?

2

3 Answers 3

13

But was there anything other than video that was a source of hardware compatibility issues in the first wave? Or put another way: after video, what was the second most common source of compatibility issues for the semi-compatible DOS machines of the first wave?

On the software side I'd say sound may lead a tiny bit before any other hardware device - then again, fast serial did need direct hardware access on the PC, so serial might be top sound.

On the hardware side it was simply incompatible expansion slots, limiting the hardware available.


Beside that, I'd like to add some remarks:

The first wave relied on MS-DOS as the compatibility layer

And BIOS, as next to all compatible manufacturers did as well support all BIO calls as well.

The theory was that applications wouldn't hit the hardware directly, they would use DOS calls instead, so you could sell slightly incompatible hardware

Incompatible in itself wasn't a goal. Beside cases where it was about using existing x86 machines with DOS - which in facts predates the PC, as SCP sold its DOS to several x86 boards, including their own, before licencing it to Microsoft.

as long as you had an x86 CPU and a version of DOS tweaked for your machine.

Tweeked sound like patching and a somehow lesser quality. DOS is based on a clear separation between the DOS itself, and its hardware abstraction layer (IO.SYS).

DOS was neither originally written for the IBM-PC nor used first with it. All adaptations to any specific machine had to be done by the manufacturer (see an in part explanation here). Microsoft offered a set of examples and guidelines how to do it, the rest was up to each vendor.

Adapting DOS to hardware was a straight job and standard procedure.

The problem was that the DOS display code was weirdly slow. [...]

I knew in theory you were supposed to go via DOS, so I tried that. To this day I have no idea how they managed to make it that slow without deliberately putting delay loops in the code.

The negative performance impact depends in part thru the layered structure IBM used. While DOS calls all functions in IO.SYS as far calls, IBM's IO.SYS is for much of the time a warper around BIOS, invoking it via INT calls. Not exactly the shortest path. But ...

Second attempt via BIOS: faster but still much too slow. Third attempt straight to hardware: screen updated instantly.

This usually depends way more on your program code (and language runtime) than DOS (*2):

For example if the language hands over each and every character on its own to DOS instead of doing strings. Sending "Hello World\n\r" via Function 02h (Write Character) adds 13 times the overhead of calling DOS, IO.SYS and in turn BIOS compared to using a single Function 09h (Write String) call. Each invocation of DOS (INT) and call from DOS to IO.SYS (CALLF) with its returns alone are ~182 cycles (72+36+34+44) (*3,*4) plus whatever is needed in parameter conversion and alike. It's easy to see that outputting a whole screen takes close to 0.1 seconds (only these 4 instructions) overhead between using either function. And that's still before invoking BIOS, which in case of IBM takes another INT call, adding another 116 cycles.

And yes, many language runtimes acted exactly that way.

Related to this but entirely program dependant was how a screen was handled. Of course, this depended a lot on the application, but staying within text base, it's again a huge difference between brute force updating, like cranking out the whole screen or large parts thereof with each update, or only update fields that have changed. The later was already a huge difference when working with serial terminals and it was as well true for the PC.

One value I remember is that for one system we had the average screen size in 'uncompressed' mode was about 1.2 KiB per screen. This includes all characters attributes and field markers. By using switching this for only updating what has been changed, i.e. calculating a delta and send only changed values, after positioning. It wasn't easy, but the savings were astonishing. The average dropped from over 1200 bytes to less than 300. Our terminals were connected on 288 kBit lines, so comparable fast for 1980. Still, reducing it to less than a quarter changed it from being fast to instant.

Long story short, it's exactly the same situation with the IBM-PC (*5). It depends on the way the terminal output is organized, with an over all goal of minimizing output.

I did several programs on the original PC (I was young and needed the money) at first in BASIC but soon (1984?) in Turbo PASCAL. Applications were entirely text based and for an architecture firm, so lots of numbers. some screens looked more like spread-sheets. Still it was no problem to keep updates fast - by only positioning and writing what has changed. And yes, all was done using DOS output and terminal like sequences - not at least due the reason all development was done on an Apple II using CP/M. I simply didn't want to switch at the time (*6). I guess it can't get less compatible :)

So I shrugged and kept doing it that way. So did everyone else, which is what killed off the semi-compatible DOS machines.

Mind to add what region you live(d) in? (*7) As this does not match my (European) experience. A large number of machines with less than perfect (or no) hardware compatibility prevailed during the 1980s. Sirius, Apricot, Amstrad, Siemens, Philips, Thomson, Olivetti, and many others did their own designs, only partial or not compatible at all. Some, like the Olivetti M24 SP, a 10 MHz 8086 version coming as late as 1986 (like the Amstrad 1512) and sold until the early 1990s.

The second wave understood the need to be one hundred percent compatible at the hardware level.

Again, this may differ in region.

Also there's a large number of what I would call minimum compatible machines. In the US the Tandy model 1000 might be the best known one. They may have featured some basically compatible video modes to make software run, but for serious use they did need specific drivers. I guess we all remember the marking on game boxes - some even differentiating between Tandy 1000 models. This lasted well into the late 1990s.

[...] compatibility issues for the semi-compatible DOS machines of the first wave?

By now (aka 40 years later) we should have learned to stay away from single sided and twisted wording as it was used back then to promote a certain agenda. These machines were DOS (and usually BIOS) compatible computers. There is no semi-compatible when it was about these software interfaces. Only the hardware side had a continuum of compatibility, starting with processor speed and type, over video memory location (*8), all the way to fully incompatible.


*1 - Here BTW non compatible machines provided additional speed just by the way of calling. Like the AT&T PC6300 (a rebadged Olivetti M24) directly called the ROM routines, not going thru another INT but using a far call to ROM , thus speeding up every invocation (36 cycles vs. 72). Others, like the Siemens PC-D, were the BIOS was integrated within IO.SYS, shaved of even more overhead.

*2 - But of course the way it is implemented. See *1.

*3 - All cycle counting in 8088 reference. 80286 might be faster, but not much (47+23+22+28=120).

*4 - Or it can be, depending on the implementation in IO.SYS. In every case it saves at least 12 invocations of DOS and DOS calls to IO.SYS.

*5 - In fact, it's even close numerical with what's possible on a PC.

*6 - Which had the advantage of being able to program using my Apple II. Development cycle included programming and debugging on the Apple, moving it to a PC, that has only be bought to compile and write PC floppies to deliver.

*7 - Filling up ones RC.SE profil, including the location, does help in communicating, doesn't it?

*8 - I used a PC-D until the late 1990 as my main work machine. I simply loved the keyboard and it was fast enough for editing and compiling. It was in no way hardware compatible by using a 186 CPU and not a single interface at the same address, even more, using different chips for all I/O. In my memory much software did run right away, and a great number needed only small patches - like changing the segment address used for the video buffer. Of course all Windows and GEM software did run right away as supposed.

7
  • 2
    Good answer, thanks! Though I definitely didn't do DOS or BIOS output one character at a time. I'm in Ireland. First time I wrote code on a PC compatible was in 1988.
    – rwallace
    Jun 26, 2020 at 0:51
  • 1
    It should be noted that MSDOS 2.11 could run on just about any 8088/8086 machine. It was similar to CP/M in that a software based "BIOS" was used. I recall MSDOS 2.11 running on machines that used an ASCII monitor and keyboard connected to the computer via a serial cable.
    – rcgldr
    Jun 26, 2020 at 7:36
  • 1
    "I did several programs on the original PC (I was young and needed the money)" - oh come on, programming is not some dirty job that we don't like to speak about in polite company :) Be proud of it! It's awesome that you saw that opportunity and pursued it, acquiring the needed knowledge. Jun 26, 2020 at 17:55
  • 1
    I was asked once to write a code inliner that could be used to speed up generic applications on systems that only had BIOS-level compatibility. It was done for a school district, i.e. for an end user, and not a software vendor. It did inline BIOS code into DOS, producing a custom DOS - removing the layers of indirection, and it had a peephole machine code optimizer that was adapted for the systems it was used in. It made the life bearable for users of the oddball systems bought in large quantities via government contracts. It was used on XT and AT-class systems. Jun 26, 2020 at 18:03
  • 1
    @ReinstateMonica LOL. Thanks for the nice words, but back then doing an IBM PC job (for a genuine PC user) was only barely above touching a C64. Which, for an Apple II user was next to unthinkable - we had standards :) The printer job sounds nice, just, why didn't you use the print spool that came with DOS?
    – Raffzahn
    Jun 26, 2020 at 18:13
11

Aspects I recall, perhaps influenced by the area I was working in at the time:

  • Video display (as you mention)
  • Serial ports
  • Timer interrupt

I only had to work with "near-compatible" machines a few times before everything went to "100% compatible" for most hardware interfaces.

1

If video was 1st, then printing was a close behind 2nd.

  1. Applications needed their own printer drivers to format the application's data for printout, in a language that the printer could understand. There were several families of printers, each family's language somewhat compatible within that family (often with caveats and incompatibilities). Yet many oddball printers existed in the 70s, 80s and into the 90s that were compatible with nothing else and needed specialized support.

  2. Applications could achieve better performance by skipping the BIOS and/or DOS and accessing the printer interface hardware directly.

A program could generate print data and send it directly to the printer interface I/O port. It was faster by bypassing the DOS printer device, especially if the software's data generation speed was comparable to print speed. Software could then completely avoid buffering - simplifying the code, and lowering overhead.

I implemented such techniques a few times long ago. The following description is based on what I can still remember from those "good old days". Well - no, there was nothing too good about it. There's a finite amount of fun one can have in writing banding rasterizers and debugging ESCp language incompatibilites. I'm sure I killed some of my hearing by sitting in a room with a couple dozen dot-matrix and daisy wheel printers, driven from a hardware buffer box, with print data read from floppies. It was a terrible racket. Supporting printer output was hard work before Windows restored some sanity by offering an abstraction over printer drivers.

Print Data Generation

An application generates the data for printing by "walking" over some data structures that represent the document to be printed. Such "document" may be mostly text - as in a word processor - or it may be some graphical primitives, such as in a CAD program. Sometimes the document is pre-processed into an auxiliary data structure, called a display list, that is used to effectively draw it on an output device (screen, printer, etc.). Whether a display list is used or not, the overall process still involves "walking" or iterating over some data, and converting it into a format understandable by the printer. The details of this process vary a lot based on the capabilities of the printer. For example, if the data is text, it may be sent to the printer almost as-is, with only minor additional formatting data added. If there are graphical primitives, they have to be converted into bitmap (pixel) data for printing - unless they were rendered on a Postscript printer, an expensive rarity. Whatever the process, its output was a stream of bytes to be sent to the printer.

Data Output to the Printer

Getting this data to the printer depended on the speed of data production by the application, relative to its consumption by the printer. If the application is much faster than the printer, it'll likely need to buffer the data somewhere - perhaps in a disk file. Otherwise, the application may be stuck waiting for printer, unable to interact with the user.

Applications with graphical output usually needed to use the entire limited RAM to produce printer output. There was no space left to keep the state needed for user interaction. Thus, it was beneficial to be "done" with printing as quickly as possible, to return the control to the user.

Applications that outran the printer would save the entire to-be-printed output to disk. This was later read in the background and sent to the printer - either by the application's own hardware driver, or the DOS printer device (LPT), or by the DOS 2.0 (and later) print spooler.

The Benefits of Fast Printers

When laser printers came about, they offered a known minimal amount of internal buffer RAM. The program that did the printing could estimate whether its print output would fit in the printer's buffer. You could let the program know the amount of RAM actually installed in the printer. When the program could foresee that the raw bytes to be sent to the printer would fit in the printer's buffer, it wouldn't need to use any intermediate buffering. Any time it had a byte of data ready for the printer, it could load it directly into the printer port's hardware, with minimum or no waiting involved. In all cases this took less CPU time overall than sending print data in the background using interrupts. Not only did this save on any RAM needed for buffers, but it also saved on CPU time needed to copy data around, i.e. to a buffer, and then out of a buffer.

Thus, some DOS applications had a printer "driver" that would both format the data for the printer's language and send it out immediately via parallel port hardware. It was usual to employ modifiable code, so there were no separate variables storing the printer port in use. The I/O addresses needed for a given printer port were read from the application's configuration file, calculated in the print setup code, and were written directly into the driver's code. This minimized both the memory and CPU instruction overheads.

Sidebar: "Adding" the Printer

These days, we "add" a printer to the operating system, and that's that. But there was no such centralized printing infrastructure on plain DOS.

There were graphical output packages that applications could leverage to access some number of display and printer/plotter hardware. Those packages provided library and/or resident code that the application could use directly, instead of implementing its own system. But that came later, and effectively ended then Windows became the defacto "graphical output package".

But how was all this configured? Early PCs had little memory: configuration files would be a luxury at runtime. Their use called for more code that had to fit in the limited RAM. Instead, there would be a separate "installer" or "configurator" application that would directly patch the main application's code to adapt it to the user's hardware. E.g. printer port selection would be translated to I/O port numbers written directly into the executable. Selecting a print driver could be implemented by copying the print driver routine into the executable.

The limited RAM presented another problem: all the code that an application may need would not fit into RAM at once. Two solutions were common.

Overlays

The application would, at runtime, read chunks of its code from disk into RAM. The chunks contained related code and data, e.g. printing code. This made the user experience more seamless: the user interacted with just one application, even though technically the application would replace large parts of itself as needed, acting as if it was multiple sub-applications, with any one resident in RAM at any one time.

Judicious choice of chunk sizes and contents could provide a reasonable user experience, with minimum wait times. On floppy-based systems, the response latency was dominated by the time needed to spin up the drive and access the sector(s) containing the requested data, but even a 1-2s latency was 1-2 orders of magnitude better than having to switch applications manually..

Multiple Single-Task Applications

This was more common in technical applications, their users more tolerant of abysmal and arcane user experience. This approach dominated in the computer aided design (CAD) systems. The primary application was the graphical editor. A multitude of auxiliary applications were used to do everything else. E.g. to run a design rule check on the documented, you exited the graphical editor, ran the DRC application, read its summary output on screen, and if any errors were indicated you had to print out the full DRC report. The graphical editor had to be restarted, the fixes implemented, and the process would repeat. To print the document or convert it into other "output" formats, a separate application was involved.

The choice of functionality boundaries between the auxiliary applications often had to do with implementation details. Suppose we're still talking about CAD systems, their native data using a "vector" representation, i.e. simple primitives like line segments, arc segments, polygons (potentially filled), etc.

A "plot" program would handle output in vector formats. The "vector" data was simplified to what the "plotter" could understand, and then formatted for the chosen output device - such as a desktop plotter, a LaserJet printer using HPGL language, a PostScript printer, a numerically controlled mill/engraver, or a photoplotter.

A "print" program would handle output in bitmap formats. The "vector" data would be rasterized by generic code, provided with output-specific parameters such as resolution, colorspace/gamma curve and raster pattern. High resolution printing on 9-pin dot matrix printers used a non-rectangular raster pattern, and the rasterizer had to be aware of that - otherwise an expensive post-processing step was needed. The raster data was then formatted in the language specific to the given printer, and then typically it could either be printed out directly, or saved to a file for later printing. It wasn't uncommon to plot or print to a file on a floppy, then carry it elsewhere to be output.

4
  • Sure one needed printer drivers, but that issue was the same for the IBM-PC, as neither DOS not BIOS did provide any layer beside output buffering by DOS' PRINT command.
    – Raffzahn
    Jun 26, 2020 at 18:02
  • Since the printer isn't really part of the base PC hardware, it seems like it's not an incompatibility in the "MS-DOS machine" in the terms of the OP. The same printer driver works on Brand X PC and on Brand Y PC.
    – dave
    Jun 26, 2020 at 18:04
  • It's arbitrary, but I don't consider "the machine" to be just what fits into the box with the mainboard. Peripherals are just as much the part of this "machine" from the software point of view. Jun 26, 2020 at 18:10
  • Peripherals, yes, e.g. the keyboard; but there wasn’t a “reference” printer on the PC platform... (Unix systems ended up assuming PostScript, Macs had the LaserWriter, but there was nothing to be incompatible with on the PC.) Jun 26, 2020 at 18:50

Not the answer you're looking for? Browse other questions tagged .