Pinball on 64-bit Alpha AXP Windows NT

This is a guest post from Yufeng Gao

One of the most popular OS built-in games is no doubt Pinball, known by its full name 3D Pinball for Windows – Space Cadet. It started out as Full Tilt! Pinball, developed by Cinematronics and published by Maxis. It offered 3 tables, and one of them, Space Cadet, was licensed to Microsoft to be included in Microsoft Plus! 95 and, later, built into the Windows operating system.

Windows XP was the last version of Windows to include Pinball, and Raymond Chen explained why it didn’t make it to Windows Vista on his blog. The reason was it had a collision detector bug when it was compiled for 64-bit Windows, which caused the ball to pass through various objects – falling off the screen through the plunger instead of being launched, for instance. The bug rendered the game unplayable, and Raymond and his colleague were unable to find a fix in a reasonable amount of time, so he removed it. At least that’s the story we were told, for about a decade.

In 2021, NCommander launched a series of investigations to challenge that, testing Pinball on various 64-bit (IA-64 and AMD64) builds of Windows XP and pre-release Vista. He found that the 64-bit versions of Pinball were all highly playable, with only very minor glitches, and speculated that the reason for its removal was that the UI did not fit into the Windows Vista design.

Not long after NCommander published his video, Raymond followed up with a post that filled in some gaps in the story and shed more light on the bug. He said it was the 64-bit Alpha AXP version of Pinball that had the extremely bad collision detection bug. This claim had been unverifiable for the past 5 years, for the following reasons:

  • No 64-bit Windows was ever released for the Alpha AXP – Compaq killed Windows NT support before NT was ported to 64-bit
  • One 64-bit Alpha AXP NT build was leaked in 2023, but the included Pinball does not work, as it segfaults immediately upon running

I’ve had an interest in the DEC Alpha for quite some time now, mainly out of my love for DEC architectures and my love for UNIX. VAX is the direct successor of PDP-11, and Alpha is the direct successor of VAX. Earlier, some Alpha emulation breakthroughs dropped, and I was pinged by a few friends that NT 4.0 could now run on a fork of the ES40 emulator, as well as on QEMU. I never thought Alpha NT would ever run under emulation, because unlike the familiar Tru64, Linux and the BSDs, NT uses its own custom PALcode and depends on ARC (Advanced RISC Computing) instead of SRM. Of course, people noted that the emulators couldn’t run the holy grail of Alpha NT – Windows (XP?) build 2210, because its kernel would panic with a memory management error in QEMU, or wouldn’t detect the keyboard and bug out in ES40. A few trips to hell in the symbol-less NT kernel and a few MMU emulation fixes later, I was able to patch up both QEMU and ES40 to boot that only surviving 64-bit build of Alpha NT.

After torturing my brain debugging a symbol-less NT kernel without a kernel debugger, I thought I’d give fixing Pinball a go, to make things worthwhile. One of the benefits of debugging a userland process is that, while there’s still no debugger, there is Dr. Watson, which takes core dumps and performs simple post-mortems. Something is better than nothing, as people would say.

Running Pinball gives the classic crash symptom immediately, with no graphics drawn:

Dr. Watson concludes that it died of a segfault:

It gave a nice dump of registers at the time of the fault:

State Dump for Thread Id 0x124

  v0=01002930 00000000   t0=00000000 00360000   t1=00000000 00000001
  t2=00000000 00360000   t3=00000000 00000000   t4=00000000 00000000
  t5=00000000 0000011c   t6=000003ff fff8f868   t7=00000000 00303030
  s0=000003ff fff8fac0   s1=01002930 00000000   s2=000003ff fff8fad8
  s3=00000000 00000000   s4=00000000 0106f2a8   s5=00000000 01000000
  fp=00000000 00000010   a0=01002930 00000000   a1=00000000 00000000
  a2=000003ff fff8fad8   a3=00000000 30010000   a4=00000000 69e17610
  a5=00000000 69e0a360   t8=000003ff fff8f868   t9=00000000 00000000
 t10=00000000 00300000  t11=00000000 00000002   ra=00000000 69e9d5c0
 t12=00000000 6a264710   at=ffffffff fffffe10   gp=00000000 00000000
  sp=000003ff fff8fa50 zero=00000000 00000000 fpcr=08000000 00000000
SoftFpcr=00000000 00000000  fir=6a264710
 psr=00000003
mode=1 ie=1 irql=0 

Some disassembly around the faulting instruction:

function: Otsstrlen
FAULT ->00000000'6a264710: 2f700000 ldq_u t12,0(a0)
        00000000'6a264714: 239fffff lda at,-1(zero)
        00000000'6a264718: 4b90065c mskql at,a0,at
        00000000'6a26471c: 4600f000 and a0,#7,v0
        00000000'6a264720: 477c041b bis t12,at,t12
        00000000'6a264724: 43fb01fb cmpbge zero,t12,t12
        00000000'6a264728: 43e00520 subq zero,v0,v0
        00000000'6a26472c: f7600005 bne t12,00000000'6a264744 Otsstrlen+00000034
        00000000'6a264730: 2f700008 ldq_u t12,8(a0)
        00000000'6a264734: 42011410 addq a0,#8,a0
        00000000'6a264738: 40011400 addq v0,#8,v0
        00000000'6a26473c: 43fb01fb cmpbge zero,t12,t12

And a very useful stack backtrace:

*----> Stack Back Trace <----*

FramePtr ReturnAd Param#1  Param#2  Param#3  Param#4  Function Name
000003FFFFF8FA50 0000000069E9D5BC 0100293000000000 0000000000000000 000003FFFFF8FAD8 0000000030010000 !Otsstrlen 
000003FFFFF8FA50 0000000069E9DB64 0100293000000000 0000000000000000 000003FFFFF8FAD8 0000000030010000 !PostThreadMessageA 
000003FFFFF8FA90 0000000069E9C000 0100293000000000 0000000000000000 000003FFFFF8FAD8 0000000030010000 !SetClassLongA 
000003FFFFF8FB80 000000000100F914 000003FFFFF8FC58 0000000000000000 000003FFFFF8FAD8 0000000030010000 !RegisterClassA 
000003FFFFF8FBF0 0000000001012A1C 0000000001000000 0000000001002DC8 000003FFFFF8FAD8 0000000030010000 !<nosymbols> 
000003FFFFF8FCA0 0000000001064B0C 0000000001000000 0000000001002DC8 000003FFFFF8FAD8 0000000030010000 !<nosymbols> 
000003FFFFF8FED0 0000000068948C50 0000000001000000 0000000001002DC8 000003FFFFF8FAD8 0000000030010000 !<nosymbols> 
000003FFFFF8FFC0 0000000000000000 0000000001064800 0000000001002DC8 000003FFFFF8FAD8 0000000030010000 !BaseProcessStart 

Ok, so it died inside RegisterClassA, a critical Win32 API function. That API function couldn’t have been the culprit, because if it were bugged, no GUI Win32 program would run at all. This means the only possible source of the error is its sole argument – a pointer to a WNDCLASSA struct. Needless to say, the pointer itself was valid, otherwise the API would’ve detected the invalid argument, or the segfault would’ve happened a lot sooner.

From the stack trace, the return address of RegisterClassA was 0x100F914, inside the function splash_screen. A quick disassembly of the instructions preceding that address shows a WNDCLASSA structure being built with the following layout:

00000000 u32 style         = 0
00000004 u64 lpfnWndProc   = splash_message_handler (0x100FE40)
0000000C u32 cbClsExtra    = 0
00000010 u32 cbWndExtra    = 8
00000014 u64 hInstance     = *0x106AE30
0000001C u64 hIcon         = NULL
00000024 u64 hCursor       = LoadCursorA(NULL, IDC_ARROW)
0000002C u64 hbrBackground = NULL
00000034 u64 lpszMenuName  = "" (0x1002710)
0000003C u64 lpszClassName = "3DPB_SPLASH_CLASS" (0x1002930)

Right off the bat, I noticed something wrong – the field alignment. It is a general requirement that fields be aligned to their size, as in 8-bit fields should be byte-aligned, 16-bit fields should be 16-bit (2-byte) aligned, 32-bit fields should be 32-bit (4-byte) aligned, and 64-bit fields should be 64-bit (8-byte) aligned. If you look at the offsets of the fields above, the 32-bit ones are indeed 4-byte aligned, but the 64-bit ones are not. At the start, we have a 32-bit style field followed by a 64-bit lpfnWndProc, and to satisfy the alignment requirements, a 4-byte padding should be inserted between style and lpfnWndProc to ensure that lpfnWndProc starts on an 8-byte boundary. RegisterClassA was expecting this padding, but Pinball lacked it, so it read data from the wrong offset and crashed.

To fix this, I simply bumped the offset of each field after style up by 4 bytes.

 00000000 u32 style         = 0
-00000004 u64 lpfnWndProc   = splash_message_handler (0x100FE40)
+00000008 u64 lpfnWndProc   = splash_message_handler (0x100FE40)
-0000000C u32 cbClsExtra    = 0
+00000010 u32 cbClsExtra    = 0
-00000010 u32 cbWndExtra    = 8
+00000014 u32 cbWndExtra    = 8
-00000014 u64 hInstance     = *0x106AE30
+00000018 u64 hInstance     = *0x106AE30
-0000001C u64 hIcon         = NULL
+00000020 u64 hIcon         = NULL
-00000024 u64 hCursor       = LoadCursorA(NULL, IDC_ARROW)
+00000028 u64 hCursor       = LoadCursorA(NULL, IDC_ARROW)
-0000002C u64 hbrBackground = NULL
+00000030 u64 hbrBackground = NULL
-00000034 u64 lpszMenuName  = "" (0x1002710)
+00000038 u64 lpszMenuName  = "" (0x1002710)
-0000003C u64 lpszClassName = "3DPB_SPLASH_CLASS" (0x1002930)
+00000040 u64 lpszClassName = "3DPB_SPLASH_CLASS" (0x1002930)

But that was not sufficient – Pinball calls RegisterClassA in 4 different places – Sound_Initsplash_screenWinMain and WaveMixStartup. I’d already patched the one in splash_screen, so I started going through the rest one by one.

The ones in Sound_Init and WinMain were identical to the one in splash_screen, but for some strange reason the one in WaveMixStartup already had the correct alignment:

00000000 u32 style         = 0
00000004 u32 <unused>      = <undefined>
00000008 u64 lpfnWndProc   = WndProc (0x105CFA0)
00000010 u32 cbClsExtra    = 0
00000014 u32 cbWndExtra    = 0
00000018 u64 hInstance     = *0x106B818
00000020 u64 hIcon         = NULL
00000028 u64 hCursor       = LoadCursorA(NULL, IDC_ARROW)
00000030 u64 hbrBackground = GetStockObject(LTGRAY_BRUSH)
00000038 u64 lpszMenuName  = NULL
00000040 u64 lpszClassName = "WavMix32" (0x10050B0)

I can’t think of why the same struct would be aligned differently within the same binary, unless they came from different objects compiled with different flags or something.

Anyway, with the WNDCLASSA struct alignment fixed in 3 of the 4 places, I ran Pinball again. This time it created the fullscreen window and attempted to draw the splash screen before dying of another segfault:

Crash log shows that the segfault happened deep in the Win32 audio system, while calling auxSetVolume:

*----> Stack Back Trace <----*

FramePtr ReturnAd Param#1  Param#2  Param#3  Param#4  Function Name
000003FFFFF8E9C0 0000000050306E84 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 !<nosymbols> 
000003FFFFF8E9E0 00000000503034B8 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 !<nosymbols> 
000003FFFFF8EA10 0000000050304B60 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 !<nosymbols> 
000003FFFFF8EA90 0000000050305B8C 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 !<nosymbols> 
000003FFFFF8EB20 000000000001AF78 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 !<nosymbols> 
000003FFFFF8EB60 0000000000025AFC 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 !auxSetVolume 
000003FFFFF8EBD0 0000000000025F98 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 !mixerSetControlDetails 
000003FFFFF8EC80 0000000000027214 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 !mixerSetControlDetails 
000003FFFFF8ED30 0000000000030644 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 !mciSendCommandW 
[...]
000003FFFFF8FC60 0000000001012AB4 0000000000000000 000003FFFFE5DE00 000003FFFFE5D420 0000000000000000 !CreateWindowExA 
000003FFFFF8FCA0 0000000001064B0C 0000000000000000 000003FFFFE5DE00 000003FFFFE5D420 0000000000000000 !<nosymbols> 
000003FFFFF8FED0 0000000068948C50 0000000000000000 000003FFFFE5DE00 000003FFFFE5D420 0000000000000000 !<nosymbols> 
000003FFFFF8FFC0 0000000000000000 0000000001064800 000003FFFFE5DE00 000003FFFFE5D420 0000000000000000 !BaseProcessStart

The fault happened while trying to dereference a pointer in the register a0 (r16):

function: <nosymbols>
        00000000'50305658: 00000000 halt
        00000000'5030565c: 00000000 halt
        00000000'50305660: 23deffe0 lda sp,-20(sp)
        00000000'50305664: b53e0000 stq s0,0(sp)
        00000000'50305668: b55e0008 stq s1,8(sp)
        00000000'5030566c: b57e0010 stq s2,10(sp)
        00000000'50305670: b75e0018 stq ra,18(sp)
        00000000'50305674: 47f00409 bis zero,a0,s0
        00000000'50305678: 47f1040a bis zero,a1,s1
        00000000'5030567c: 47ff040b bis zero,zero,s2
FAULT ->00000000'50305680: a2100128 ldl a0,128(a0)
        00000000'50305684: 20500001 lda t1,1(a0)
        00000000'50305688: e440001b beq t1,00000000'503056f8 00000000'503056f8
        00000000'5030568c: d35ff6f8 bsr ra,00000000'50303270 00000000'50303270
        00000000'50305690: e4000019 beq v0,00000000'503056f8 00000000'503056f8
        00000000'50305694: 47e00411 bis zero,v0,a1
        00000000'50305698: 454b0801 xor s1,s2,t0
        00000000'5030569c: e4200005 beq t0,00000000'503056b4 00000000'503056b4
        00000000'503056a0: a2090128 ldl a0,128(s0)
        00000000'503056a4: d35ff722 bsr ra,00000000'50303330 00000000'50303330
        00000000'503056a8: 4160300b addl s2,#1,s2
        00000000'503056ac: 47e00411 bis zero,v0,a1

The register dump shows the value of a0 at the time of the crash:

State Dump for Thread Id 0x150

  v0=000003ff ffe5de00   t0=00000000 00000000   t1=00000000 00000058
  t2=00000000 50306150   t3=00000000 0000015e   t4=00000000 00000001
  t5=00000000 00000001   t6=00000000 50300000   t7=00000000 00ed39fb
  s0=00000000 ffe5d420   s1=00000000 00000000   s2=00000000 00000000
  s3=00000000 00000000   s4=00000000 00000001   s5=00000000 50305a10
  fp=00000000 000123b8   a0=00000000 ffe5d420   a1=00000000 00000000
  a2=000003ff ffe5d420   a3=00000000 00000000   a4=00000000 00000000
  a5=00000000 cc5a4dbc   t8=00000001 00000000   t9=00000000 00000612
 t10=d1b71758 e219652c  t11=00000000 00000612   ra=00000000 50306e88
 t12=00000000 00000000   at=00000000 00010000   gp=00000000 00000000
  sp=000003ff fff8e9c0 zero=00000000 00000000 fpcr=89000000 00000000
SoftFpcr=00000000 00000000  fir=50305680
 psr=00000003
mode=1 ie=1 irql=0 

Indeed, it was an invalid pointer! As you can see, it’s identical to the pointer in a2, but with the entire top 32 bits zeroed. It must’ve been truncated by a bug somewhere, either in the audio subsystem or in Pinball itself.

I spent some time and pinned down the DLL responsible for the fault – mciseq.dll, and did some tracing. The truncation of a0 happened when a2 was moved into a0:

50306150    ZAPNOT  a2,#15,a0

ZAPNOT is an interesting instruction – it takes a source register, a bitmask and a destination register, and it “zaps” (zeros) the bytes whose corresponding bit in the bitmask is 0. In this case, the bitmask is 15, which is 00001111 in binary. From this we can work out that the ZAPNOT instruction at 0x50306150 zeros the upper 4 bytes of a2, when it is copied into a0. This perfectly explains why, at the time of the fault, a0 contained a truncated version of the pointer in a2.

Of course, 0x50306150 was not the only place where it truncated 64-bit pointers, I found 6 truncations of the exact same type in mciseq.dll. I have not the slightest clue why it decided to truncate pointers. If I had to guess, maybe they had pointer → integer → pointer casts for whatever reason, and that integer type was 32-bit. With all 6 truncations patched out, we have some Pinball for ourselves:

Here’s proof that Pinball is indeed running on a 64-bit build of Alpha NT:

To make Pinball work on your NT build 2210 install, replace %ProgramFiles%\Windows NT\Pinball\pinball.exe and %windir%\system32\mciseq.dll with the following:

You could also patch the installation files and burn them to a new CD if you want Pinball to work out of the box on fresh installs – simply copy these files to the AXP64 directory of the install disc:

The Bug

Now I’m going to disappoint you with the fact that I did not find the collision detector bug Raymond talked about. With the struct alignment and pointer truncation issues patched, the game now works flawlessly. Ok, I’m not sure if it’s actually flawless, but I never saw any glitches in the few games I played. At the very least, the ball does not fall through the plunger, can be launched and bounces around the table just fine.

Below are the 2 reasons I could think of for not seeing the collision detector bug:

  • The bug was introduced after build 2210. Build 2210 has Pinball installed by default and predates Windows XP by almost a year and a half, so it almost certainly predates Raymond removing it. In this build, Pinball doesn’t even run by default, so there’s no way they could’ve tested it and seen the bug. They probably only started testing Pinball later, after they fixed the struct alignment and pointer truncation issues.
  • The bug only manifests in free/release builds, not in checked/debug builds. Maybe it only shows up when the code is compiled with the more aggressive optimisation used by release builds – something that happens quite often when code has undefined behaviour or the compiler has bugs. This is less plausible, however, as I’m sure Raymond would’ve used debug builds when he attempted to debug it, and discovered any differences between debug and retail builds.

Of course, only Raymond himself could shed more light on this topic. It was fun (read: painful) debugging Pinball, as well as the NT kernel to fix the emulators – too much fun (read: pain) that I will never do it again.


Some trivia about me and pinball:

I spent a fair chunk of my kindergarten and pre-school days playing the various games my dad installed on our Windows XP home computer, however, there was one game that I never quite figured out how to play – Pinball. It came bundled with the OS, and the splash screen scared me every time I tried to open it up.

The flipper looked like a pistol to my 3-year-old self, and the overall darkness of the splash screen just injected fear into me. I would open the game, close my eyes, count to 20, then open my eyes again, to skip past the splash screen.

The first time I actually played a full game of pinball was on the first day of this year, when a friend of mine took me to an arcade. After playing pinball in real life, the Pinball game finally started to make sense.

I just wanted to play Simpsons Hit & Run, or how I really hate Apple peripherals

Such a simple goal!

Months ago at the local CeX I had spotted The Simpson’s hit & run for a mere 8GBP. Sweet, I know the game has a massive cult following, and I wanted to try it, but being old and grumpy I wanted to have a physical copy, you know so I could know it only had weird Vivendi spyware on it.

Fun fact! Vevendi bought the call centre I worked at in Miami back in the early 00’s and I had hoped to somehow swing my way to Sierra. Instead I got saddled working with Ticketmaster. Not the fun I wanted.

Anyways flash forward a few decades and yeah, this game is from ’03 back in those good olde days. Wow time flies!

On the home front, I’m not a big fan of Windows 11. As a matter of fact, I hate it. The UI is just obnoxious, and as much fun as WSL is, even it cannot save the horror that is Windows 11’s two things that drive me away from the platform

  • The absolute braindead notepad
  • It’s reverse sorting of applications on the ALT-TAB stack

Seriously, the last application I used should be the FIRST on the ALT-TAB stack not the last. WTF. And notepad, what the actual fuck, with AI? I can’t even reliably search & replace without it absolutely trashing a document trying to replace double spaces with single spaces. How can you fuck up notepad? Microsoft found a way. Even better replacing it with the one from Windows 8.1 or launch Windows 10 just completly screws up the OS.

Great job guys!

So I did what anyone else would do, I put aside a hundred pounds a month, and after 6 months I pulled the trigger and got a M4 Mac Mini.

Cyberpunk 2077 over 100FPS!

The good? It’s surprisingly fast for what it is. It actually plays CyberPunk 2077 (there is a native version, you can even hit over 100fps!, or even 72fps with ray tracing – granted I did drop the resolution to 720p, and medium textures, and added in frame generation), Crossover is mostly okay, I can still use SQL Server 4.20, and Word 6 for NT, although Excel has major issues for some reason. Edge & Onedrive work just fine, and shockingly. whisper.cpp using the metal backend & ggml isn’t too horrible:

whisper_model_load: model size    =  538.59 MB
whisper_backend_init_gpu: using Metal backend
ggml_metal_init: allocating
ggml_metal_init: found device: Apple M4
ggml_metal_init: picking default device: Apple M4
ggml_metal_load_library: using embedded metal library
ggml_metal_init: GPU name:   Apple M4
ggml_metal_init: GPU family: MTLGPUFamilyApple9  (1009)
ggml_metal_init: GPU family: MTLGPUFamilyCommon3 (3003)
ggml_metal_init: GPU family: MTLGPUFamilyMetal3  (5001)

Just remember to build with “-DWHISPER_COREML=1” set for Apple hardware.

I went ahead to test using the old “Lord of the Rings” tapes I’d got last year, and aribitrarly picked tape 12 side 1:

Input #0, flac, from 'lotr-tape12-sie1.flac':
  Metadata:
    title           : Mount Doom Part 1
    album           : The Lord of the Rings
    artist          : Brian Sibley
    date            : 1981
    genre           : Audio Book
    track           : 23
    encoder         : Lavf58.76.100
  Duration: 00:31:35.63, start: 0.000000, bitrate: 596 kb/s

And the m4 Mac Mini crunched through the 31 minutes in 2:47! You can check the output here:

18.52s user 1.69s system 12% cpu 2:47.64 total

Or the JFK benchmark:

whisper_print_timings:    total time =  1464.12 ms  
./medium.sh samples/jfk.wav  0.18s user 0.15s system 22% cpu 1.512 total
                               

Ok that’s all great, but what about this optical drive?

I picked up an Apple Super Drive A1379 used from CeX, again for a whopping 28GBP. Sure it’s a bit scuffed up and ugly but plugging it into my Windows 11 laptop it shows up right away. Nice

Also let me take a moment to say thanks for basically writing this on the under side of the drive in what may as well have been black in on a black surface. I’ve had to use sunlight & a full flash to get this to show up to verify the model number. And what I suspect is 2 parts of the larger problem, it being an optical drive from 2012.

Model No: A1379

Like seriously could they make it any harder. And yes dropoping support has always been a thing.

Okay, so I still have my Windows 11 laptop, and when connected I cannot insert a disc to save my life. Well to cut the story short, YOU NEED A DRIVER. I kid you not.

The driver, named AppleODDInstaller64.exe is what you are after, and luckily for you, I’ve already gone through the motion of extracting various bootcamp driver packs to find it, and upload it to archive.org.

With the driver loaded, I could then finally just copy the files off the install discs and install into crossover. Of course the default install requires CD1 to be inserted like a key disc, so gamecopyworld to the rescue.

Simpsons Hit & Run on OS X Sequoia 15.6.1 (24G90) / CrossOver Version 25.1.1 (25.1.1.38624)

I have to say that running x86 code through the new rosetta feels pretty snappy. The biggest dissapointment of course is that there is no 32bit support in OS X. Crossover at least maintains that pretty well, although there is no Win16 support. And yes I’ve tried otvdm, and no it doesn’t work.

The funny part is that Hit&Run runs signiicantly faster on the M4 OS X / Crossover setup. That’s unexpected! The annoying part is that although Crossover does support controllers, neither DirectInput or Xinput seem to work. So I’m forced to use keyboard and mouse, which is kind of annoying as I still don’t have a proper desk after moving, and I end up just using bluetooth and my TV to do stuff, as I’m even writing this from my couch.

At least there are some alterantives out there. I know there will be the inevitable cry, what about Linux, and honestly I’d probably go with the Milk-V Titan, and all in on RISC-V. But considering how much more expensive the Titan is than the Jupiter, I’ll be sitting on the sidelines for the first wave to see if the much hoped for 64GB of RAM, and real GPU support actually works. Although I’m glad I got the 16gb model of the Jupiter, I never could get any GPU device recognized so I mostly use it for weird internet edge stuff, as at least if I do get hit with buffer overflows, being RISC-V means default out of the box x86/x86_64 attacks are meaningless.

Apparently talking about DOS Extenders is too hot for Twitter: AKA Phar Lap 386

I had a small twitter account, and I tried not to get dragged into anything that would just be basically wasting my time. Just stay focused and on topic. FINE. I just wanted to see if anyone ever saw it, if it was even worth the effort of doing WIP’s as I didn’t want to make it super annoying.

I logged on to post a fun update that I’d finally gotten a Phar Lap 386 version 4.1 app to do something halfway useful, the sairen AGI interpreter up and running in the most basic sense.

Talking about DOS Extenders is spammy and manipulation!

I don’t get what triggered it, but oh well there was a ‘have a review’ and yeah that was fine. Great. So I’m unlocked so I go ahead and post with the forbidden topic, as I’m clearly dumb, and forgetting that Twitter is for hate mobs & posting pictures of food, and cat pictures.

The Sairen AGI interpreter built with Watcom 386/7.0 & Phar Lap 386 4.1

So yes, that was a line too far, and now that’s it.

Now some of you may think, if you buy ‘the plan’ you’ll no doubt be exempt from the heavy hands of Twitter

3 squids a month

But I already was and had been for a while.

Your account is suspended

So that’s the end of that. I guess it’s all too confusing for a boomer like me.

Cancel me, cancel you

So needless to say I cancelled Twitter as well. Kind of sneaky they didn’t auto-cancel taking money.

So yeah, with that out of the way, let’s continue into DOS Extender land. I added just enough 386 magic, onto github: neozeed/sarien286. Yes I see now it really was a poorly named repo. Such is life.

There is 3 main things for porting old programs where they take care of all the logic, it’s going to be File I/O, Screen I/O, and timers. Luckily this time it was easier than I recalled.

Over on usenet (google groups link) Chris Giese shared this great summary on direct memory access from various methods:

/* 32-bit Watcom C with CauseWay DOS extender */
int main(void) {
char *screen = (char *)0xA0000;
initMode13();
*screen = 1;
return 0;
}

/* 32-bit Watcom C with DOS/4GW extender
(*** This code is untested ***) */
int main(void) {
char *screen = (char *)0xA0000;
initMode13();
*screen = 1;
return 0;
}

/* 32-bit Watcom C with PharLap DOS extender
(*** This code is untested ***) */
#include <dos.h> /* MK_FP() */
#define PHARLAP_CONVMEM_SEL 0x34
int main(void) {
char far *screen = (char far *)MK_FP(PHARLAP_CONVMEM_SEL, 0xA0000);
initMode13();
*screen = 1;
return 0;
}

/* 16-bit Watcom C (real mode) */
#include <dos.h> /* MK_FP() */
int main(void) {
char far *screen = (char far *)MK_FP(0xA000, 0);
initMode13();
*screen = 1;
return 0;
}

It is missing the Phar Lap 286 method:

/* Get PM pointer to text screen */
  DosMapRealSeg(0xb800,4000,&rseg);
  textptr=MAKEP(rseg,0);

But it’s very useful to have around as documentation is scarce.

Which brings me to this (again?)

Phar Lap 386|Dos-Extender 4.1

Years ago, I had managed to score a documentation set, and a CD-ROM with a burnt installed copy of the extender. I didn’t know if it was complete, but of course these things are so incredibly rare I jumped on the chance to get it!

2011!

Unfortunately, I didn’t feel right breaking the books apart, and scanning them, then add in some bad life choices on my part, and I ended up losing the books. Fast forward *years* later and Foone uploaded a document set on archive.org. GREAT! As far as I can tell the only difference in what I had is that I’ve got a different serial number. Thankfully I was smart enough to at lest email myself a copy of the CD-ROM contents! And this whole thing did inspire me to gut and upload the Phar Lap TNT 6.0 that I had also managed to acquire.

Although unlocking the video RAM wasn’t too bad, once I knew what to do, the other thing is to hook the clock for a timer. ISR’s are always hell, but at least this is a very simple one:

void (__interrupt __far *prev_int_irq0)();
void __interrupt __far timer_rtn();
int clock_ticks;
#define IRQ0 0x08
void main()
  {
   clock_ticks=0;
   //get prior IRQ routine
   prev_int_irq0 = _dos_getvect( IRQ0 );
   //hook in new protected mode ISR
   _dos_setvect( IRQ0, timer_rtn );

/* do something interesting */
   //restore prior ISR
   _dos_setvect( IRQ0, prev_int_irq0 );
  }

void __interrupt __far timer_rtn()
  {
    ++clock_ticks;
    //call prior ISR
    _chain_intr( prev_int_irq0 );
  }

The methodology is almost always the same, as always, it’s the particular incantation.

So yeah, it’s super simple, but the 8086/80286 calling down to DOS/BIOS from protected mode via the int86 just had to be changed to int386, and some of the register structs being redefined. I’m not sure why but the video/isr code compiled with version 7 of Watcom, but crashes. I think its more drift in the headers, as the findfirst/findnext/assert calls are lacking from Watcom 7, so I just cheated and linked with Watcom 10. This led to another strange thing where the stdio _iob structure was undefined. In Watcom 10 it became __iob, so I just updated the 7 headers, and that actually worked. I had to include some of the findfirst/next structures into the fileglob.c file but it now builds and links fine.

Another thing to do differently when using Watcom 7, is that it doesn’t include a linker, rather you need to use 386LINK. Generating the response file, as there is so many objects didn’t turn out too hard once I realized that by default everything is treated as an object.

Another fun thing is that you can tell the linker to use the program ‘stub386.exe’ so that it will run ‘run386’ on it’s own, making your program feel more standalone. From the documentation:

386 | LINK has the ability to bind the stub loader program, STUB386.EXE, to 
the front of an application .EXP file. The resulting .EXE file can be run by 
typing the file name, just like a real mode DOS program. The stub loader 
program searches the execution PATH for RUN386.EXE (the 

386 | DOS-Extender executable) and loads it; 386 | DOS-Extender then loads 
the application .EXP file following the stub loader in the bound .EXE file. 


To autobind STUB386.EXE to an application .EXP file and create a bound 
executable, specify STUB386.EXE as one of the input object files on the 
command line.

So that means I can just use the following as my linker response file.

agi.obj,agi_v2.obj,agi_v3.obj,checks.obj,cli.obj,console.obj,cycle.obj
daudio.obj,fileglob.obj,font.obj,getopt.obj,getopt1.obj,global.obj
graphics.obj,id.obj,inv.obj,keyboard.obj,logic.obj,lzw.obj,main.obj
menu.obj,motion.obj,pharcga3.obj,objects.obj,op_cmd.obj,op_dbg.obj
op_test.obj,patches.obj,path.obj,picture.obj,rand.obj,savegame.obj
silent.obj,sound.obj,sprite.obj,text.obj,view.obj
words.obj,picview.obj stub386.exe
-exe 386.exe
-lib \wat10\lib386\dos\clib3s.lib \wat10\lib386\math387s.lib
-lib \wat10\lib386\dos\emu387.lib

It really was that simple. I have to say it’s almost shocking how well this went.

So, this brings me back, full circle to where it started, me getting banned for posting this:

32bit!

I thought it was exciting!

For anyone who feels like trying it, I prepped a 5 1/4″ floppy disk image.

running on 86box, 386DX-40 CGA graphics

One interesting observation is that the 386 extender is actually smaller than the 286 one. And being able to compile with full optimisations it is significantly faster.

16bit on the left, 32bit on the right.

I ran both the prior 16bit protected mode version (on the left), and 32bit version (on the right), on the same IBM PS/2 80386DX 16Mhz machine. You can see how the 32bit version is significantly faster!.

I really should profile the code, and have it load all the resources into RAM, it does seem to be loading and unloading stuff, which considering were in protected mode, we should use all ram, or push the VMM386 subsystem to page, and not do direct file swapping, like it’s the 1970s.

Phar Lap’s 286/DOS-Extender: why nobody used it for games

Your scientists were so preoccupied with whether or not they could, they didn’t stop to think if they should.

-Ian Malcolm

Ever since I got my first 286 board way back in the early 90’s (1990? 1991?) I have been intrigued by the whole protected mode of operation. Unfortunately, in the era the required tools were way out of my reach, and of course were not available at retail. But now I live in the future where I have all the parts! Let’s look at the needed parts

I have to once more again thank my patrons, and people tolerating the google ads as it made all the difference in being able to buy all this stuff. And now is as good a time as any other to put it all together.

I stumbled upon this repo on sourceforge, Sarien. It included a Turbo C++ port, which is pretty exciting! So, this became my goal, get Sarien running on Phar Lap 286.

Installing Microsoft C 6.0a

Installing Microsoft C requires you to pick and choose both hosting, targeting environments, along with what the preferred libraries are. In the business we call this foreshadowing as this can be such a giant PITA. At least virtual machines are fast, plentiful, and cheap. In addition I had been using MS-DOS player to host the tools on Windows 11. This of course proved weird later.

The first step was getting it running on MS-DOS using Microsoft C 6.0a. This was actually pretty easy, the hard part was working out the makefile, as some files don’t compile with optimisations. And overall, the project doesn’t seem to work with /Ox at all. I haven’t spent enough time mixing and matching settings to find what actually doesn’t work, but I’m in a hurry, and /Os seems to work just fine.

In no time I had both the CGA & VGA drivers up and running and verified working on my PS/2. Great!

Now comes the fun, getting it ready to run on Pharlap.

The magic!

Phar Lap’s 286|DOS-Extender is pure magic. A DOS extender is a special program that can load a protected mode program into memory on a 286 or better computer and run it. At it’s heart, it can proxy MS-DOS functionality from protected mode to real mode, allowing you to use a lot of methodology and code from traditional real mode code. Phar Lap, goes beyond that by providing a pseudo OS/2 1.2 environment on MS-DOS, including advanced features like DLL’s, and being able to use ALL the RAM in your computer. Of course on the 286 there is a massive caveat:

The 286 has no built-in function for switching from protected mode to real mode. This makes programs that rely a LOT on MS-DOS potentially very slow. You can absolutely feel the difference between the real mode and the protected mode version of Sarien.

Phar Lap does include a test program, swtest which can benchmark the switching methods, so let’s run it and get some scores.

Switch code version = 1.14
BIOS signature: BA66CC86
BIOS date: 02/13/87

Machine ID = 0, A20 method = PS2, Reset method = Standard
Starting test for Switch Mode 3 (SLOW) ... Test complete.
Avg switch time (usecs): To prot = 34, To real = 101, Total = 135
Min switch time (usecs): To prot = 32, To real = 98, Total = 130
Max switch time (usecs): To prot = 35, To real = 103, Total = 138

Machine ID = 0, A20 method = PS2, Reset method = Standard
Starting test for Switch Mode 2 (AT) ... Test complete.
Avg switch time (usecs): To prot = 34, To real = 86, Total = 120
Min switch time (usecs): To prot = 33, To real = 83, Total = 116
Max switch time (usecs): To prot = 36, To real = 88, Total = 124

Machine ID = 0, A20 method = PS2, Reset method = Standard
Starting test for Switch Mode 1 (SURE) ... Test complete.
Avg switch time (usecs): To prot = 34, To real = 70, Total = 104
Min switch time (usecs): To prot = 32, To real = 68, Total = 100
Max switch time (usecs): To prot = 35, To real = 72, Total = 107

For those of you wondering what the timing is like on a 386, here is my 16Mhz PS/2 Model 80 board (now with fully 32bit memory)

Switch code version = 1.14
BIOS signature: 039D2DB4
BIOS date: 03/30/87

Machine ID = 0, A20 method = PS2, Reset method = Standard
Starting test for Switch Mode 5 (386) ... Test complete.
Avg switch time (usecs): To prot = 31, To real = 22, Total = 53
Min switch time (usecs): To prot = 30, To real = 20, Total = 50
Max switch time (usecs): To prot = 32, To real = 23, Total = 55

I’m honestly surprised the 286 switches from protected back to real so quickly! Although as you can see from the 386 timings it’s significantly faster than the 286.

Here is a quick video, real mode on the left, protected mode on the right. Yes I need to get a VGA capture card. Sorry.

Real mode on the left, Protected on the right. Running on the 386

Being a DOS extender it does have built in functions for things like hooking interrupts like this:

    /* install our new timer tick routine */
    DosSetPassToProtVec(IRQ0, (PIHANDLER)new_prot_timer_tick,
        &old_prot_timer_tick, &old_real_timer_tick);

Unlike some kind of OS/2 method which would involve creating a thread and or timers.

Setting video modes via the video BIOS is also supported, using the built in int86 style calls:

    memset(&r,0x0,sizeof(r));
    r.h.ah = 0;
    r.h.al = 3;
    int86(0x10, &r, &r);

Using pointers into things like video ram do require ‘asking for permission’ but it’s not too involved:

    int rseg;
  /* Get PM pointer to text screen */
    DosMapRealSeg(0xb800,4000,&rseg);
    textptr=MAKEP(rseg,0);

with the segment mapped, and a pointer to the segment, and now I can read/write directly into video RAM!

  /* save text screen */
    memcpy(textbuf,textptr,4000);

Just like that!

I’m not sure what I screwed up on the VGA graphics, as it doesn’t work correctly, but oddly enough CGA does work.

And now this is where everything goes off the rails.

Sairen running on Qemu & DOSbox

It ran fine on emulation. So all excited I fired up the PS/2 and….

General protection fault

This lead me to more fun in how on earth to debug this. Of course Phar Lap 286 version 2.5 requires me to have Microsoft C/C++ 7.0. I shamelessly downloaded a disk set from pcjs.org. You actually need to install it, to copy out the files required:

  • 28/05/1991 05:37 pm 47,216 CFIG286.EXE
  • 26/11/1991 11:19 am 13,531 GORUN286.EXE
  • 19/03/1992 04:00 am 42,720 shw0.dll
  • 19/03/1992 04:00 am 105,039 eew0cxx.dll
  • 19/03/1992 04:00 am 410,112 cvw4.exe
  • 19/03/1992 04:00 am 91,118 eew0can.dll
  • 19/03/1992 04:00 am 74,400 emw0w0.dll
  • 03/08/1992 09:34 pm 2,649 INT33.DLL
  • 03/08/1992 09:40 pm 2,718 MSG.DLL
  • 03/08/1992 09:40 pm 1,702 NAMPIPES.DLL
  • 03/08/1992 09:42 pm 2,073 NLS.DLL
  • 03/08/1992 09:43 pm 5,184 PTRACE.DLL
  • 03/08/1992 09:45 pm 2,320 SESMGR.DLL
  • 03/08/1992 09:50 pm 1,508 WIN87EM.DLL
  • 05/08/1992 12:04 am 3,100 KEYBOARD.DLL
  • 05/08/1992 06:33 pm 270 TOOLHELP.DLL
  • 14/08/1992 07:38 pm 7,891 KERNEL.DLL
  • 14/08/1992 09:40 pm 14,545 USER.DLL
  • 09/09/1992 10:59 pm 209,922 RUN286.EXE
  • 09/09/1992 10:59 pm 229,046 RUN286D.EXE
  • 14/09/1992 11:01 pm 14,024 TLW0LOC.DLL
  • 17/09/1992 07:26 pm 34,152 CVP7.EXE

While CVP7 does come with Phar Lap, you have to run it via run286. As you may have noticed there is a mixture of OS/2 and Windows DLL’s in here, as at this point CodeView was a Windows protected mode debugger. The divorce was in full swing, and Microsoft C/C++ 7.0 had amputated the majority of OS/2 support. I’m sure all this is in the manuals, however all I have is disk images. There was no C 6.0a supported hosted debugger. Maybe it’s in the v1/v2 of Phar Lap 286, but I only have 2.5. There is version 3 files on the internet but I wanted to stick to 2.5.

Exception #13

And this is all I got. Yes, I did recompile with ‘/Od /Zi’ along with using cvpack on the executable. Yes, after copying the source to the PS/2 I was able to see the source line mapping, but it immediately jumps to assembly and GP Faults. All this is fine, but IT RUNS UNDER EMULATION.

What is going on?!

I asked around on discord, and found someone willing to test on their 286. It also crashed. I tried VMware and .. it crashed too! So did 86box! Ok now we’re going somewhere!

Since I had been using MS-DOS player to run the tools, I had an issue with the linker in C 6.0a crashing, so I tried the one from C/C++ 7. It also didn’t work. I tried the one from Visual C++ 1.5. It also failed. Almost giving up on the entire thing, since I had copied the source code to the PS/2, I tried something really silly, I compiled it using the /qc or QuickC flag. I wasn’t too worried about sizes as again I’m going to run in protected mode. It took some 20-30 minutes to compile, as 10Mhz machines are not the best for building software in this modern age. Much to my surprise it actually ran.

First run!

This was kind of shocking as I’m not sure what I screwed up to not get this to work, but it worked! I went ahead and changed the build to not use QuickC, but rebuild with /Os (Optimize for space). It took about an hour. And it too worked.

Phar Lap 286 running on OS/2 2.00 on VMware

Shockingly it runs! I’m not sure what on earth is up with the linking. I did find it easier to just rebuild on Qemu since it can easily map into my source directory and copy everything over and re-build very quickly.

Later I did try copying over compiled objects built using the MS-DOS Player, and linked them natively, and they ran fine.

What is it with the LINK?!

It all fits! Stacker rules!

One weird thing on 86box is my pre-built machine I was using has a 5 1/4″ 1.2Mb floppy for the A: drive. It’s too small to fit MS-DOS, the game data and Phar Lap 286 all on there. Although Stacker to the rescue and it fits!

It can save and even load those saves!

I removed a lot of Unix quality of life, to make it more MS-DOS dumping everything in the same directory so you can save & load games.

Assuming anyone is interested in this at all, I have the source up on github. I’ll follow up with some performance videos showing how much slower real vs protected mode is, along with some binaries/demos. A 5 1/4″ floppy disk image can be downloaded here for any suitable emulator.

Zork for the PDP-11 / RT-11 recreated

I know this is a weird one, but I’ve always wanted to run Infocom games from ever since I found out it was a thing.

The cover of the RT-11 Zork that sold on ebay for $2,348.31

I know you maybe thinking of the FORTRAN port of the full Zork game, which does run on the same system. But this is NOT the FORTRAN reverse engineered game, rather it’s a port of the Z-Machine to the RT-11 / PDP-11.

Also this is NOT a 3rd party reverse engineering effort, it is the official Infocom Z-Machine source.

;       Proprietary documentation of:
;
;               Infocom, Inc.
;               55 Wheeler St.
;               Cambridge, MA 02138
;
;       Copyright (C) 1982, 1983 Infocom, Inc.  All rights reserved.

Yes it’s the real deal!

Ok so what or where to do this?! First you need SIMH or any other good PDP-11 emulator, a copy of RT-11, and of course the source to the interpreter oddly enough named PDP11.ZIP. Just keep in mind that this is NOT a pk-zip file, it’s a text file. It’s Macro-11 assembler source.

First you need a very simple config/type in to the SIMH PDP-11 emulator:

attach rk0 rtv4_rk.dsk
attach ptr pdp11.zip
boot rk

All being well you should boot into RT-11.

Now we copy the source into the machine through the paper tape reader. Just type in ‘COPY PC: ZIP.MAC’

.COPY PC: ZIP.MAC
 Files copied:
PC:            to DK:ZIP.MAC

.

This will create a .mac or macro assembler source file. The extension matters as it will tell the compiler what file it is and what to do. But luckily this is a single file, and assembles quite easily. As a tip to Unix folk, I found that making the assembly source in MS-DOS CR/LF made life easier.

Compiling & linking is very straightforward

.COMPILE ZIP.MAC
ERRORS DETECTED:  0

.LINK ZIP.OBJ

. 

Now we need to import a game file. I usually test with Planetfall, so I grabbed the data file, and placed it into the working directory and then attached it to the emulator

Simulation stopped, PC: 152644 (BR 152622)
sim> att ptr planetfa
sim> c


.COPY PC: PLANET.IML
 Files copied:
PC:            to DK:PLANET.IML

.

Notice the filenames are short, very 8.3 for some strange coincidence! Also I named it planet.iml, as that is what the interpreter is expecting. Now we can just run the zip and point it to the game data file!

.RUN ZIP
Line width (default is 80, end with LF for status line):
File name (current default is DK:$GAME$.IML) *dk:planet.iml
PLANETFALL
Infocom interactive fiction - a science fiction story
Copyright (c) 1983 by Infocom, Inc. All rights reserved.
PLANETFALL is a trademark of Infocom, Inc.
Release 37 / Serial number 851003

And there we go! We are now running Planetfall on our simulated PDP-11!

There is quite a few great 80’s systems in the github repository, I have no doubt the rest can be built, but I thought I’d tackle a system that was another bigfoot, a thing of pure legend!

Knights of the Old Republic PowerPC

I just scored a G5 iMac for £20 with a damaged panel. It doesn’t bother me at all as I’m not going to use it for anything serious, I’m just wanting something mainstream.

I did want one thing which was KOTOR.

So I looked up eBay, and yeah turns out it’s a collectors thing?

£147!! No way!

I saw this for far less, the Star Wars Mac Pack!

vBut at the flip side had this ominous warning….

Intel only

I thought I’d just try the disc anyway.. nothing to lose?

Universal!?

and yeah, not only is KOTOR is PPC, but yes it does run on OS X 10.4!

PPP KOTOR

granted it’s on steam, gog and of course available for pretty much anything modern. And sure yeah, it was originally PC/Xbox, but for some odd reason I’m feeling nostalgic for that last gen PPC.

Stadia controllers can be re-flashed to generic PC controllers

Stadia controller

So I saw this controller for sale for 20. I figured that there ought to be a way to hack it to do something useful so it may be fun. Turns out all it needs is a re-flash of new firmware and it’ll become a generic controller.

All you need to do is peer it to your computer, go to https://stadia.google.com/controller and follow the prompts.

You might be slightly scared that the browser can talk directly to bluetooth devices, including the unlock, and re-programming the controller.

And yeah after that it’s all unlocked and good to go!

Not sure if it matters all that much, but for anyone wanting either a controller on the cheap, or if you bought into Stadia when it was a thing (did anyone remember other than the announcement and the expected cancellation?).

I guess it’s nice that google is providing an unlock service but I’m sure that won’t be around forever, so yeah. yay.

Today is Starfield day!

And there is a map of all the locales and when the launch. I was busy working and didn’t notice that Hong Kong had already launched.

many geo zones!

Thankfully I did get a payment voucher which fully covered the super fancy edition. Sadly there was no option for GPU’s so I’m going to try this with an OG 6GB GTX Titan.

It was a bit weird to figure out if I could run the game or not but I did get a pop up that today was the day so I guess so.

0% verified

I’ll have no choice but to update as it goes, so nothing yet to report.

I’m just hoping it’s not the hollow world experience of Fallout 76.

After all that downloading, turns out it doesn’t like my video card at all.

So yeah GTX 1070ti minimum. wow.

So I abused a work machine that ironically has a decent mobile GPU. swapped some storage and booted into the game, played a few hours.. And yeah

Old tyme Bethesda quality

When |I could, I took a detour to Earth’s moon, and went out for a walk. Naturally there was a base less then half a click away, and naturally it had 30 insurgents automatically hostile. What a vast but populated world. And then the NPC following me around suddenly took off her space suit, and broke the whole thing. I wanted to maybe get an ARC but apparente it doesn’t run at all on ARC. Seems kinda fishy to me tho. NVIDIA released a new driver like today for the special launch. I don’t know if there will be any fallout.