A C++ library for building emulator frontends on macOS. Provides GPU-accelerated graphics (2D and 3D), audio playback with frame-based synchronization, and gamepad input handling.
| Platform | Status |
|---|---|
| macOS (Metal) | Supported |
| Linux (Vulkan) | Planned — SPIR-V shaders not yet embedded |
| Windows (DirectX/Vulkan) | Planned — SPIR-V shaders not yet embedded |
The Renderer class currently embeds MSL shaders only. Display, Audio, and Input use SDL3 abstractions and are portable, but a full cross-platform build requires pre-compiled SPIR-V bytecode to be added for Vulkan. Contributions welcome.
-
GPU-Accelerated Rendering
- Display: Simple 2D framebuffer renderer for pixel-based systems (NES, SNES, Genesis, etc.)
- Renderer: Advanced 3D renderer for transformed geometry and textured draws (PS1, N64 style) — macOS only for now (see platform support above)
- Built on SDL3 GPU API
-
High-Quality Audio
- Frame-based audio synchronization for emulation accuracy
- Real-time audio resampling (libsoxr)
- Dynamic Rate Correction (DRC) to handle clock drift
-
Input Handling
- Gamepad/joystick support via SDL3
- Keyboard fallback (arrow keys + ZXRS)
- C++17 compiler (GCC, Clang, MSVC)
- SDL3 — graphics, audio, input
- libsoxr — audio resampling
- CMake 3.16+ — recommended build system for integration
- pkg-config — used by the Makefile build
# Install dependencies via Homebrew
brew install sdl3 libsoxr pkg-config xcode-select
# (If needed) Install Xcode Command Line Tools
xcode-select --install
# Build library and example
makesudo apt update
sudo apt install build-essential pkg-config libsdl3-dev libsoxr-dev
make# Via MSYS2/MinGW-w64
pacman -S mingw-w64-x86_64-sdl3 mingw-w64-x86_64-libsoxr
makeNote: On Linux and Windows,
Display,Audio,Input, andResamplerall work.Rendererwill throw at runtime on non-Metal backends until SPIR-V shader support is added (see platform support).
# Build everything (static lib, dynamic lib, example)
make
# Build just the static library
make static
# Build the example binary
make example
# Run the example
./build/example
# Clean build artifacts
make cleanInstall the library and headers system-wide (defaults to /usr/local):
make install # installs to /usr/local
make install PREFIX=~/.local # installs to a custom prefixThis copies libraries to $(PREFIX)/lib/ and headers to $(PREFIX)/include/phosphene/.
Override variables on the command line:
# Use a different compiler
make CXX=clang++
# Build with debug symbols
make CXXFLAGS="-std=c++17 -Wall -Wextra -g"
# See all detected settings
make infoPhosphene uses CMake and exposes the Phosphene::phosphene target. There are two integration paths depending on whether you want to vendor the source or install it system-wide.
No install step required — CMake builds Phosphene as part of your project.
git submodule add https://github.com/ooPo/Phosphene vendor/phospheneIn your CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(MyEmulator)
add_subdirectory(vendor/phosphene)
add_executable(my_emu main.cpp)
target_link_libraries(my_emu PRIVATE Phosphene::phosphene)SDL3 and libsoxr must be installed on the build machine (see Installation).
Build and install Phosphene to a prefix, then use find_package from any project.
cmake -S path/to/phosphene -B phosphene_build
cmake --install phosphene_build --prefix /usr/local # or any prefixIn your CMakeLists.txt:
find_package(Phosphene REQUIRED)
add_executable(my_emu main.cpp)
target_link_libraries(my_emu PRIVATE Phosphene::phosphene)If you installed to a custom prefix, point CMake to it:
cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/prefixThe Phosphene::phosphene target propagates everything automatically — include paths, link libraries, and the C++17 requirement. No manual include_directories or target_compile_options calls are needed.
Your source files include headers as:
#include <phosphene/window.h>
#include <phosphene/display.h>
#include <phosphene/audio.h>
#include <phosphene/resampler.h>
#include <phosphene/input.h>.
├── include/
│ └── phosphene/ # Public headers
│ ├── window.h
│ ├── display.h
│ ├── renderer.h
│ ├── audio.h
│ ├── resampler.h
│ ├── input.h
│ ├── math3d.h
│ └── span.h
├── src/ # Implementation
│ ├── window.cpp
│ ├── display.cpp
│ ├── renderer.cpp
│ ├── audio.cpp
│ ├── resampler.cpp
│ ├── input.cpp
│ └── math3d.cpp
├── examples/
│ ├── basic/ # Colour-cycling 2D display + audio + DRC
│ ├── cube/ # Untextured spinning cube
│ ├── textured_cube/ # Textured spinning cube
│ └── hud/ # 3D scene with alpha-blended HUD overlay
├── build/ # Build output (generated)
├── Makefile
└── README.md # This file
#include <phosphene/window.h>
#include <phosphene/display.h>
#include <phosphene/audio.h>
#include <phosphene/input.h>
int main() {
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD);
// Initialize context
Window ctx;
ctx.init("My Emulator", 512, 480);
// Initialize components
Display video;
video.init(ctx, 256, 240); // NES framebuffer size
Audio audio;
audio.init(44100, 1, 60.0); // 44.1 kHz mono @ 60 FPS
Input input;
input.init();
// Main loop
bool running = true;
while (running) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_QUIT)
running = false;
input.handle_event(event);
}
// Get input
InputState state = input.read();
// ... use state.up, state.down, state.a, state.b, etc.
// Frame pacing
audio.wait_for_frame();
// Generate and present framebuffer
uint32_t framebuffer[256 * 240];
// ... fill framebuffer with emulated output
Framebuffer fb = { framebuffer, 256, 240 };
video.present(fb);
// Generate and push audio samples
float samples[1024];
// ... fill with emulated audio
audio.push(samples, 1024);
}
input.shutdown();
audio.shutdown();
video.shutdown();
ctx.shutdown();
SDL_Quit();
return 0;
}Manages SDL3 GPU device and window lifecycle. Initialize once at startup.
Methods:
init(title, width, height)— Create window and GPU devicegpu()— Get GPU device handlewindow()— Get window handleshutdown()— Clean up resources
Simple 2D renderer for pixel-based emulators.
Methods:
init(ctx, fb_width, fb_height)— Initialize with framebuffer dimensionspresent(framebuffer)— Upload and display framebuffer each frameshutdown()— Clean up GPU texture
Usage:
Framebuffer fb = { pixel_data, width, height };
video.present(fb);Advanced 3D renderer for transformed geometry.
Methods:
init(ctx, render_width, render_height)— Set internal render resolutionbegin_frame()— Start a new render passsubmit_draw(cmd)— Queue a draw callend_frame()— Flush and presentcreate_texture(width, height)— Allocate GPU texturedestroy_texture(tex)— Release texture
Frame-synchronized audio playback with resampling support.
Methods:
init(sample_rate, channels, emulated_fps)— Initialize devicepush(samples, count)— Enqueue audio sampleswait_for_frame()— Block until audio buffer ready for next framecompute_drc_rate(resampler)— Compute DRC rate adjustmentshutdown()— Clean up device
High-quality sample rate conversion (libsoxr wrapper).
Methods:
init(in_rate, out_rate, channels)— Initialize resamplerprocess(in, in_count, out, out_capacity)— Resample audioset_out_rate(new_rate)— Update output rate (for DRC)shutdown()— Clean up
Gamepad and keyboard input handling.
Methods:
init()— Initialize input systemhandle_event(event)— Process SDL eventsread()— Get current button stateshutdown()— Clean up
Input Mappings:
- Gamepad: D-pad, A/B buttons, Select, Start
- Keyboard: Arrow keys, Z (A), X (B), Right Shift (Select), Enter (Start)
- Non-owning pointers:
DisplayandRendererhold non-owning references toWindow - Forward declarations: Headers minimize coupling (e.g.,
Resamplerforward-declared inAudio) - Exception safety: Initialize/shutdown pattern with cleanup on error
- GPU resource management: SDL3 GPU API handles memory; you call
destroy_texture()for manual releases
- Frame pacing:
audio.wait_for_frame()blocks until the audio buffer reaches a target level, providing natural frame pacing - DRC:
audio.compute_drc_rate()adjusts the resampler output rate to keep audio synchronized with video - Nearest-neighbour filtering:
Displayuses nearest-neighbour scaling for authentic retro appearance - Depth testing:
Rendererincludes Z-buffering for correct 3D rendering
Four examples are included, each targeting a different feature set:
| Example | What it shows |
|---|---|
basic |
2D colour-cycling display, sine audio, DRC, window scaling |
cube |
Untextured spinning cube with the 3D renderer |
textured_cube |
Textured spinning cube with checkerboard texture upload |
hud |
3D scene with an alpha-blended HUD overlay composited on top |
Build and run with the Makefile:
make example # builds and links examples/basic
./build/example
make # builds all targets including cube, textured_cube, hud
./build/cube
./build/textured_cube
./build/hudOr via CMake with -DPHOSPHENE_BUILD_EXAMPLES=ON.
When adding new features:
- Update relevant headers with Doxygen-style documentation
- Maintain consistent code style (see existing files)
- Test on supported platforms
- Update README if API changes
BSD 2-Clause. See LICENSE.txt.
Compilation errors for SDL3?
- Verify SDL3 is installed:
pkg-config --cflags sdl3 - Try
make infoto see detected paths - On macOS:
brew install sdl3 - On Linux:
sudo apt install libsdl3-dev
Audio not playing?
- Check system volume and audio device
- Verify
soxr_create()succeeds: run the example and look for errors - Ensure output sample rate matches hardware
Example window not appearing?
- On Linux/Wayland, SDL3 may require additional environment variables
- Try
SDL_VIDEODRIVER=x11 ./build/exampleon Linux