Porting 3D Movie Maker to Linux - part 2
Fri Apr 17 2026
Porting 3D Movie Maker to Linux - part 2
[Update: Ben has now posted about his work at https://benstoneonline.com/posts/porting-3d-movie-maker-to-linux/]
After a break over the summer (and also to tend to some other QEMU bits and pieces), I returned to the 3DMMEx repository and saw with amazement that Ben had now started merging his SDL backend implementation into the main branch, which suggested that it must be working. A quick email and reply from Ben confirmed this, so in theory I could now build the entire 3D Movie Maker on Linux!
I immediately pulled in the latest main branch from the 3DMMEx repository, and then started the somewhat painful process of rebasing my hack branch onto Ben’s latest work. This probably took me the best of a week during the evenings slowly removing or updating the workarounds that I had added to build my stub kauai library, until once again I could build it with the upstream SDL backend.
Defining a platform separate from architecture
One of the interesting things you notice when looking at the kauai library source is that
there are a large number of #ifdef MAC constructs, along with files containing some
Mac-specific class implementations e.g. fnimac.cpp. Going back to 1995 when 3D Movie
Maker was released there was clearly an effort for kauai to run on Classic MacOS, although
I suspect it remained in early alpha due to the large amount of unimplemented routines.
Whilst it is unlikely that the Classic MacOS port was ever functional, the presence of this
code was a huge help during this process because i) it ensured that the internal class
hierarchy was already able to support a non-Windows platform and ii) I could easily locate
parts of the code requiring closer inspection to build on Linux using grep.
Going further, I would guess that kauai was intended to be the cross-platform runtime for the range of “Microsoft Kids” products: it includes a set of fairly low-level core routines for handling files, memory, strings, clocks, graphics and sound. For a Linux port it is necessary for all of these routines to be both implemented and working properly.
For the file handling, there were already Windows and Classic MacOS versions available in
filewin.cpp and filemac.cpp along with a higher level FNI API in fniwin.cpp and
fnimac.cpp so that was easy to handle for now: I generated a new set of stubs
fileposix.cpp and fniposix.cpp from a mixture of the Windows and Classic MacOS originals
as required, and then simply added a RawRtn() function call to each. This caused each
virtual function to enter the debugger, so I could easily grab a backtrace and step through
the Windows version in my Windows 11 VM to understand what each implementation should do.
The kauai core memory handling was located in memwin.cpp and memmac.cpp, and it was
quickly clear that the Classic MacOS implementation was incomplete, generating a number of
compile errors for missing functions. Given that Classic MacOS also made use of a
handle-based
memory management system, it was decided to attempt the Linux port using the Windows
implementation as that would be the best match for a modern memory API.
The string handling could be found in utilstr.cpp and looked like it could work without
too much difficulty: there were a few Win32 API calls for upper-casing and lower-casing
strings and a handful of missing implementations, so again I updated the #ifdef logic
and added a RawRtn() in those functions I was unable to implement for Linux.
Related to this there was yet one more source of a large number of compile warnings being
generated during a gcc Linux build, and that was due to widespread casting of char * to
const char *. Whilst older compilers were less strict about this, it is often a sign
that something is wrong. Amongst the lower-level kauai string types are SZ (String
Zero-terminated) and PSZ (Pointer to String Zero-terminated) which map directly onto the
standard C char * character arrays.
My solution here was to introduce a new PCSZ (Pointer to Constant String Zero-terminated) type which was the equivalent of the C const char * character array, and then work through all the compile warnings replacing the PSZ type as necessary. This turned out to be easier than I thought, with the result that the remaining compile errors were reduced significantly.
Next to tackle was the issue of clocks, or making sure that everything runs at the right
time. Again, this was another task that was fairly easy to complete: from what I could
see everything used the TsCurrentSystem() function to return the current time as a
32-bit integer, so I could simply add a different implementation for Linux. But what if
3D Movie Maker became so popular that it would be used beyond 2038? Fortunately luck was
on our side in that TsCurrentSystem() was only ever used for relative timings, and so
the solution was store the initial time (minus one second) as a 64-bit integer and have
TsCurrentSystem() return the delta from this time in milliseconds as a 32-bit integer.
I noticed there were a few other functions in the same file as TsCurrentSystem() that
were dependent upon the platform and needed to be implemented for Linux. The only one
I feel is worth mentioning is the MUTX (mutex) class which is used throughout the
codebase to manage concurrent access. For this I created a very simple implementation
using the pthread_mutex API to implement the Enter() and Leave() methods, hoping
that there were no subtle differences in behaviour compared with the equivalent Win32
API versions.
And that was (in theory) everything all covered. Ben’s hard work on the SDL backend implementation meant that I should be able to draw graphics, and for now I could use the Audioman empty stub implementation until we figured out what to do about sound.
Working around HWND
One of the earlier discussions I’d had with Ben when working on my initial hacks was how to solve kauai’s dependency upon HWND when passing around a Window reference. For an SDL backend it is necessary to pass a pointer to an SDL_Window instead, which works fine for code within the SDL backend but not in the common superclasses where either a HWND OR an SDL_Window pointer is needed.
Ben’s solution here was to devise a new KWND class which represented either a HWND or an SDL_Window depending upon the graphical backend in use. By defining a suitable set of C++ operators it became possible to allow assignment between each type of window handle without having to devise some kind of horrible pre-processor hack to allow both types of handle to be passed around.
I was pleased to find that I only needed some minimal #ifdef tweaks to kwnd.h to be able
to get it to compile on Linux, and with that a huge number of errors disappeared from my
kauai stub library. This gave me confidence that this was the right approach going forward.
Chomping away (or building a chomp compiler)
It was now time to actually try and build something that used my version of kauai. Nearly all 3D Movie Maker data resources are stored as “chunks” in a series of .chk files which are compiled from the equivalent .cht files. These .cht files contain links to resources such as graphics, sounds, strings along with a very simple scripting language.
As part of the 3D Movie Maker build process, a binary called chomp is generated which
identifies itself as the “Microsoft (R) Chunky File Compiler”, and is then used to process
the .cht input files into binary .chk files. This was a good testing ground for my kauai
implementation since it would prove that I could actually compile and link against it, and
of course generate the binary .chk files required to start up 3D Movie Maker.
To make things a little more fun, the .cht files were themselves generated from .cht.i files
that were run through the C pre-processor as part of the studio-chomp-chunks target. When
trying to execute this target on gcc, the command line launching gcc in pre-processor mode was
failing due to requiring command line arguments compared with MSVC. After a bit of
experimentation I was able to devise the correct arguments and then add the relevant ${is-*}
generators in TargetChompSources.cmake so that it would run on all of MSVC, clang and gcc.
With a few more minor changes (mostly replacing Windows-specific types with stdint.h types),
I was able to load and run chomp on Linux for the first time. But of course as soon as I
gave it a filename, it segfaulted immediately. This turned out to be fairly easy to fix:
the command line options parser assumed that anything beginning with ‘/’ was an option,
which is fine for Windows but not for Linux when using absolute paths.
Now this was fixed, I tried again… and this time I hit a RawRtn() assert. This was
promising because in my kauai implementation, I had stubbed any unimplemented routines with
RawRtn() to make it obvious I had to fill them in at a later date. Looking at the
backtrace I could see that I was somewhere in fileposix.cpp attempting to open a file!
So it was time to implement the required file routines.
Over the period of a couple of weeks, I started to fill in the missing routines in both
fileposix.cpp and fniposix.cpp, borrowing heavily from the Windows implementation
and using the VS Studio debugger to ensure that my implementations were working in the
same way. I added in support for LF-only endings in the source .cht source files, and then
continued to add in the missing file routines as I hit the RawRtn() asserts.
One slight problem was that Linux does not have a native API for reading BMP files unlike Windows, but fortunately the BMP format consists of a simple header followed by the bitmap data. I did consider whether it was worth bringing in an external library to handle BMP files, but when attempting to compile the Windows code it looked as if things would just work as long as the struct headers were available. I then came across the libBMpp project which was also MIT-licensed like 3D Movie Maker with its own versions of these structs, but with slightly different field names(!). Fortunately I was able to fix them up using the Wikipedia article on the BMP format to map between the two, and that gave me a working BMP reader!
At this point I could get chomp to start reading files, but it would segfault in the lexer
when parsing certain lines. After some more debugging the problem turned out to be that in
order to provide source file references, the lexer implementation used in chomp would
attempt to parse any lines beginning #line generated by the MSVC C pre-processor to track
source line numbers. The line markers generated by MSVC looked like this:
#line <num> <filename>
whereas the line markers generated by gcc looked like this:
# <n> <filename> <flags>
The solution was to adapt the parser to detect whether MSVC pre-processor output or gcc pre-processor output was being generated, and to parse the line number and flags accordingly. And with this fixed… succcess! I could now generate the required .chk files on Linux ready for use with 3D Movie Maker.
Building and linking 3dmovie (hello, McZee!)
With everything working as it should, it was now apparent that I was fairly close to being
able to link 3dmovie on Linux. Fixing up the remaining build errors was reasonably
straightforward: replace more Windows-specific types with stdint.h, fix up a few bits of
#ifdef-fery, and implement any obviously missing routines. And then it built! Here in
front of me was the first 3D Movie Maker binary that could run on a non-Windows platform.
I crossed my fingers and started it up.
And… it segfaulted fairly early on. The backtrace pointed at a set of UTF-8 conversion functions in the STN class with which I wasn’t familiar when attempting to locate various directories. After a brief conversation with Ben, it transpired that these functions had been introduced as part of his work to add support for Unicode builds based upon analysis of the Japanese version of 3D Movie Maker.
Fortunately the assert() was being generated by our old friend RawRtn() which Ben had
added to indicate the STN::GetUtf8Sz() and STN::SetUtf8Sz() indicating that the failure
was due to the functions not being implemented. To the best of my knowledge, there was a
strong assumption in 3D Movie Maker that ANSI (i.e. non-Unicode) builds of 3D Movie Maker
used WIN-1252 encoding for strings. So I put together a simple set of conversion functions
using iconv and then tried again.
Almost at once I was greeted by the “Microsoft Home” splash screen, and after a small delay I found myself staring in amazement at the main menu once again. This was working much better than expected, so I went through a few more options: clicking “Go to Imaginopolis” took me to an animation with McZee flying through Imaginopolis on a rollercoaster and eventually ending up with the option to create or watch a movie. Certainly the 3D Movie Maker team deserves a huge shout out for designing the software in such a way using kauai so that porting to other platforms would be possible.
Clicking around the software showed that most things were working really well. Of course the
one thing I really wanted to do was create a new movie, to see if I could improve on my
efforts from all those years ago. However trying to browse any scenes or actors resulted in
a crash so it was time to dust off gdb once again. After a bit of poking around the cause
became quite clear: about 3 years ago in the 3DMMForever project, there were a couple of
commits that converted all of the filenames in the repository to lower case. Since Windows
filesystems are case insensitive, it didn’t matter that the filenames in the application
were no longer in upper-case, but it did matter when running on a Linux filesystem. This led
to a follow-up series that converted all the filenames in the .cht files to lower case
(along with a few minor tweaks to the fniposix.cpp and fileposix.cpp implementations)
which was enough to allow me to start editing and playing my own movies on Linux!
Native File Dialogs and miniaudio sound
Meanwhile whilst all this had been going on, Ben had been continuing his work with the SDL backend and figured out that at least 2 important components were missing for non-Windows platforms: 1) file dialogs and 2) a working audio implementation. Due to the way in which Ben had implemented the SDL backend, it was possible to use it for a Windows build which allowed him to continue working whilst I was knee-deep in the Linux side.
3D Mover Make made use of custom file dialogs built from Windows resources which would not work on non-Windows platforms, so Ben managed to put together a working implementation using Native File Dialog Extended that allowed simple file dialogs to be generated across all out platforms of interest (Windows, Linux and macOS).
The audio implementation turned out to be a bit trickier: Ben had put together a prototype using SDL_Mixer, but this revealed 2 problems that were not easy to solve. First SDL_Mixer required loading the entire audio track into memory before playback (which was not suitable for streaming MIDI events, and second there didn’t seem to be an easy way for users to record their own audio for use within 3D Movie Maker.
Ben then suggested miniaudio as an alternative, and set about producing a prototype to see how it compared with the SDL_Mixer implementation. After a few weeks it was working well enough for Ben to share it with me, and with working sound 3D Movie Maker on Linux suddenly came to life! I had quite a lot of fun playing around the studio and playing some of the included sample movies, often being amazed by the originality shown by their creators.
With a miniaudio backend up and running, Ben then proceeded to add the ability to record sounds from a microphone, since what could be more fun than recording your own movie sound effects?
Adding a MIDI player using FluidSynth
Now that the miniaudio backend was working, there was one important part of the 3D Movie Maker experience that was missing: MIDI background music! In my original proposal to Ben the previous year, I’d suggested using FluidSynth to generate the background music so off I went and started reading through the API docs to work out what was possible.
From what I could see most of the tutorials involved using FluidSynth’s in-built audio driver to play the sounds, but that didn’t work here for 2 reasons: firstly when playing a MIDI file in this way a second audio source (along with its own volume) would appear in pipewire, and secondly at least here on Debian, FluidSynth defaults to using the alsa driver, even when pipewire is installed. The effect of this is that after the first MIDI file is played, all subsequent attempts to play a sound in the login session would freeze until the current user logged out and then logged back in again. Not good.
Digging into the 3DMMEx MIDI code showed that there were actually 2 different MIDI backend implementations possible on Windows: WMS and OMS. But what was the difference? Once again Ben was able to provide the answer in that WMS was intended for Windows 95 whilst OMS was intended for Windows NT. The WMS code appeared to delegate the processing of the event stream to the MCI (Media Control Interface) API, whilst the OMS code spins up a separate thread and uses that to generate a set of MIDI events at the appropriate time.
Looking at the FluidSynth API (and of course various examples found with online search), I realised that it should be possible to re-use the OMS implementation to generate a set of real-time MIDI events that could be pushed into a separate FluidSynth rendering thread. This rendering thread would simply ask FluidSynth to render a set of frames, and then push them directly to a separate miniaudio sound.
My initial attempt to get this working was fairly conservative in that I did a minimal
refactoring to split the WMS and OMS implementations out of mididev2.cpp with the aim of
coming up with a separate implementation for FluidSynth. At this point I realised I didn’t
have a way of submitting audio frames to a separate channel in miniaudio, but the docs
indicated that it was possible to set up a ringbuffer linked to a separate sound that
would be ideal for handling a continuous stream of audio data. What was missing from the
3DMMEx miniaudio backend was a way to setup the buffer and submit audio frames to it.
I shared my prototype using the FluidSynth in-built audio driver with Ben, who once again came to the rescue! A short time later Ben shared a new branch which contained a new miniaudio stream class, along with a method to submit audio frames into the ringbuffer. Fortunately the prototype only needed a little bit of further tweaking, and with that I could now play all the sample movies with background music routed through miniaudio.
One last observation from Ben when reviewing the FluidSynth changes was that the differences between the Windows and FluidSynth implementations could be reduced by refactoring the WMS and OMS implementations such that they both derived from a common Windows MIDI stream class. During the next week Ben shared a branch with the refactored code which looked great, so I duly rebased my work (including some simplification) on top and with that the MIDI background music implementation was complete.
Adding a video player using gstreamer
Whilst the majority of 3D Movie Maker’s cut scenes are encoded as resources in the .chk
files, there are a number of them that are supplied as separate AVI files and played on
top of the 3D Movie Maker window. It isn’t known exactly why this is the case, but I can
take a guess from my work on 3DMMEx that encoding the .chk files would take quite a bit of
time on 90s PC hardware, and my feeling is that as deadlines drew closer there just wasn’t
the time available to keep running chomp.
In particular no port of 3D Movie Maker would be complete without the main cut scene when selecting “Go To Imaginopolis” where McZee introduces himself, rides a trolley into Imaginopolis, and then turns into a superhero and a cheesecake before arriving at the theatre.
When it comes to playing video in the open soure world there are two popular solutions: ffmpeg and gstreamer. But which would be the best one to use for 3DMMEx? After a bit of research I came to the conclusion that either would work well enough for our purposes, but what eventually tipped the balance to gstreamer was that I found a couple of examples that already looked fairly close to what I was trying to do. In particular they showed how both video and audio buffers could be decoded in a separate thread, and how the raw data could be accessed to be sent separately to miniaudio and the SDL backend (without using any of the in-built gstreamer drivers).
Fortunately after the work that went into the FluidSynth implementation, I had a fairly good idea how to set up a separate rendering thread, but there weren’t many examples online that showed how to extract the audio frames from the stream and process them along with the video stream.
The solution was to use a uridecodebin pipeline in conjunction with a named element to allow the video and audio frames to be delivered to separate appsinks. Each appsink callback pulls the sample, and then notifies the rendering thread causing it to read the sample buffer and either copy it to the display surface or send it to miniaudio as appropriate.
Eventually after enough tinkering, I was able to play the initial cut scene where McZee introduces himself. At this point I was quite excited that it was all working, and so pushed a branch for Ben to look at and do some testing. The next day Ben reported back that whilst the sound was working fine, the video always displayed as a black rectangle. Hmmm. I then spent a couple of evenings trying to reproduce this locally to figure out what was going on, my best guess being that it was somehow related to the window manager configuration, or even X vs. Wayland. Despite my best efforts I was unable to reproduce the issue.
After another discussion thread with Ben, we both started converging on the idea that the problem we were seeing was due to SDL not being completely thread-safe. In order to test this theory, I borrowed an idea from the GVDS (Video Stream) class which works by registering a command handler that runs upon every execution of the main event loop. I then changed the rendering thread so that instead of attempting to draw on the SDL surface, it copies the data to a buffer and then sets a variable to indicate to the main event loop handler that it should draw onto the SDL surface.
The change worked well locally, so I pushed a branch for Ben to review and… success! Ben reported back that this solved the issue, and the video was now being displayed correctly. And so that was it: after all this work, we now had a fully featured port of 3D Movie Maker running on Linux.
Summary
Porting 3D Movie Maker to run on Linux has been one of the most challenging and yet most fun projects I have worked on. If you want to see the final set of commits required to implement Linux support in 3DMMEx then you can find the full set of 17(!) PRs on the 3DMMEx GitHub site. I highly recommend looking at them if you are interested in the low-level internals of 3D Movie Maker, since a lot of topics in this write-up have been glossed over to keep it readable.
I can’t thank @benstone enough for their support and help getting this working: without Ben’s knowledge of the codebase, reviews, and new SDL backend and audio implementations then it is unlikely I would have managed to complete it. I think this project shows how small teams can do amazing things: two people at opposite ends of the globe working together over a period of about 15 months to help bring 3D Movie Maker to an even wider non-Windows audience.
Finally I have to of course acknowledge the huge amount of work done by others that enabled us to complete this project:
-
All the Microsoft employees who worked on the 3D Movie Maker (Socrates) project
-
Alice Averlong (formerly Foone Turing) for persuading Microsoft to open source 3D Movie Maker, and to Argonaut’s Jez San for agreeing the same for BRender. Also a shout out to Scott Hanselman for making everything happen at the Microsoft end.
-
All the developers of the 3DMMForever project upon which 3DMMEx was built
-
@prettytofugirl for their port of 3DMM-BRender which made the cross-platform rendering possible
-
@btzy for the Native File Dialog Extended project
-
The FluidSynth project and developers
-
The gstreamer project and developers
Now that this work is complete, we are reaching out to other people interested in preserving 3D Movie Maker for future generations. In particular we’re looking to see if anyone can now build on top of our work to build 3DMMEx on macOS, and help with building Linux deb/rpm packages so that my dream of running 3D Movie Maker on a Rasperry Pi to share with my 2 young boys can be realised :)
And for the future? Who knows, but if you are reading this and interested then please reach out and let’s see where this goes!
