Skip to content

OpenRakis/Spice86

Spice86 - A PC emulator for real mode reverse engineering

Linux macOS Windows .NET Build NuGet Licence downloads

Spice86 is a tool to execute, reverse engineer and rewrite real mode DOS programs for which source code is not available.

Releases are available on Nuget.

Pre-releases are also available on the Release page

NOTE: This is a port, and a continuation from the original Java Spice86.

It requires .NET 10 and runs on Windows, macOS, and Linux.

Approach

Rewriting a program from only the binary is a hard task.

Spice86 is a tool that helps you do so with a methodic divide and conquer approach.

General process:

  • You start by emulating the program in the Spice86 emulator.
  • As the program runs, Spice86 builds a Control Flow Graph (CFG) of the executed code.
  • At the end of each run, the emulator dumps some runtime data (memory dump, execution flow and the CFG).
  • From that CFG, Spice86 directly generates a self-contained, runnable C# project that overrides the original assembly with equivalent C# code.
  • You then gradually rewrite the generated C# functions, replacing the mechanical translation with readable, intentful code (a task an LLM can help with).
  • This is helpful because:
    • Small sequences of assembly can be statically analyzed and are generally easy to translate to a higher level language.
    • You work all the time with a fully working version of the program so it is relatively easy to catch mistakes early.
    • Rewriting code function by function allows you to discover the intent of the author.

Spice86 also dumps Ghidra-compatible symbols, so you can optionally load the memory dump into Ghidra via the spice86-ghidra-plugin for deeper static analysis. This is no longer required to produce C# overrides; it is just an extra analysis aid.

Running your exe

Command Description
Spice86 -e file.exe Run the specified executable
Spice86 -e file.com Run a COM file
Spice86 -e file.bat Run a DOS batch file
Spice86 -e file.bin Run a BIOS file

Dumping data

Setting Description
Environment Variable SPICE86_DUMPS_FOLDER - Set this to control the base directory where data is dumped
Command Line Use --RecordedDataDirectory to specify the base dump location
Default Location Current directory if neither of the above is specified

Note: Regardless of the base directory setting, dumps are always placed in a subdirectory named with the program's SHA-256 hash. This ensures that multiple executables from the same game (e.g., SETUP.EXE, GAME.EXE) have isolated dump folders.

The emulator dumps the following files:

  • spice86dumpMemoryDump.bin: Snapshot of the real mode address space
  • spice86dumpExecutionFlow.json: Contains function addresses, labels, and executed instructions
  • spice86dumpGhidraSymbols.txt: Ghidra-compatible symbols (function and label addresses), for optional import into Ghidra
  • spice86dumpCfgBlocks.json: The explored CFG as basic blocks with execution-context metadata
  • spice86dumpCfgPartitions.json: The recovered function/helper partitions and inter-partition transfers, derived from the block graph
  • spice86dumpCfgGeneratedOverrides.cs: C# overrides generated from the explored CFG
  • GeneratedProject/: A self-contained, runnable C# override project (see Generated C# override project)

When there is already data in the specified location, the emulator will load it first and complement it.

CFG graph reload: The CFG instruction graph is also dumped (spice86dumpCfgReload.json) and, by default, reloaded from the recorded data directory at startup so previously explored program structure is preserved across runs. Disable with --ReloadCfgGraph false. It is a no-op if the file is absent.

Code overrides: When code overrides are active (--UseCodeOverride true with an --OverrideSupplierClassName), the CFG and execution flow dumps (spice86dumpExecutionFlow.json, spice86dumpCfgBlocks.json, spice86dumpCfgPartitions.json, spice86dumpCfgReload.json) are not written. Overrides replace the emulated asm, so the recorded graph would jump between overrides and no longer reflect the program.

CPU Heavy Logging

For detailed debugging, you can enable CPU heavy logging which records every executed instruction to a file.

⚠️ Warning: This feature has a significant performance impact and should only be used for debugging purposes.

Setting Description
--CpuHeavyLog Enables CPU heavy logging (default: false)
--CpuHeavyLogDumpFile Custom path for the log file (optional). If set, will enable CpuHeavyLog as well. Default Location: {DumpDirectory}/cpu_heavy.log
--CpuHeavyLogExpressions Named expressions appended to every CPU heavy log line. Repeat the option for each expression to append (for example: --CpuHeavyLogExpressions "life=AX+1" --CpuHeavyLogExpressions "myvalue=word ptr DS:[0x456]"). Only active when --CpuHeavyLog is true.

Log Format: Each line contains: SegmentedAddress InstructionString

Example:

017D:0000 mov AX,0xDD1D
017D:0003 call near 0xE4AD
017D:E4AD mov SI,0x0080

Usage:

# Enable with default location
Spice86 -e program.exe --CpuHeavyLog

# Use custom log file path
Spice86 -e program.exe --CpuHeavyLog --CpuHeavyLogDumpFile "C:\logs\cpu.log"

# Append named expressions to each log line (example)
Spice86 -e program.exe --CpuHeavyLog \
  --CpuHeavyLogExpressions "life=AX+1" \
  --CpuHeavyLogExpressions "myvalue=word ptr DS:[0x456]"

Command line options

  --Debug                            (Default: false) Starts the program paused and pauses once again when stopping.
  --Cycles                           (Default: null) Target CPU cycles per ms, for the rare speed sensitive game. 3000 by default. Overrides Instructions per second option below if used.
  --Xms                              (Default: true) Enables 15 MB of XMS memory.
  --Ems                              (Default: true) Enables EMS memory. EMS adds 8 MB of memory accessible to DOS programs through the EMM Page Frame.
  --A20Gate                          (Default: false) Disables the 20th address line to support programs relying on the rollover of memory addresses above the HMA (slightly above 1 MB).
  -m, --Mt32RomsPath                 Zip file or directory containing the MT-32 ROM files
  -c, --CDrive                       Path to C drive, default is exe parent
  -r, --RecordedDataDirectory        Directory to dump data to when not specified otherwise. If blank dumps to SPICE86_DUMPS_FOLDER, and if not defined dumps to a sub directory named with the program SHA 256 signature
  -e, --Exe                          Required. Path to executable
  -a, --ExeArgs                      List of parameters to give to the emulated program
  -x, --ExpectedChecksum             Hexadecimal string representing the expected SHA256 checksum of the emulated program
  -f, --FailOnUnhandledPort          (Default: false) If true, will fail when encountering an unhandled IO port. Useful to check for unimplemented hardware. false by default.
  -g, --GdbPort                      GDB port. If 0, GDB server will be disabled. Default is 10000.
  -o, --OverrideSupplierClassName    Name of a class that will generate the initial function information. See documentation for more information.
  -p, --ProgramEntryPointSegment     (Default: 4096) Segment where to load the program. DOS PSP and MCB will be created before it.
  -u, --UseCodeOverride              (Default: true) <true or false> if false it will use the names provided by overrideSupplierClassName but not the code
  -i, --InstructionTimeScale        <number of instructions that have to be executed by the emulator to consider a second passed> if blank will use time based timer.
  --ClockJitterSeed <CLOCKJITTERSEED> Optional integer seed enabling small deterministic clock jitter (±0.01 ms). Omit to disable.
  --ClockStartTime <CLOCKSTARTTIME> Optional UTC start date/time for the emulated clock (parseable by DateTime.Parse, e.g. 1993-06-01T00:00:00Z). When omitted defaults to the current UTC time.
  -t, --TimeMultiplier               (Default: 1) <time multiplier> if >1 will go faster, if <1 will go slower.
  -h, --HeadlessMode [Mode]          (Default: false) Headless mode. The mode 'Minimal' does not use any UI components, 'Avalonia' uses the full UI and consumes a bit more memory.
  -l, --VerboseLogs                  (Default: false) Enable verbose level logs
  -w, --WarningLogs                  (Default: false) Enable warning level logs
  -s, --SilencedLogs                 (Default: false) Disable all logs
  -i, --InitializeDOS                (Default: true) Install DOS interrupt vectors or not.
  --CpuHeavyLog                      (Default: false) Enable CPU heavy logging. Logs every executed instruction to a file. Warning: significant performance impact.
  --CpuHeavyLogDumpFile              Custom file path for CPU heavy log output. If not specified, defaults to {DumpDirectory}/cpu_heavy.log
  --CpuHeavyLogExpressions           (Default: none) Named expressions appended to every CPU heavy log line. Repeat the option for each expression to append (for example: --CpuHeavyLogExpressions "life=AX+1"). Only active when --CpuHeavyLog is true.
  --ReloadCfgGraph                   (Default: true) Reload the previously dumped CFG instruction graph (spice86dumpCfgReload.json) at startup so explored program structure is preserved across runs. No-op if the file is absent.
  --AsmRenderingStyle                Style of the ASM rendering. Spice86 or DosBox.
  --StructureFile                    Path to a C header file that describes the structures in the application. Works best with exports from IDA or Ghidra
  --mcp-http-port                    (Default: 8081) Port for the MCP HTTP server
  --RenderingMode                    (Default: Async) Selects the VGA rendering mode. Sync fires VGA events on the emulation thread for determinism; The default mode is for performance.
   --JitMode                         (Default: InterpretedThenCompiled) Controls how the JIT compiler handles instruction execution delegates.
   --AllowIvtAddress0                (Default: false) Controls whether an INT instruction whose IVT entry is 0:0 is treated as valid.
  --version                          Display version information.

Sound Blaster and OPL/Adlib Gold Options

  • SbType: Sound Blaster card type. Values: None, SB1, SB2, SBPro1, SBPro2, Sb16, GameBlaster, AdlibGold.
  • SbIrq: Sound Blaster IRQ line. Default is 7. Common values: 5, 7, 9, 10.
  • SbDma: Sound Blaster 8-bit DMA channel. Default is 1. Common values: 0, 1, 3.
  • SbHdma: Sound Blaster 16-bit high DMA channel. Default is 5. Common values: 5, 6, 7.
  • SbBase: Sound Blaster base I/O address (hex). Default is 0x220. Common values: 0x220, 0x240, 0x260, 0x280.
  • OplMode: OPL synthesis mode. Values: None, Opl2, DualOpl2, Opl3, Opl3Gold. Default is Opl3.
  • SbMixer: Enable Sound Blaster mixer control of OPL voices. Default is true.

Dynamic analysis

Spice86 speaks the GDB remote protocol:

  • it supports most of the commands you need to debug.
  • it also provides custom GDB commands to do dynamic analysis.

Connecting to GDB

The GDB server is always started along with the program to execute unless option is set to 0. Default port is 10000.

If you want to pause before starting execution to setup breakpoints and so on, use the --Debug option.

Here is how to connect from GDB command line client and how to set the architecture:

(gdb) target remote localhost:10000
(gdb) set architecture i8086

Vanilla GDB

You can add breakpoints, step, view memory and so on.

Example with a breakpoint on VGA VRAM writes:

(gdb) watch *0xA0000

Viewing assembly:

(gdb) layout asm

Removing a breakpoint:

(gdb) remove 1

Searching for a sequence of bytes in memory (start address 0, length F0000, ascii bytes of 'Spice86' string):

(gdb) find /b 0x0, 0xF0000, 0x53, 0x70, 0x69, 0x63, 0x65, 0x38, 0x36

GDB does not support x86 real mode segmented addressing, so pointers need to refer to the actual physical address in memory. VRAM at address A000:0000 would be 0xA0000 in GDB.

Similarly, The $pc variable in GDB will be exposed by Spice86 as the physical address pointed by CS:IP.

Custom GDB commands (where the magic happens)

The list of custom commands can be displayed like this:

(gdb) monitor help

Dump information about current run

(gdb) monitor dumpall

Dumps everything described below in one shot. Files are created in the dump folder as explained here Several files are produced:

  • spice86dumpMemoryDump.bin: Snapshot of the real mode address space. Contains the instructions that are actually loaded and executed. They may differ from the exe you are running because DOS programs can rewrite some of their instructions / load additional modules in memory.
  • spice86dumpExecutionFlow.json: Contains information about the run such as addresses of the functions, the labels, and the instructions that have been executed. Also consumed by the optional spice86-ghidra-plugin.
  • spice86dumpCfgBlocks.json and spice86dumpCfgPartitions.json: the explored CFG as basic blocks, and the function/helper partitions plus transfers derived from those blocks.
  • spice86dumpCfgGeneratedOverrides.cs and GeneratedProject/: the C# overrides and runnable project generated from the explored CFG (see Generated C# override project).

Special breakpoints

Break after x emulated CPU Cycles:

(gdb) monitor breakCycles 1000

Break at the end of the emulated program:

(gdb) monitor breakStop

Seer

For a pleasing and productive experience with GDB, the seerGDB client is highly recommended.

At best, use the configuration file spice86.seer provided in the doc directory (here): run Seer with seergdb --project spice86.seer.

If you use a different port for gdb, adjust spice86.seer correspondingly.

Also, while in Seer, set Settings/Configuration/Assembly/Disassembly Mode to “Length”, otherwise the Assembly View won't work.

Emulator features

Component Support Level
CPU 8086/8088 fully implemented and tested
80186 (BOUND instruction missing)
80286 (protected mode not implemented)
80386 (partial support, protected mode not implemented)
Only 16-bit instructions fully supported
Most 32-bit instructions implemented but not fully tested
No FPU except detection instructions
Memory 1MB address space with segmented addressing
A20 Gate support
EMS 3.2 implemented
XMS 4.0 is implemented
HMA is implemented
No paging support
Graphics Text modes, VGA, EGA, and CGA implemented
EGA and CGA modes are best effort (you may find bugs)
VESA VBE 1.2 is supported
DOS Largely complete DOS and INT 21h implementation (DOS 5.0)
Input Keyboard and mouse supported
No joystick support
CD-ROM MSCDEX and CDDA support is implemented, including CD images
Floppy Floppy disk emulation is implemented, including floppy images and booting on a floppy image

Sound support

Sound Type Support Level Notes
PC Speaker ✅ Full Implemented
Adlib/SB OPL ✅ Full Ported from DOSBox Staging
SoundBlaster ✅ Full Ported from DOSBox Staging
Adlib Gold ✅ Full Ported from DOSBox Staging
MT-32 ⚠️ Partial Not available on macOS
Gravis Ultrasound ❌ None Not implemented yet
General MIDI ✅ Full Supported

Misc

Feature Details
C Drive Configurable with --CDrive, defaults to current folder
Program Arguments Pass up to 127 chars with --ExeArgs
Time Handling Real elapsed time (adjustable with --TimeMultiplier) or instruction-based timing with --InstructionTimeScale
Screen Refresh 30 FPS and on VGA retrace wait detection
Structure Viewer Requires C header file (--StructureFile) to display memory structures

Reverse engineering process

Concrete example with Cryo Dune here.

First run your program and make sure everything works fine in Spice86. If you encounter issues it could be due to unimplemented hardware / DOS / BIOS features.

When Spice86 exits, it dumps its data (memory dump, execution flow, CFG) and generates a C# override project in the dump folder (current folder, or the folder specified by the SPICE86_DUMPS_FOLDER env variable / --RecordedDataDirectory).

The generated project under GeneratedProject/ is immediately runnable and already overrides the explored assembly with equivalent C#. From there you rewrite the generated functions, function by function, to discover and document the program's intent. This is well suited to LLM assistance.

See Generated C# override project for how to run it and Overriding emulated code with C# code for the override API.

Optionally, you can also open the memory dump in Ghidra with the spice86-ghidra-plugin for additional static analysis (see Ghidra plugin).

Generated C# override project

On every dump (when not already running with overrides), Spice86 turns the explored CFG into a ready-to-run C# project, so you do not need any external tool to start rewriting the program in C#.

Two artifacts are produced in the dump folder:

  • spice86dumpCfgGeneratedOverrides.cs: the generated overrides on their own (a CfgGeneratedOverrideSupplier plus a CfgGeneratedOverrides : CSharpOverrideHelper mapping every discovered function via DefineFunction).
  • GeneratedProject/: a self-contained, runnable project wrapping those overrides:
    • Spice86.Generated.csproj references the Spice86 GUI project, so the build always matches the running emulator.
    • Program.cs forwards all your arguments to Spice86 and defaults in --OverrideSupplierClassName, --UseCodeOverride true, and the program's --ExpectedChecksum (so the overrides cannot run against a different binary).
    • CfgGeneratedOverrides.cs contains the generated overrides.

Run it like a normal .NET project, passing the path to your executable:

cd <dump folder>/GeneratedProject
dotnet run -- -e /path/to/PROGRAM.EXE

Then progressively replace the mechanically generated function bodies with your own readable C# implementations.

Note: while running with overrides active (--UseCodeOverride true), the CFG and execution-flow dumps are not rewritten, so regenerating from scratch should be done from a plain (non-override) run.

Overriding emulated code with C# code

You can provide your own C# code to override the program original assembly code. This is exactly what the generated project does for you automatically; the section below explains the API so you (or an LLM) can write or refine overrides.

Defining overrides

Spice86 can take in input an instance of Spice86.Core.Emulator.Function.IOverrideSupplier that builds a mapping between the memory address of functions and their C# overrides.

For a complete example you can check the source code of Cryogenic.

Here is a simple example of how it would look like:

namespace My.Program;

// This class is responsible for providing the overrides to spice86.
// There is only one per program you reimplement.
public class MyProgramOverrideSupplier : IOverrideSupplier {
  public IDictionary<SegmentedAddress, FunctionInformation> GenerateFunctionInformations(
      ILoggerService loggerService, Configuration configuration, ushort programStartSegment, Machine machine) {
    return new MyOverrides(new Dictionary<SegmentedAddress, FunctionInformation>(),
        machine, loggerService, configuration).FunctionInformations;
  }

  public override string ToString() {
    return "Overrides My program exe. class is " + GetType().FullName;
  }
}

// This class contains the actual overrides. As the project grows, you will probably need to split the reverse engineered code in several classes.
public class MyOverrides : CSharpOverrideHelper {
  private readonly MyOverridesGlobalsOnDs globalsOnDs;

  public MyOverrides(IDictionary<SegmentedAddress, FunctionInformation> functionInformations,
      Machine machine, ILoggerService loggerService, Configuration configuration)
      : base(functionInformations, machine, loggerService, configuration) {
    globalsOnDs = new MyOverridesGlobalsOnDs(Memory, State.SegmentRegisters);
    // IncDialogueCount47A8 will get executed instead of the assembly code when a call to 017D:A1E8 is performed.
    // The override method receives the load offset (segment where the program was loaded) as parameter.
    DefineFunction(0x017D, 0xA1E8, IncDialogueCount47A8_017D_A1E8_0C0B8);
    DefineFunction(0x017D, 0x0100, AddOneToAX_017D_0100_01FD0);
  }

  public Action IncDialogueCount47A8_017D_A1E8_0C0B8(int loadOffset) {
    // Accessing the memory via accessors
    globalsOnDs.SetDialogueCount47A8(globalsOnDs.GetDialogueCount47A8() + 1);
    // Depends on the actual return instruction performed by the function, needed to be called from the emulated code as
    // some programs like to mess with the stack ...
    return NearRet();
  }

  public Action AddOneToAX_017D_0100_01FD0(int loadOffset) {
    // Assembly for this would be
    // INC AX
    // RETN
    // Note that you can access the whole emulator to change the state in the overrides.
    State.AX++;
    return NearRet();
  }
}

// Memory accesses can be encapsulated into classes like this to give names to addresses and make the code shorter.
public class GlobalsOnDs : MemoryBasedDataStructureWithDsBaseAddress {
    public GlobalsOnDs(IByteReaderWriter memory, SegmentRegisters segmentRegisters) : base(memory, segmentRegisters) {
    }

    // Getters and Setters for address 0x1DD:0x2/0x1DD2.
    // Was accessed via the following registers: DS
    public int Get01DD_0002_Word16() {
        return UInt16[0x2];
    }

    public void Set01DD_0002_Word16(ushort value) {
        UInt16[0x2] = value;
    }

    // Getters and Setters for address 0x1138:0x0/0x11380.
    public int Get1138_0000_Word16() {
        return UInt16[0x0];
    }
}

The override method name encodes the address (Name_<segment>_<offset>_<linear>) so it can be parsed back into a FunctionInformation; pass an explicit name argument to DefineFunction if you prefer not to follow that convention.

Remember: You must tell Spice86 to use your C# code overrides with the command line argument "--UseCodeOverride true" when debugging your project, along with the mandatory path to your DOS program passed with the "-e" / "--Exe" argument.

Debugger

Spice86 comes with a built-in debugger. that can be used to debug the emulated program. It is a simple debugger that allows you to inspect the memory, the disassembly, the registers, and the stack.

Structure viewer

The structure viewer allows you to inspect the memory in a structured way. It is useful to inspect the memory as a structure, like the DOS PSP, the DOS MCB, the VGA registers, etc.

First you need a C header file that describes the structures in the application. You can generate one with Ghidra or IDA. Then you can load it with the --StructureFile commandline argument. This will enable the "Structure view" button in the Memory tab of the debugger.

There you enter a segment:offset address and choose the structure you want to view. The structure will be displayed in a tree view and the memory in a hex view.

The display updates whenever the application is paused, so you can step through the program and see how the structure changes. Exporting a new C header file from Ghidra or IDA will also update the structure viewer with the new information real-time.

You can also enter the Structure view by selecting a range of bytes in the Memory tab and right-clicking on it.

HTTP server

Spice86 includes a built-in HTTP server for quick runtime inspection and memory access.

  • The HTTP server is disabled by default. Enable it by specifying a port with --HttpApiPort.

Available endpoints:

Method Route Description
GET /api API metadata and endpoint list
GET /api/status Current emulator status (pause state, CPU state, CS:IP, cycles, memory size)
GET /api/memory/{address}/byte Read one byte at address
PUT /api/memory/{address}/byte Write one byte at address
GET /api/memory/{address}/range/{length} Read a memory range

Misc details

C Drive

It is possible to provide a C: Drive for emulated DOS functions with the option --CDrive. Default is executed program folder. For some games you may need to set the C drive to the parent folder.

Emulated program arguments

You can pass arguments (max 127 chars!) to the emulated program with the option --ExeArgs. Default is empty.

Time

The emulated Timer hardware of the PC (Intel 8259) supports measuring time from either:

  • The real elapsed time. Speed can be altered with parameter --TimeMultiplier.
  • The number of instructions the emulated CPU executed. This is the behaviour that is activated with parameter --InstructionTimeScale and is forced when in GDB mode so that you can debug with peace of mind without the timer triggering.

Compatibility list available here.

How to build on your machine

  • Install the .NET 10 SDK (once)
  • clone the repo
  • run this where Spice86.sln is located:
   dotnet build

How to run

   Spice86 -e <path to executable>

or use this where Spice86.csproj is located:

   dotnet run -e <path to executable>

if you don't want to manually build and run separately, use the helper script at the root of the repository which builds and runs the app, passing all arguments through:

   ./run.sh -e <path to executable>

Ghidra plugin

Importing the dump into Ghidra is optional. Spice86 already generates a runnable C# override project on its own (see Generated C# override project); the Ghidra plugin is only useful if you additionally want to explore the dump inside Ghidra (static analysis, labeling, or producing a --StructureFile C header for the structure viewer).

This uses Ghidra and Java 17.

Before using it, define an environnement variable named SPICE86_DUMPS_FOLDER pointing to a folder where the Spice86 dumps are located. They are generated on exit.

General procedure, in order:

1.Ghidra's own script 'ImportSymbolScript.py' (input used is "spice86dumpGhidraSymbols.txt")

2.Ghidra's Auto-Analyze (only enable 'Dissasemble Entry Points')

3.Now, you can use the plugin.

Remember: if Ghidra displays SUBROUTINES, use the 'f' key to convert them into functions.

Also, if you have any weird behaviour, make sure you have Java 17 and ONLY Java 17. That's how Ghidra likes it.

Cfg Cpu

Doc here

MCP

MCP server documentation is available in doc/mcp.md.

Some screenshots

Cryo dune:

Prince of persia:

Stunts:

Betrayal at Krondor:

Credits

The SoundBlaster and Adlib Gold implementations are fully ported from dosbox-staging, replacing the previous one which was modified from the Aeon emulator. This includes PCM and OPL sound quality improvements, far greater emulation accuracy, SB/OPL compatibility, mixer thread logic, audio events, audio hardware delays, and a complete audio re-architecture.

The NukedOpl3 port to C# was done by codeEngine. It is bit-accurate. Thanks a lot!

The DOS implementation is heavily inspired by the clean code from FreeDOS, and DOSBox Staging.

The implementations of MSCDEX, CDDA, Floppy emulation, CD images support, CD drive emulation all used DOSBox Staging as a model (even if the architecture is different), escpecially for conformance about expected behavior (ie. IOCTL).

The BIOS implementation draws heavily from SeaBIOS, IBM PC BIOS reconstructionns, and sometimes DOSBox Staging.

EMS was written with the help of the specs. XMS was written with the help of the specs, but a lot had to be rewritten by looking at the real HIMEM driver, and by reproducing the XMS tests from Microsoft.

Additionally, the project no longer relies on PortAudio. Instead, it uses a fully cross-platform C# port of the SDL2 audio APIs. We only depend on WASAPI (Windows), ALSA (Linux), or CoreAudio (macOS).

This project uses JetBrains Rider licenses, thanks to JetBrains' Open Source Community Support.

The UI is powered by Avalonia UI.

Packages

 
 
 

Contributors

Languages