Project: 2ine

Creator profile picture

Ryan C. Gordon

Jan 21, 2018

Project: 2ine

Jan 21, 2018

You have no idea how much effort went into getting this stupid white square on the screen.

I thought I’d explain what all this is about in more detail, since I mentioned it in the December wrapup. This is a long and technical post about weird low-level stuff and you can totally skip this if you don't care about weird low-level stuff.

I've been building a project called “2ine,” pronounced “twine,” which emulates OS/2 binaries in the same way that Wine emulates Windows binaries. I named it as such to continue the fine tradition of programmers producing good code and bad product names.

Any introduction to computers starts with a whole lot of lies. It’s complexity all the way down to the bottom, and you don’t need to know it all when you just want to write a program that prints out “hello world.” This is a blessing and a curse, but mostly a survival mechanism; if you knew everything you were about to stumble into, you’d never start. In this way, I stumbled into writing an OS/2 emulator.

In 2003, when I was working on what would become Unreal Tournament 2004, I was trying to get the software renderer working on Linux. The market was rotten with fast CPUs and lousy GPUs, and OpenGL support on Linux was spotty back then in any case, so having a software fallback was super useful. For a software renderer, UT2004 uses RAD Game Tools’ Pixomatic, which is one of those amazing pieces of code that seems to defy physics.

Pixomatic is itself a technical marvel of lowlevel x86 sorcery; Michael Abrash wrote about it in detail here and here and here. But of all the things it can do, all you need to know for now is that the “Linux port” of Pixomatic consists of a Windows .dll and a small piece of C code to load it.

I’m not kidding.

All Pixomatic cares about is lighting up pixels in a memory buffer and only needs the win32 API to manage pages of memory. So this C code would get the Windows DLL loaded, provide a few function pointers that would call mmap() when Pixomatic thought it was calling VirtualAlloc() and such, and then everything just sort of works on Linux. At the end of our trip to Win32 Land, UT2004 would use SDL to flip that memory buffer to the screen as if nothing unusual happened here at all. It’s wild.

What surprised me about this wasn’t the approach—although that was unexpected too—but the simplicity of the C code that loads the DLL. 618 lines of code! Is this all a DLL needs?

The answer, as always, is yes and no.

Shared libraries on most operating systems work like this: you get a table of information of where to place data in memory and the system loader blasts it out there. There’s more to the effort than that, but the juiciest tasks are:

  • Load a data block into memory.  It might be code, or initial data for global variables, or some constant thing like string literals.

  • Fix up specific bytes in memory with real addresses of things it needs. When your program calls printf(), this puts the actual address of printf into memory for the call instruction. There are lots of different kinds of fixups for different needs.

  • Keep track of symbols exported from the library so you can fix up other things that need this library later.

And that’s it! In most cases, programs you run use the same format as shared libraries with minor differences. (Linux users! Did you ever try to run libc.so.6 like it was a program instead of a shared library? I’ll wait while you try it.)

Pixomatic’s freakishly simple DLL loader got stuck in my head. Once you do the loading and fixing up, this isn’t really a win32 program at all any more. It’s code in a Linux process. Could we load other things like this? You bet.

Ten years later, I stumbled into writing an ELF loader for Linux, because dlopen() needs a filename and I wanted to load shared libraries from a memory buffer. ELF is more complicated than the Windows format, and this code is more robust in general, getting us to about 1200 lines of code, in a project I called MojoELF.

Once I had built that, I thought: if I load an ELF binary on a Mac, it’s no longer a Linux program. It’s code in a Mac process. And since the Mac has all that POSIX goodness and a quality SDL port, if we fix up the POSIX calls to function addresses that bridge differences in data layout, and fix up calls into SDL to the real Mac SDL library, well…maybe we can play a Linux build of Quake 3.

None of this is novel at all: this is roughly how Wine has always done things. They just had to work harder to deal with system calls into an OS that’s completely alien on Linux.

But in any case, this is an idea that works.

I don’t know what prompted me to do an OS/2 loader in the first place, but it was probably a straightforward case of nerd-sniping. I keep a list of interesting-waste-of-time projects and sometimes when my mind wanders, I foolishly look at this list and my productivity drops to zero until I can build some ridiculous thing that caught my eye.

Documents that explain the OS/2 file format (the binaries are called Linear Executable format, LX for short) are easy to find on the Internet, so why not try? It should send up alarms when I tell myself “Why not spend an hour and see how far you get?” There should be a Degrassi High School episode about this exact scenario, to serve as a warning to future programmers during their formative years!

Like that Pixomatic C code, loading an LX binary into memory is mostly easy. But even if you discount the ugly corner cases, you still have to implement the OS/2 API for the loader to be useful.

The first version of my OS/2 loader ran exactly one program, a Hello World thing, written in assembly because loading a C runtime was too complex at this point.

(Strictly speaking, the true first version was probably a program that set EAX to 42 and returned, just to see if the process exit code was also 42 when 2ine terminated, demonstrating we could bounce into OS/2 Land and back safely.)

OS/2, like Windows, doesn’t offer a single C runtime like Linux and macOS do, and all of them do some complicated tap dancing with system calls before main() even runs, and I didn’t want to mess with that yet. Eventually, I got to filling in some basic Unix-like bits, with the naming OS/2 uses: DosOpen() to open files, DosWrite() to write to a file handle, etc. The nice thing about DosWrite() is that file handles 0, 1, and 2 match up with Unix stdin, stdout and stderr; this helped get a bunch of OS/2 command line programs running without added drama, and you can even pipe them through to other Linux processes.

System APIs are written in C, as native Linux code. When the OS/2 module reports that it needs the (system-provided) DOSCALLS.DLL, 2ine dlopens its own libdoscalls.so with these reimplemented APIs, and uses the native Linux entry points to fix up the OS/2 module. Now when the app calls DosWrite, the CPU calls directly into a Linux ELF shared library where this function was reimplemented, not knowing the difference. The 32-bit calling conventions happen to match up well enough between the two platforms that it just happens to work.

An OS/2 app can run under 2ine using a mix of native Linux libraries that reimplement system APIs and real OS/2 DLLs, so long as those DLLs don't do weird things or depend on DLLs that do weird things (what qualifies as a "weird thing" could fill a whole other blog post, though). Right now, some things will run on 2ine if they have access to a handful of IBM's system DLLs with native libraries spackling in the cracks, that otherwise would fail to operate.

With some effort, and with enough APIs filled in, the OS/2 port of GCC that I used in high school (2.8.1! It doesn’t even support Pentium 1 instructions!) started running, since it doesn’t need much more than stdio and a way to launch child processes, allowing me to build OS/2 programs on Linux, using the compiler under my emulator. Now we’re getting somewhere!

And then I thought, hey, let’s get Watcom C working too, and here my troubles began. Specifically, my troubles began with the compiler’s help screen saying “press return to continue.”

(That’s right, you can debug OS/2 binaries on Linux with GDB by running them under 2ine, as long as you don’t expect debug symbols or source code views! Also, as long as you can convert 16:16 pointers to a linear address in your head!)

OS/2 2.0 was a 32-bit operating system. 1.0 was not. Many of the APIs from 1.0 survived into the 32-bit transition, but they never got converted to 32-bit APIs themselves, even in the final 4.5 releases, years later. I suppose this was because IBM wanted developers to write Presentation Manager (the new GUI/window functions) programs instead of VIO (text mode/command line) programs. APIs for things like file management and thread primitives continued to exist as 16-bit APIs while also adding new 32-bit entry points for the same functions, but things that dealt with text-based programs (Vio* for writing to a console, Kbd* for keyboard input, etc) never got 32-bit equivalents.

(The mythical PowerPC port of OS/2 fixed this, making these APIs 32-bit clean, apparently, but these features never returned to the Intel port.)

Now one could definitely write a 32-bit command line program on OS/2, but if one needed to call these older system functions, one had to call into 16-bit code. This is done through the magic of thunking and some wizardry with memory segments. Imagine my surprise when Watcom C would print out a page of command line information, then wait for a keypress with KbdCharIn(). To do this, it would jump into a 16-bit code segment, which would save off some registers and call the never-updated-for-32-bit API call, restore some registers afterwards and jump back to 32-bit land with the results.

First problem: I don’t have a 16-bit code segment! Second: I don’t have a way to generate 16-bit code with GCC.

After some googling, I found there’s a Linux-specific system call to help with this, which Wine and dosemu use to support Win16 and MS-DOS. It’s called modify_ldt(), and it lets you map pages from your 32-bit linear address space to a 16-bit selector. LDTs are a feature of the x86 processor; you can read up on them on Wikipedia. Operating systems rely heavily on them, and userspace code doesn't unless it's doing wacky things like emulating ancient OSes.

Okay, now I can create 16-bit segments, so what segments do I create?

If you’re OS/2 2.0, the answer is: all of them. OS/2 would “tile” the entire address space, so any 32-bit pointer you might have automatically exists in some 16-bit segment, and you could do some simple math on the pointer itself to determine it (shift the top 16 bits left by 3 and bitwise OR with 7 to get the selector, bottom 16 bits are your offset).

The problem with this approach is that you only have 8192 possible selectors (you only get to use 13 of the bits!), times 64 kilobytes in each segment, which means 32-bit OS/2 apps can only access the first 512 megabytes of their address space in this system. In IBM’s defense, if your machine had more than 4 megabytes of total physical RAM at the time, that was a powerhouse computer.

Later versions of OS/2 stopped tiling like this, and offered an API to do the conversion for you (“DosFlatToSel”), but lots of programs rely on tiling and do the pointer math themselves without using the API. In hopes that this matches what OS/2 ended up doing, I tile the main thread’s stack (since this is probably where most data you want to reach in 16-bit code lives; temporary local variables for an API call) and any memory segments in the LX module that were marked as 16-bit. Non-tiled LDTs are then allocated when a DosFlatToSel() call is made, using cached selectors from previous allocations and tiles when possible. So far, it’s working out okay and we aren’t limited to 512 megabytes of memory, under the assumption most 16-bit calls happen at a handful of locations and the things that assume the pointer math works only try it in unsurprising ways. Knock on wood.

Now I probably have the address space politics worked out (minus Thread Local Storage, which does something bonkers I'll explain some other time), but I still can’t generate 16-bit code with GCC. The solution is not to. Just because the OS/2 app wants to call a function in a 16-bit code segment doesn’t mean we need the function to be 16-bit code. All we need is a little bit of bridge code for the OS/2 app to land in that moves us into our native implementation. As an added benefit, it means these APIs are usable to OS/2 apps recompiled from source as native Linux apps with no 16-bitness at all; think Wine vs Winelib.

So you get some macro salsa to define functions:

As you can see, this writes the bytes of the 16-bit x86 instructions directly to a memory buffer, instead of trying to get GCC to assemble them. The macro is used once for each “16-bit” API we export. The code got assembled with Netwide Assembler, then disassembled with ndisasm and pushed through a perl script to produce this code. 

And then, like doing skateboard tricks, there’s nothing left to do but say “watch this,” and see if you pull off something awesome or just crash.

This is the sort of inefficiency and trouble that drives engineers mad, but here’s how we kept the CPU happy through this process:

  • OS/2 app saves some things and calls into a 16-bit code segment that likely exists just to call into a 16-bit API.

  • 16-bit code segment saves some things and calls into 2ine’s 16-bit bridge code.

  • 2ine’s 16-bit bridge saves some things and jumps directly back to 2ine’s 32-bit bridge code.

  • 2ine’s 32-bit bridge code saves some stuff and calls the native implementation of whatever API.

  • Whatever API is implemented in C as a real piece of Linux code in an ELF shared library, talking directly to various Linux interfaces.

  • Native implementation returns to 32-bit bridge code.

  • 32-bit bridge code restores things, jumps back to 16-bit bridge code.

  • 16-bit bridge code restores things, returns to OS/2 app’s 16-bit code segment.

  • OS/2 app’s 16-bit code segment restores things, returns to OS/2 app’s 32-bit code segment.

  • OS/2 app’s 32-bit code carries on like it made a simple function call.

Whew!

One more piece of magic for 16-bit support: when writing x86 Linux code, you probably don’t think about your 32-bit linear address space as having a “code segment,” but it does. It’s not guaranteed to be any specific value, but is currently hardcoded (0x23 if you’re running a 32-bit app on an amd64 kernel, 0x73 if you’re on a real 32-bit kernel). You have one because code segments are how x86 processors keep track of code privilege level; the kernel runs in a different segment with higher privileges, which lets it have instructions that your userland code can’t use.

OS/2 also has a hardcoded code segment for 32-bit code, too. It’s 0x5B. I spent a week trying to get IBM’s command line FTP.EXE client to not crash because it wants to read passwords from the keyboard without echoing them to the screen, and that needs a 16-bit API, even though all the rest of the input is just a 32-bit DosRead() on stdin. After much head scratching about why it was trying to jump back from 16-bit land to a totally bogus 32-bit code segment, I found an ancient IBM CourseWare document on the Internet Archive that explained this. Since the OS/2 kernel hardcoded code segment 0x5B, IBM’s CSet/2 compiler hardcoded it into a bunch of apps, too, to get back to 32-bit land. EMX (which was basically an OS/2 port of GCC) was smart enough to save off the CS register and not do this, avoiding this problem.

2ine can’t map code segment 0x5B; it’s a GDT entry, not an LDT entry, which you can’t really mess with in userland, so the best we can do is sniff through 16-bit code segments we load from an OS/2 binary for far jumps to that segment and fix them up. That code is dirty-nasty-gross, though.

With that fix in place, and enough implementation of TCPIP32.dll, we were really flying now.

There were other fixes to be made, and features to implement, but now that most of the 16-bit drama was handled, and most of the “fun” problems with command line apps were done, it was time to move on to Presentation Manager apps, which generally don’t make 16-bit calls at all. The problem here isn’t binary compatibility but that Presentation Manager is a massive API surface that I’d have to write from scratch.

My pep-talk sounds a lot like this, echoing in my dark office at 2am: “this had to run on 386 machines with 2 megabytes of RAM and was built with caveman-primitive development tools. It couldn’t be that complex.” Or, more succinctly: “Simplicity encourages speed.” Often when trying to imagine how some system was implemented, in 1992, I tried to imagine the cleanest, easiest way to write it, and prayed that’s actually how it went down at Big Blue, too. This is straight-up cockeyed optimism on my part.

I tried to write the simplest PM program possible. I can’t even call it a “hello world” app because rendering text is an extra layer of complexity. Instead, I went for something that creates a 100x100 pixel window, at screen coordinate (100, 100). It paints it white when it needs painting, and if you click on it, it quits the program. Here’s the code:

The function names are different, but this should look familiar to you if you’ve ever done any Windows programming at the win32 (or win16) API level. It quickly becomes apparent that Windows and OS/2 started with the same API and drifted apart as their parents slid into divorce.

That program looks like this, running on OS/2:

(“Netscape Communicator” is not the most obsolete web browser installed on this system, believe it or not.)

See that white square? That’s our app! If you’re wondering why it’s so low on the desktop, OS/2’s coordinate system puts (0, 0) at the lower left corner, so that’s 100 pixels from the bottom, not the top.

To get this working, I just need to implement 14 functions! Unfortunately, some of these functions are hella complex. WinCreateWindow(), for example, is basically the core of the entire paradigm, so there are tons of arguments that do different things, flags that alter behavior, etc. WinGetMsg() needs to produce hundreds of possible window system events, and WinDefWindowProc() needs to recognize all of them.

Don’t panic: aim for simplicity first. We don’t need all those events right now, and even if we did, WinDefWindowProc() responds to almost all of them by just returning zero.

I’ve been collecting up books on OS/2 programming from the web (the Internet Archive has PDFs of so many programming books that are otherwise cluttering landfills now) and physical copies from Amazon where I can. I’m looking for quirks and tiny implementation details of these APIs. But, unexpected to me, the best resource turned out to be IBM’s SDK documentation.

Most things are covered, not just in basic function call info, but subtle interactions, window messages it generates, etc. I’m not sure why I assumed this would be lacking, but it seems to be the best route towards reimplementation.

So let’s reimplement! I knew immediately that I didn’t want to talk to X11 directly, because X11 sucks in general to work with and with luck it’s a dying system anyhow. Since I am (ahem) familiar with SDL, and Epic Games and I had spent so much time working with SDL as the backend of a GUI toolkit, I figured I’d start there. In 2ine, top-level windows (things with the desktop as their parent) generate an SDL window. Using the terminology of Java’s Swing framework, this is a “heavyweight” window. Child windows slice that heavyweight window into chunks, and since they don’t make an operating system window of their own, but just maintain some logical state about themselves, they’re “lightweight” windows.

The heavyweight window also does something else interesting: it creates an SDL_Renderer and an SDL_Texture to draw to. This lets us render drawing primitives to the window with OpenGL and keep a backing store of any rendering (so no need to send paint messages just because a window got dragged out of the way). Other benefits: clipping is basically free when a child window is drawing, and we can scale up apps that thought 800x600 was an impossibly massive screen resolution.

So about 1600 lines of C later, I had enough of the Presentation Manager implemented to get our little white square popping up on a Linux desktop, produced by an OS/2 binary.

Going further on this is non-trivial, though. The effort involved is probably about equivalent to the man-hours the Wine project needed to work before you could reasonably assume a Win16 program would function correctly. Trust me: there’s a lot of work to be done.

I do hope to do this work at some point, but I’m probably at the point where continuing on this is trying everyone’s patience, so I’m going to move back to something more practical next (and something impractical, like a video game). Still, it feels to good to have set out to climb a mountain, as ridiculous as that mountain might be to climb, and stand on a hill some distance above the ground. I will set camp here for now and examine some other mountains for a while.

Get more from Ryan C. Gordon

Join to get free updates and posts delivered right to your inbox.

Get free updates and posts delivered right to your inbox.


44 comments

·

I have a confession to make: it turns out that it only takes a couple of months of on-off work to get Age of Empires running on top of SDL2 in a similar manner.

AUTHOR

·

Oh David, shine on, you crazy diamond. :)

·

Only, if you don't know it: OSNews wrote about this project <a href="http://www.osnews.com/comments/30205" rel="nofollow noopener" target="_blank">http://www.osnews.com/comments/30205</a>

·

·

Linus and I had quite a few arguments back in the day about supporting a binary driver mechanism for Linux as I believed binary drivers like what we had developed was the right approach to keeping things running and avoiding what I call code rot (code that continues to compile but no longer works as stuff around it changes). I still think he was wrong :)

·

Love articles like this one. I remember EMX as basically Cygwin for OS/2. Not only had GCC, but also a port of XFree86 and IceWM ported to it, and quite a few utilities. As for the most obsolete web browser, I imagine you're talking about the no-frames-having WebExplorer. Back when I first made my home page, I actually built it specifically for that web browser. (There was one other web browser that IBM released written in Smalltalk, but it wasn't much better than WebEx.)

·

·

I remember my microsoft C5.1 compiler had an OS2 1.3 target... Due to bugs in that compiler I ended up using parts of the gnu compiler to preprocess my source. Still needed the 5.1 compiler, since the gnu compiler was not able to create simple 16 bits dos executables (as far as I know, and internet did not really exist at that moment.).

AUTHOR

·

Microsoft C 5.1 was sort of legendary at the time, because it could produce OS/2 device drivers (which, to my horror even in 1993, had to be 16-bit code. IBM never fixed this limitation.) GCC never had 16-bit support; EMX would use assembly code when it have to dive into a 16-bit code segment.

·

I recently got a bit into retro stuff, and can indeed be a fun experience: <a href="https://youtu.be/afwIZDtrRj4?t=14m15s" rel="nofollow noopener" target="_blank">https://youtu.be/afwIZDtrRj4?t=14m15s</a>

AUTHOR

·

That video was awesome. I was on the edge of my seat waiting to see if the fsck was going to survive. :)

·

I have never run OS/2 - I got into PCs, as opposed to home computers, just as OS/2 was on the rocks - and have literally never even seen it outside of a book or a YouTube video, except maybe once at a bank; but I am seriously impressed by the work and by the writeup.

·

AUTHOR

·

Hmm, catching this in the signal handler is a good fallback. Good idea!

·

Great stuff! (I write this while I'm nearly done phasing out the last productive OS/2 [eComStation] machine I have running.) Now let's bring the Workplace Shell to Linux, as there is nothing yet that would really compare! ;-)

·

Project: 2ine

Creator profile picture

Ryan C. Gordon

Jan 21, 2018

Project: 2ine

Jan 21, 2018

You have no idea how much effort went into getting this stupid white square on the screen.

I thought I’d explain what all this is about in more detail, since I mentioned it in the December wrapup. This is a long and technical post about weird low-level stuff and you can totally skip this if you don't care about weird low-level stuff.

I've been building a project called “2ine,” pronounced “twine,” which emulates OS/2 binaries in the same way that Wine emulates Windows binaries. I named it as such to continue the fine tradition of programmers producing good code and bad product names.

Any introduction to computers starts with a whole lot of lies. It’s complexity all the way down to the bottom, and you don’t need to know it all when you just want to write a program that prints out “hello world.” This is a blessing and a curse, but mostly a survival mechanism; if you knew everything you were about to stumble into, you’d never start. In this way, I stumbled into writing an OS/2 emulator.

In 2003, when I was working on what would become Unreal Tournament 2004, I was trying to get the software renderer working on Linux. The market was rotten with fast CPUs and lousy GPUs, and OpenGL support on Linux was spotty back then in any case, so having a software fallback was super useful. For a software renderer, UT2004 uses RAD Game Tools’ Pixomatic, which is one of those amazing pieces of code that seems to defy physics.

Pixomatic is itself a technical marvel of lowlevel x86 sorcery; Michael Abrash wrote about it in detail here and here and here. But of all the things it can do, all you need to know for now is that the “Linux port” of Pixomatic consists of a Windows .dll and a small piece of C code to load it.

I’m not kidding.

All Pixomatic cares about is lighting up pixels in a memory buffer and only needs the win32 API to manage pages of memory. So this C code would get the Windows DLL loaded, provide a few function pointers that would call mmap() when Pixomatic thought it was calling VirtualAlloc() and such, and then everything just sort of works on Linux. At the end of our trip to Win32 Land, UT2004 would use SDL to flip that memory buffer to the screen as if nothing unusual happened here at all. It’s wild.

What surprised me about this wasn’t the approach—although that was unexpected too—but the simplicity of the C code that loads the DLL. 618 lines of code! Is this all a DLL needs?

The answer, as always, is yes and no.

Shared libraries on most operating systems work like this: you get a table of information of where to place data in memory and the system loader blasts it out there. There’s more to the effort than that, but the juiciest tasks are:

  • Load a data block into memory.  It might be code, or initial data for global variables, or some constant thing like string literals.

  • Fix up specific bytes in memory with real addresses of things it needs. When your program calls printf(), this puts the actual address of printf into memory for the call instruction. There are lots of different kinds of fixups for different needs.

  • Keep track of symbols exported from the library so you can fix up other things that need this library later.

And that’s it! In most cases, programs you run use the same format as shared libraries with minor differences. (Linux users! Did you ever try to run libc.so.6 like it was a program instead of a shared library? I’ll wait while you try it.)

Pixomatic’s freakishly simple DLL loader got stuck in my head. Once you do the loading and fixing up, this isn’t really a win32 program at all any more. It’s code in a Linux process. Could we load other things like this? You bet.

Ten years later, I stumbled into writing an ELF loader for Linux, because dlopen() needs a filename and I wanted to load shared libraries from a memory buffer. ELF is more complicated than the Windows format, and this code is more robust in general, getting us to about 1200 lines of code, in a project I called MojoELF.

Once I had built that, I thought: if I load an ELF binary on a Mac, it’s no longer a Linux program. It’s code in a Mac process. And since the Mac has all that POSIX goodness and a quality SDL port, if we fix up the POSIX calls to function addresses that bridge differences in data layout, and fix up calls into SDL to the real Mac SDL library, well…maybe we can play a Linux build of Quake 3.

None of this is novel at all: this is roughly how Wine has always done things. They just had to work harder to deal with system calls into an OS that’s completely alien on Linux.

But in any case, this is an idea that works.

I don’t know what prompted me to do an OS/2 loader in the first place, but it was probably a straightforward case of nerd-sniping. I keep a list of interesting-waste-of-time projects and sometimes when my mind wanders, I foolishly look at this list and my productivity drops to zero until I can build some ridiculous thing that caught my eye.

Documents that explain the OS/2 file format (the binaries are called Linear Executable format, LX for short) are easy to find on the Internet, so why not try? It should send up alarms when I tell myself “Why not spend an hour and see how far you get?” There should be a Degrassi High School episode about this exact scenario, to serve as a warning to future programmers during their formative years!

Like that Pixomatic C code, loading an LX binary into memory is mostly easy. But even if you discount the ugly corner cases, you still have to implement the OS/2 API for the loader to be useful.

The first version of my OS/2 loader ran exactly one program, a Hello World thing, written in assembly because loading a C runtime was too complex at this point.

(Strictly speaking, the true first version was probably a program that set EAX to 42 and returned, just to see if the process exit code was also 42 when 2ine terminated, demonstrating we could bounce into OS/2 Land and back safely.)

OS/2, like Windows, doesn’t offer a single C runtime like Linux and macOS do, and all of them do some complicated tap dancing with system calls before main() even runs, and I didn’t want to mess with that yet. Eventually, I got to filling in some basic Unix-like bits, with the naming OS/2 uses: DosOpen() to open files, DosWrite() to write to a file handle, etc. The nice thing about DosWrite() is that file handles 0, 1, and 2 match up with Unix stdin, stdout and stderr; this helped get a bunch of OS/2 command line programs running without added drama, and you can even pipe them through to other Linux processes.

System APIs are written in C, as native Linux code. When the OS/2 module reports that it needs the (system-provided) DOSCALLS.DLL, 2ine dlopens its own libdoscalls.so with these reimplemented APIs, and uses the native Linux entry points to fix up the OS/2 module. Now when the app calls DosWrite, the CPU calls directly into a Linux ELF shared library where this function was reimplemented, not knowing the difference. The 32-bit calling conventions happen to match up well enough between the two platforms that it just happens to work.

An OS/2 app can run under 2ine using a mix of native Linux libraries that reimplement system APIs and real OS/2 DLLs, so long as those DLLs don't do weird things or depend on DLLs that do weird things (what qualifies as a "weird thing" could fill a whole other blog post, though). Right now, some things will run on 2ine if they have access to a handful of IBM's system DLLs with native libraries spackling in the cracks, that otherwise would fail to operate.

With some effort, and with enough APIs filled in, the OS/2 port of GCC that I used in high school (2.8.1! It doesn’t even support Pentium 1 instructions!) started running, since it doesn’t need much more than stdio and a way to launch child processes, allowing me to build OS/2 programs on Linux, using the compiler under my emulator. Now we’re getting somewhere!

And then I thought, hey, let’s get Watcom C working too, and here my troubles began. Specifically, my troubles began with the compiler’s help screen saying “press return to continue.”

(That’s right, you can debug OS/2 binaries on Linux with GDB by running them under 2ine, as long as you don’t expect debug symbols or source code views! Also, as long as you can convert 16:16 pointers to a linear address in your head!)

OS/2 2.0 was a 32-bit operating system. 1.0 was not. Many of the APIs from 1.0 survived into the 32-bit transition, but they never got converted to 32-bit APIs themselves, even in the final 4.5 releases, years later. I suppose this was because IBM wanted developers to write Presentation Manager (the new GUI/window functions) programs instead of VIO (text mode/command line) programs. APIs for things like file management and thread primitives continued to exist as 16-bit APIs while also adding new 32-bit entry points for the same functions, but things that dealt with text-based programs (Vio* for writing to a console, Kbd* for keyboard input, etc) never got 32-bit equivalents.

(The mythical PowerPC port of OS/2 fixed this, making these APIs 32-bit clean, apparently, but these features never returned to the Intel port.)

Now one could definitely write a 32-bit command line program on OS/2, but if one needed to call these older system functions, one had to call into 16-bit code. This is done through the magic of thunking and some wizardry with memory segments. Imagine my surprise when Watcom C would print out a page of command line information, then wait for a keypress with KbdCharIn(). To do this, it would jump into a 16-bit code segment, which would save off some registers and call the never-updated-for-32-bit API call, restore some registers afterwards and jump back to 32-bit land with the results.

First problem: I don’t have a 16-bit code segment! Second: I don’t have a way to generate 16-bit code with GCC.

After some googling, I found there’s a Linux-specific system call to help with this, which Wine and dosemu use to support Win16 and MS-DOS. It’s called modify_ldt(), and it lets you map pages from your 32-bit linear address space to a 16-bit selector. LDTs are a feature of the x86 processor; you can read up on them on Wikipedia. Operating systems rely heavily on them, and userspace code doesn't unless it's doing wacky things like emulating ancient OSes.

Okay, now I can create 16-bit segments, so what segments do I create?

If you’re OS/2 2.0, the answer is: all of them. OS/2 would “tile” the entire address space, so any 32-bit pointer you might have automatically exists in some 16-bit segment, and you could do some simple math on the pointer itself to determine it (shift the top 16 bits left by 3 and bitwise OR with 7 to get the selector, bottom 16 bits are your offset).

The problem with this approach is that you only have 8192 possible selectors (you only get to use 13 of the bits!), times 64 kilobytes in each segment, which means 32-bit OS/2 apps can only access the first 512 megabytes of their address space in this system. In IBM’s defense, if your machine had more than 4 megabytes of total physical RAM at the time, that was a powerhouse computer.

Later versions of OS/2 stopped tiling like this, and offered an API to do the conversion for you (“DosFlatToSel”), but lots of programs rely on tiling and do the pointer math themselves without using the API. In hopes that this matches what OS/2 ended up doing, I tile the main thread’s stack (since this is probably where most data you want to reach in 16-bit code lives; temporary local variables for an API call) and any memory segments in the LX module that were marked as 16-bit. Non-tiled LDTs are then allocated when a DosFlatToSel() call is made, using cached selectors from previous allocations and tiles when possible. So far, it’s working out okay and we aren’t limited to 512 megabytes of memory, under the assumption most 16-bit calls happen at a handful of locations and the things that assume the pointer math works only try it in unsurprising ways. Knock on wood.

Now I probably have the address space politics worked out (minus Thread Local Storage, which does something bonkers I'll explain some other time), but I still can’t generate 16-bit code with GCC. The solution is not to. Just because the OS/2 app wants to call a function in a 16-bit code segment doesn’t mean we need the function to be 16-bit code. All we need is a little bit of bridge code for the OS/2 app to land in that moves us into our native implementation. As an added benefit, it means these APIs are usable to OS/2 apps recompiled from source as native Linux apps with no 16-bitness at all; think Wine vs Winelib.

So you get some macro salsa to define functions:

As you can see, this writes the bytes of the 16-bit x86 instructions directly to a memory buffer, instead of trying to get GCC to assemble them. The macro is used once for each “16-bit” API we export. The code got assembled with Netwide Assembler, then disassembled with ndisasm and pushed through a perl script to produce this code. 

And then, like doing skateboard tricks, there’s nothing left to do but say “watch this,” and see if you pull off something awesome or just crash.

This is the sort of inefficiency and trouble that drives engineers mad, but here’s how we kept the CPU happy through this process:

  • OS/2 app saves some things and calls into a 16-bit code segment that likely exists just to call into a 16-bit API.

  • 16-bit code segment saves some things and calls into 2ine’s 16-bit bridge code.

  • 2ine’s 16-bit bridge saves some things and jumps directly back to 2ine’s 32-bit bridge code.

  • 2ine’s 32-bit bridge code saves some stuff and calls the native implementation of whatever API.

  • Whatever API is implemented in C as a real piece of Linux code in an ELF shared library, talking directly to various Linux interfaces.

  • Native implementation returns to 32-bit bridge code.

  • 32-bit bridge code restores things, jumps back to 16-bit bridge code.

  • 16-bit bridge code restores things, returns to OS/2 app’s 16-bit code segment.

  • OS/2 app’s 16-bit code segment restores things, returns to OS/2 app’s 32-bit code segment.

  • OS/2 app’s 32-bit code carries on like it made a simple function call.

Whew!

One more piece of magic for 16-bit support: when writing x86 Linux code, you probably don’t think about your 32-bit linear address space as having a “code segment,” but it does. It’s not guaranteed to be any specific value, but is currently hardcoded (0x23 if you’re running a 32-bit app on an amd64 kernel, 0x73 if you’re on a real 32-bit kernel). You have one because code segments are how x86 processors keep track of code privilege level; the kernel runs in a different segment with higher privileges, which lets it have instructions that your userland code can’t use.

OS/2 also has a hardcoded code segment for 32-bit code, too. It’s 0x5B. I spent a week trying to get IBM’s command line FTP.EXE client to not crash because it wants to read passwords from the keyboard without echoing them to the screen, and that needs a 16-bit API, even though all the rest of the input is just a 32-bit DosRead() on stdin. After much head scratching about why it was trying to jump back from 16-bit land to a totally bogus 32-bit code segment, I found an ancient IBM CourseWare document on the Internet Archive that explained this. Since the OS/2 kernel hardcoded code segment 0x5B, IBM’s CSet/2 compiler hardcoded it into a bunch of apps, too, to get back to 32-bit land. EMX (which was basically an OS/2 port of GCC) was smart enough to save off the CS register and not do this, avoiding this problem.

2ine can’t map code segment 0x5B; it’s a GDT entry, not an LDT entry, which you can’t really mess with in userland, so the best we can do is sniff through 16-bit code segments we load from an OS/2 binary for far jumps to that segment and fix them up. That code is dirty-nasty-gross, though.

With that fix in place, and enough implementation of TCPIP32.dll, we were really flying now.

There were other fixes to be made, and features to implement, but now that most of the 16-bit drama was handled, and most of the “fun” problems with command line apps were done, it was time to move on to Presentation Manager apps, which generally don’t make 16-bit calls at all. The problem here isn’t binary compatibility but that Presentation Manager is a massive API surface that I’d have to write from scratch.

My pep-talk sounds a lot like this, echoing in my dark office at 2am: “this had to run on 386 machines with 2 megabytes of RAM and was built with caveman-primitive development tools. It couldn’t be that complex.” Or, more succinctly: “Simplicity encourages speed.” Often when trying to imagine how some system was implemented, in 1992, I tried to imagine the cleanest, easiest way to write it, and prayed that’s actually how it went down at Big Blue, too. This is straight-up cockeyed optimism on my part.

I tried to write the simplest PM program possible. I can’t even call it a “hello world” app because rendering text is an extra layer of complexity. Instead, I went for something that creates a 100x100 pixel window, at screen coordinate (100, 100). It paints it white when it needs painting, and if you click on it, it quits the program. Here’s the code:

The function names are different, but this should look familiar to you if you’ve ever done any Windows programming at the win32 (or win16) API level. It quickly becomes apparent that Windows and OS/2 started with the same API and drifted apart as their parents slid into divorce.

That program looks like this, running on OS/2:

(“Netscape Communicator” is not the most obsolete web browser installed on this system, believe it or not.)

See that white square? That’s our app! If you’re wondering why it’s so low on the desktop, OS/2’s coordinate system puts (0, 0) at the lower left corner, so that’s 100 pixels from the bottom, not the top.

To get this working, I just need to implement 14 functions! Unfortunately, some of these functions are hella complex. WinCreateWindow(), for example, is basically the core of the entire paradigm, so there are tons of arguments that do different things, flags that alter behavior, etc. WinGetMsg() needs to produce hundreds of possible window system events, and WinDefWindowProc() needs to recognize all of them.

Don’t panic: aim for simplicity first. We don’t need all those events right now, and even if we did, WinDefWindowProc() responds to almost all of them by just returning zero.

I’ve been collecting up books on OS/2 programming from the web (the Internet Archive has PDFs of so many programming books that are otherwise cluttering landfills now) and physical copies from Amazon where I can. I’m looking for quirks and tiny implementation details of these APIs. But, unexpected to me, the best resource turned out to be IBM’s SDK documentation.

Most things are covered, not just in basic function call info, but subtle interactions, window messages it generates, etc. I’m not sure why I assumed this would be lacking, but it seems to be the best route towards reimplementation.

So let’s reimplement! I knew immediately that I didn’t want to talk to X11 directly, because X11 sucks in general to work with and with luck it’s a dying system anyhow. Since I am (ahem) familiar with SDL, and Epic Games and I had spent so much time working with SDL as the backend of a GUI toolkit, I figured I’d start there. In 2ine, top-level windows (things with the desktop as their parent) generate an SDL window. Using the terminology of Java’s Swing framework, this is a “heavyweight” window. Child windows slice that heavyweight window into chunks, and since they don’t make an operating system window of their own, but just maintain some logical state about themselves, they’re “lightweight” windows.

The heavyweight window also does something else interesting: it creates an SDL_Renderer and an SDL_Texture to draw to. This lets us render drawing primitives to the window with OpenGL and keep a backing store of any rendering (so no need to send paint messages just because a window got dragged out of the way). Other benefits: clipping is basically free when a child window is drawing, and we can scale up apps that thought 800x600 was an impossibly massive screen resolution.

So about 1600 lines of C later, I had enough of the Presentation Manager implemented to get our little white square popping up on a Linux desktop, produced by an OS/2 binary.

Going further on this is non-trivial, though. The effort involved is probably about equivalent to the man-hours the Wine project needed to work before you could reasonably assume a Win16 program would function correctly. Trust me: there’s a lot of work to be done.

I do hope to do this work at some point, but I’m probably at the point where continuing on this is trying everyone’s patience, so I’m going to move back to something more practical next (and something impractical, like a video game). Still, it feels to good to have set out to climb a mountain, as ridiculous as that mountain might be to climb, and stand on a hill some distance above the ground. I will set camp here for now and examine some other mountains for a while.

Get more from Ryan C. Gordon

Join to get free updates and posts delivered right to your inbox.

Get free updates and posts delivered right to your inbox.


44 comments

·

I have a confession to make: it turns out that it only takes a couple of months of on-off work to get Age of Empires running on top of SDL2 in a similar manner.

AUTHOR

·

Oh David, shine on, you crazy diamond. :)

·

Only, if you don't know it: OSNews wrote about this project <a href="http://www.osnews.com/comments/30205" rel="nofollow noopener" target="_blank">http://www.osnews.com/comments/30205</a>

·

That’s awesome! We did similar stuff years before at SciTech for our Binary Portable DLL project we used to load and run graphics device drivers we developed across multiple OSes. We built them into PE executable libraries using the Watcom (and later GCC compiler for 64-bit support) compiler an loaded the core on any OS using stubs to call back into a portable library of OS support functions. It’s the tech IBM licensed and used for years to keep OS/2 alive for those banks still using it until about 2007! Alas none of the graphics card companies would let us open source the device driver code so it never saw the light of day but the front end stuff and loader code was all Open Sourced as GPL years ago. I threw it up on git hub a while ago and the pe loader code is still there :) <a href="https://github.com/kendallb/scitech-mgl/blob/master/src/common/peloader.c" rel="nofollow noopener" target="_blank">https://github.com/kendallb/scitech-mgl/blob/master/src/common/peloader.c</a>

·

Linus and I had quite a few arguments back in the day about supporting a binary driver mechanism for Linux as I believed binary drivers like what we had developed was the right approach to keeping things running and avoiding what I call code rot (code that continues to compile but no longer works as stuff around it changes). I still think he was wrong :)

·

Love articles like this one. I remember EMX as basically Cygwin for OS/2. Not only had GCC, but also a port of XFree86 and IceWM ported to it, and quite a few utilities. As for the most obsolete web browser, I imagine you're talking about the no-frames-having WebExplorer. Back when I first made my home page, I actually built it specifically for that web browser. (There was one other web browser that IBM released written in Smalltalk, but it wasn't much better than WebEx.)

·

Now there is kLIBC, the successor for EMX. There are yum/rpm ported to it, and now there is a number of ports, packaged into RPM's, with a repository on Netlabs. There is Qt4 port, and more than 200 Qt apps are ported. I still use XFree86 (it is still EMX-based and the last version 4.4.0 is released in 2004). There is an unfinished X.org port, but unfortunately, nobody works on it now. XFree86 indeed has a lot of programs ported on it. I used Enlightenment and WindowMaker WM's, but I use BlackBox mostly. XFree86 had some web browsers ported on it, too bad that too little people used it, and they were abandoned. I still use XFree86 to run XSane for my scanner, and some remote apps from Linux system. As I remember, EMX appeared much earlier that Cygwin. According the web browsers, WebExplorer was so long time ago, then it was replaced with Netscape and Mozilla. Now there are monsters like Firefox and Seamonkey, and a number of browsers based on Qt Webkit.

·

I remember my microsoft C5.1 compiler had an OS2 1.3 target... Due to bugs in that compiler I ended up using parts of the gnu compiler to preprocess my source. Still needed the 5.1 compiler, since the gnu compiler was not able to create simple 16 bits dos executables (as far as I know, and internet did not really exist at that moment.).

AUTHOR

·

Microsoft C 5.1 was sort of legendary at the time, because it could produce OS/2 device drivers (which, to my horror even in 1993, had to be 16-bit code. IBM never fixed this limitation.) GCC never had 16-bit support; EMX would use assembly code when it have to dive into a 16-bit code segment.

·

I recently got a bit into retro stuff, and can indeed be a fun experience: <a href="https://youtu.be/afwIZDtrRj4?t=14m15s" rel="nofollow noopener" target="_blank">https://youtu.be/afwIZDtrRj4?t=14m15s</a>

AUTHOR

·

That video was awesome. I was on the edge of my seat waiting to see if the fsck was going to survive. :)

·

I have never run OS/2 - I got into PCs, as opposed to home computers, just as OS/2 was on the rocks - and have literally never even seen it outside of a book or a YouTube video, except maybe once at a bank; but I am seriously impressed by the work and by the writeup.

·

Hi Ryan, nice article! It seems like I am always doing the things like that, so let me share a few thoughts. With GDT problem, there is a trick to get it to work on linux. You need to install the SIGSEGV handler and watch an exception error code. It will match the GDT selector that was being attempted to use. Then you create an LDT segment with the same properties as the GDT segment in question, and load its selector to the proper sigcontext's segment register member. Then you return from the SIGSEGV handler, and the code keeps working as if nothing happened (you need to advance eip to prevent it from re-triggering - this is the tricky part but you can use the lib like udis86 or find the decode_segreg() func in dosemu2 sources). If SS segment is altered, there are a few tricky flags to use, like UC_SIGCONTEXT_SS. You can look them up in dosemu2 sources to see the usage. Also I believe you can use gas to generate 16bit asm. gas understands ".code16" directive. We put the global labels around such code, then we can find it from the 32bit code and memcpy() to the 16bit LDT segment. Note that modify_ldt() is available on x86_64 and works perfectly. Some distros are disabling it, but you can re-build the kernel yourself. However, an alternative approach that dosemu2 now employs, is to run the alien code inside KVM. With quite a small bit of work, you can make anything you want with KVM, even directly modify GDT and all the rest.

AUTHOR

·

Hmm, catching this in the signal handler is a good fallback. Good idea!

·

Great stuff! (I write this while I'm nearly done phasing out the last productive OS/2 [eComStation] machine I have running.) Now let's bring the Workplace Shell to Linux, as there is nothing yet that would really compare! ;-)

·

Please Keep Going with this Project !!! To everybody that has interest on OS/2 development we are trying to consolidate all OS/2 development knowledge possible on the EDM/2 wiki - <a href="http://www.edm2.com" rel="nofollow noopener" target="_blank">http://www.edm2.com</a> Other open source projects that can work as reuse/documentation/inspiration/complement are: 1) CPI API - OS2Linux: <a href="https://github.com/OS2World/LINUX-SYSTEM-OS2Linux" rel="nofollow noopener" target="_blank">https://github.com/OS2World/LINUX-SYSTEM-OS2Linux</a> 2) FreePM - <a href="http://frepm.sourceforge.net/" rel="nofollow noopener" target="_blank">http://frepm.sourceforge.net/</a> 3) OSFree - A mixture of several OSS project to try to clone OS/2 under a L4/Fiasco Kernel. <a href="http://www.osfree.org/doku/en:credits" rel="nofollow noopener" target="_blank">http://www.osfree.org/doku/en:credits</a> Good Luck !!!

Read more from your favorite creators.

Project: SDL3_net

I've hated SDL_net for over two decades now. It was something that was built, and largely abandoned, at Loki. I've never really cared for th

Oct 1, 2023

18

1

Project: PhysicsFS for Nintendo Switch

This was a quicky project--only a few hours of work--but PhysicsFS now works on the Nintendo Switch! Same rules as SDL apply: if you are a r

Mar 9, 2018

7

1

Credits for a new video

Locked

Credits for a new video

June 12

3

1

Project: DirkSimple

I have been in love with Dragon's Lair since six-year-old me wandered into a K-Mart that had an arcade room. It was like nothing I had ever

Jan 21, 2023

15

2

Project: IcculusGopher

Let this be a lesson to project managers everywhere: during long compiles, programmers' minds _wander_. I was reading a random blog post ar

Sep 10, 2017

5

8

I can't believe how many people have shown up in the last 24 hours! There's a ton of really good suggestions in the Patreon thread, in my Twitter mentions, my inbox, on gamingonlinux.com, on Reddit... I'm starting to build my _own_ wishlist from all these wishes I'm reading, and even made an initial pitch to a developer last night. We'll see where it goes. There's still so many responses from all of you to sort through, developers to contact, and work to be done! I'm really overwhelmed by all this. Not just the money--which I genuinely appreciate!--but all the stories of games that are so loved and are _this close_ to coming to Linux. We're all going to move this forward a little. We're all gonna make a few dreams come true.

Y'all are amazing. 😍

I can't believe how many people have shown up in the last 24 hours! There's a ton of really good suggestions in the Patreon thread, in my Tw

Aug 23, 2017

23

11

Project: Cliff Hanger

(also not the One More Thing thing. I know, I know.) Can you believe this is how a whole generation of American children got introduced to L

Feb 21, 2023

8

4

A few quick notes: Turok 2 is no longer in a beta branch on Steam; if you own it on Steam, it'll download like any other game for Linux and macOS now without messing with branches. Forsaken Remastered was just updated with Vulkan support! If you're on Linux, you're probably hitting 60fps with the existing OpenGL renderer, but it's good to be future proof. If you're on a Mac, though, you definitely want to switch. On my MacBook, the framerate goes from around 15 to a solid 60! On macOS, Vulkan support is supplied by MoltenVK , which we now ship with the game. It should work on any Mac that supports Apple's Metal API , which MoltenVK uses to make Vulkan work. You can change from OpenGL to Vulkan in-game in the "Video" options menu. In other news, I've got more games that I can handle right now. It's nice shipping a game a month, but some of these are not going to be quick and simple projects. Even though I didn't have much to say in the last few weeks, stick with me, it'll be worth it, I promise.

Turok 2 and Forsaken updates

A few quick notes: Turok 2 is no longer in a beta branch on Steam; if you own it on Steam, it'll download like any other game for Linux and

Sep 12, 2018

18

3

SDL update for February 2018

This month was intensely-focused Nintendo Switch work for me, and that work is almost done now! Right now the only significant missing featu

Mar 2, 2018

8

3

If you want to keep supporting me but want to dump Patreon, I've set up a Liberapay account. For the time being I'll keep posting here, but it's an option that might be better. I say might because there are lots of unknowns in there that have me nervous about their system, but they seem to have their heart in the right place. They are a non-profit based in France. For those of you that generally deal in Euros and not US dollars, they might be a better option in any case (US dollars are accepted too, for you Americans out there). Payment works slightly differently there; you can preload your account to avoid transaction fees, etc. They have a page to explain the differences . If you drop your subscription here, you might want to click the "Follow" button on my creator page so you still see updates (Liberapay doesn't really have the "social" features of Patreon, so content will still land here). Note that if you donate through Liberapay, there are no rewards and I will not know who you are . Liberapay just tells me there's money and not who it came from. If it's a total disaster, I'll shut it down and explore other options. I'm as sad as everyone else that I have to run these experiments. If you're in, though: you can find me on Liberapay here .

Patreon and/or Liberapay

If you want to keep supporting me but want to dump Patreon, I've set up a Liberapay account. For the time being I'll keep posting here, but

Dec 10, 2017

4

9

Project: mojozork-libretro

I know, what the heck, he's still talking about Zork?! I started setting up a computer just to be my, uh, emulation station. Just a small Sh

Apr 8, 2022

10

Locked

Want your name in the credits?

So I'm seven videos into this thing now, and since it's still going, I thought I might add a screen at the end with patron names on it.

Mar 11, 2022

9

4

YouTube

Porting Chocolate Doom from SDL2 to SDL3

This first part is a bit of a slog; it took longer than I expected and it's covering a lot of ground we've already covered in other videos,

June 17

18

2

SDL3_mixer is officially released! Go get it!

SDL3_mixer is ready!

SDL3_mixer is officially released! Go get it!

March 11

15

1

Credits for a new video

Locked

Credits for a new video

June 12

3

1

Project: SDL3_mixer

SDL_mixer, as far as my flaky memory can recall, was written at Loki because SimCity 3000 needed something more advanced than SDL 1.2's audi

Sep 4, 2025

23

4

Project: migrate-trello-to-github

Just a quickie here: I'm still salty about changes to Trello, so I wrote a big pile of Perl to migrate Trello boards to GitHub issues. Every

Sep 9, 2025

9

2

Project: MojoZork SDL3 client

I know, I KNOW, I've fallen off the wagon again. We're gonna have to have an Infocom intervention. It's fine, I can stop any time I want to.

Sep 24, 2025

6

YouTube

Project: mojozork-libretro visual styles

Another quickie for you! Someone was playing around with my mojozork libretro core, and wanted a more intensely nostalgic experience, and a

Sep 18, 2025

5

1

SDL3 is officially released!

Folks, we have climbed the mountain. SDL3 is official! After 17 bazillion years, there is a real, honest-to-god stable release. So as to be

Jan 22, 2025

30

5

YouTube

Porting ioquake3 from SDL2 to SDL3

As threatened^Wpromised, I have filmed myself moving ioquake3 from SDL2 to the now-safe-to-use SDL3. It's a whale of a video--93 minutes!--b

Oct 29, 2024

19

5

A couple of SDL3 quickies...

We have a Quick Reference page now! The SDL3 API is massive, but this is the most compact way we could find to show you everything in a sear

Dec 12, 2024

13

2

SDL3 is ABI-locked!

Oh man, we're almost home now. SDL3 is officially ABI-locked, which is to say we might add interfaces, but the ones that are there now will

Oct 9, 2024

18

3

YouTube

Porting DirkSimple from SDL2 to SDL3

Here's a followup to the Quake 3 video, this time working on DirkSimple. The first 35 minutes is the basic SDL2->SDL3 port, then we get into

Dec 4, 2024

10

1