Free API is an experimental C++ project that aims to reimplement a minimal subset of the Windows API (WinAPI), roughly targeting functionality available around ~1998 (Win32 era).
The goal is to provide a small, self-contained compatibility layer that allows legacy applications and games to run without depending on Microsoft Windows.
Free API provides a WinAPI-compatible subset implemented using SDL3:
WinAPI (subset ~1998)
↓
Free API
↓
SDL 3
- Free API → defines WinAPI-like headers and behavior
- SDL 3 → internal implementation detail for windowing, events, and timing
- WinAPI-like public headers (windows.h, windef.h, etc.)
- SDL3 used internally as a portable backend
- Minimalism: only what is needed is implemented
- Focus on real-world use cases (legacy games support)
- Recreate a minimal subset of WinAPI (~1998)
- Allow running legacy Win32 applications without Windows
- Provide a portable runtime layer
- Keep code simple, readable, and hackable
- Maintain cross-platform support via SDL3
WinMainabstraction (mapping tomain)
- Window creation (
CreateWindowEx) - Message loop (
PeekMessage,DispatchMessage) - Window procedures (
WNDPROC)
- Mouse motion —
SDL_EVENT_MOUSE_MOTION→WM_MOUSEMOVEwithlParam= packed(y<<16)|x - Mouse buttons —
SDL_EVENT_MOUSE_BUTTON_DOWN/UP→WM_LBUTTONDOWN/UP,WM_RBUTTONDOWN/UP,WM_MBUTTONDOWN/UP - Mouse button state —
wParamcontains liveMK_LBUTTON/MK_RBUTTON/MK_MBUTTON/MK_SHIFT/MK_CONTROLflags - Keyboard —
SDL_EVENT_KEY_DOWN/UP→WM_KEYDOWN/WM_KEYUPwithwParam=VK_*code- Letters A–Z (uppercase VK codes), digits 0–9
- Navigation:
VK_LEFT/RIGHT/UP/DOWN,VK_HOME,VK_END,VK_PRIOR,VK_NEXT,VK_INSERT,VK_DELETE - Control:
VK_RETURN,VK_ESCAPE,VK_SPACE,VK_TAB,VK_BACK,VK_SHIFT,VK_CONTROL,VK_MENU,VK_PAUSE - Function keys:
VK_F1–VK_F12(F10 usesWM_SYSKEYDOWN/WM_SYSKEYUPmatching Windows behavior)
- Text input —
SDL_EVENT_TEXT_INPUT→WM_CHAR(for name entry screens) - Window focus —
SDL_EVENT_WINDOW_FOCUS_GAINED→WM_ACTIVATEAPP(1);SDL_EVENT_WINDOW_FOCUS_LOSTis suppressed (not translated toWM_ACTIVATEAPP(0)) to prevent games from entering inactive/suspended state due to spurious desktop focus changes - Debug logging — set env
FREE_API_DEBUG_INPUT=1(aliases:FREE_API_DEBUG_MOUSE=1,FREE_API_DEBUG_REAL_INPUT=1) at runtime to log all translated messages, ENQUEUE/DISPATCH events, per-event SDLwindowIDand resolvedHWND, GetCursorPos and ScreenToClient calls.
- Mouse and keyboard events are routed to the
HWNDmatching the SDLwindowIDof the event; if no match (e.g. before window creation completes), they fall back to the active/first registered window so startup events are not silently dropped. PeekMessageAcallsSDL_PumpEvents()+PumpSdlEvents()on every invocation (not only when the queue is empty), so real OS mouse/keyboard events cannot be starved by other queued messages staying ahead in the queue. This was the root cause of "test pipeline passes but real game does not see input" reports.- The
WM_ACTIVATEAPP(0)suppression is intentional: many SDL environments deliver spuriousFOCUS_LOSTat startup or in headless/virtual environments; sending deactivation would freeze games that guard rendering/input behindg_bActive. - All input events flow through
PeekMessage→DispatchMessage→WndProc; no direct callbacks bypass the WinAPI message queue.
An automated end-to-end test (tests/test_input_pipeline.cpp) injects SDL events programmatically and verifies the full pipeline:
cmake --build build --target test_input_pipeline
./build/bin/test_input_pipeline
# Expected output: [input-pipeline-test] ALL TESTS PASSEDVerified: WM_MOUSEMOVE, WM_LBUTTONDOWN/UP, WM_RBUTTONDOWN, WM_KEYDOWN (VK_SPACE), WM_KEYUP (VK_ESCAPE).
MIDI music playback is implemented through TinySoundFont (v0.9) + TinyMidiLoader (v0.7) rendered to stereo float PCM and fed into an SDL3 audio stream.
MCI_OPEN (sequencer, musicXXX.blp)
↓
TinyMidiLoader ─ loads MIDI Type 0/1 file
↓
Mixer thread ─ advances MIDI time, sends events to TinySoundFont
↓
TinySoundFont ─ synthesises stereo float PCM using a SoundFont .sf2
↓
SDL3 audio stream ─ one shared device opened on first use
A SoundFont 2 (.sf2) file is required for audio synthesis. No SoundFont is bundled (to avoid copyright issues).
Lookup order (first found wins):
FREE_API_SOUNDFONTenvironment variableassets/soundfont/default.sf2soundfont/default.sf2
If no SoundFont is found, MCI_OPEN still succeeds but playback is silent.
A warning is printed to the SDL log:
[midi] WARNING: No SoundFont found. Music will be silent.
Set FREE_API_SOUNDFONT=/path/to/file.sf2 or place default.sf2 in assets/soundfont/ or soundfont/.
Free SoundFonts suitable for testing:
- GeneralUser GS — freely redistributable, ~30 MB
- FluidR3_GM — available in Debian/Ubuntu packages
| Command | Status |
|---|---|
MCI_OPEN with lpstrDeviceType="sequencer" |
Implemented |
MCI_PLAY with MCI_NOTIFY |
Implemented |
MCI_CLOSE |
Implemented |
MCI_SET |
Accepted, no-op |
cdaudio device type |
Gracefully declined |
| Function | Status |
|---|---|
midiOutGetNumDevs |
Implemented (returns 1 if SDL audio available) |
midiOutOpen |
Implemented (dummy handle) |
midiOutSetVolume |
Implemented (maps WinMM packed volume to linear gain) |
midiOutClose |
Implemented |
- MIDI Type 0 and Type 1 files
- Note on/off, program change, pitch bend, control change
- Channel 9 percussion
- Global volume via
midiOutSetVolume
- Looping (
MCI_PLAYwith loop flag): TODO — playback stops at end-of-song - CD audio (
cdaudio): TODO — gracefully declined - MCI_NOTIFY: posted when song ends (partial; no timeout handling)
- Concurrent music tracks: not supported (one at a time)
- Web (Emscripten): SDL3 audio may require a user gesture before audio starts
Set FREE_API_DEBUG_MIDI=1 to enable per-call logging:
FREE_API_DEBUG_MIDI=1 ./SPEEDY_BLUPI_WINDOWS| Platform | Status |
|---|---|
| Linux | Supported via SDL3 |
| Windows | Supported via SDL3 (not WinMM real MIDI) |
| macOS | Supported via SDL3 |
| Android | Supported if SoundFont is packaged in assets |
| Web (Emscripten) | Supported; may need user gesture for audio |
The following single-header libraries are vendored under external/:
external/tsf.h— TinySoundFont v0.9 by Bernhard Schelling (MIT)external/tml.h— TinyMidiLoader v0.7 by Bernhard Schelling (MIT)
timeSetEvent/timeKillEvent— implemented withSDL_AddTimer/SDL_RemoveTimer. The user-suppliedLPTIMECALLBACKfires periodically on a private SDL timer thread.- The internal WinAPI message queue (
g_messageQueue) is mutex-protected because the timer thread is allowed to callPostMessage(e.g. legacy games postingWM_TIMER/WM_UPDATEfromTimerStep). - Without this implementation, games using
MMTIMERmode (timeSetEvent(50ms, TimerStep, …, TIME_PERIODIC)) never received their tick, so they could not animate, redraw, or process input — even though raw SDL mouse/keyboard events were arriving and being translated correctly.
- Timing (
GetTickCount,Sleep) - Debugging (
OutputDebugString)
free-api/
├── include/
│ ├── windows.h
│ ├── windef.h
│ ├── winnt.h
│ └── ...
├── src/
│ └── winapi.cpp
└── CMakeLists.txt
git clone https://github.com/openeggbert/free-api.git
cd free-api
cmake -B build
cmake --build buildWork in progress
Current focus:
- Supporting
free-directrequirements - Basic windowing and message pump
- Cross-platform compatibility
This project is licensed under MIT.