oops-sdk Developer Guide & Tutorial
Practical Guide to Building Native Homebrew on Prospero with the Clean-Room OOPS SDK.
Welcome to the oops-sdk developer guide. This manual
walks you through building native, freestanding C/C++ applications for
Prospero (9th-generation) and Orbis (8th-generation) console hardware
using oops-sdk.
For exhaustive function signatures, parameters, and return codes
across all 25 subsystems, see the Complete API Reference. For porting
existing games, engine selection (oops-gl vs
oops-mesa), and dependency libraries, see the Homebrew Porting Guide.
Table of Contents
- Freestanding C Architecture
- Project Setup & Makefile Integration
- Tutorial 1: Display Output & 2D Software Drawing
- Tutorial 2: DualSense Controller Input & Feedback
- Tutorial
3: 3D Hardware Graphics (
oops-glvsoops-mesa) - Tutorial
4: Cross-Platform Development with SDL2 (
oops-sdl) - Tutorial 5: Stereo PCM Audio Output
- Tutorial 6: Dynamic Code Execution & JIT Allocation
- Tutorial 7: System Dialogs, On-Screen Keyboard & Persistent Save Data
- Tutorial
8: Standard Runtime Shimming, Directory Traversal &
oops-deps - Special & Advanced Subsystems (Escalation & PKGs)
- Hardware Safety & Clean-Room Invariants
1. Freestanding C Architecture
oops-sdk operates strictly freestanding
(-ffreestanding -nostdlib):
- Zero Vendor SDK Files: Replaces proprietary headers with mathematically verified structures and clean-room libc stubs.
- No Desktop Glibc:
obs_strlen,obs_strcmp, andobs_strncpyprovide freestanding string handling, while unprefixedmemcpy/memset/memcmp(declared in<oops/freestd.h>) cover memory operations. Debug text is emitted viaoops_klog()directly into the kernel telemetry ring. - Dynamic Linking via NIDs: The runtime resolves
platform libraries (
libkernel,libScePad,libSceAudioOut) dynamically via symbol NID hashes.
2. Project Setup & Makefile Integration
All oops-sdk applications integrate through the
canonical application Makefile
(oops-apps/common/app.mk).
Create a project directory with the following structure:
my-app/
├── Makefile
└── src/
└── main.c
Makefile
APP_NAME := my-homebrew
TITLE_ID := MYHB00001
TITLE_NAME := "My Native Homebrew"
# Sources to compile
PAYLOAD_SRCS := src/main.c
# Include canonical OOPS application rules:
include ../common/app.mkBuilding Targets:
make check: Run host-side unit tests and memory models.make elf: Build a plain ELF payload for loading overpros send(port 9021).make eboot: Package into a fake-signedeboot.bincontainer viaSELFish.make title: Create a complete, conforming native Big App directory layout (build/title/<TITLE_ID>/).
3. Tutorial 1: Display Output & 2D Software Drawing
Open exclusive HDMI scanout (OBS_VIDEO_BUS_MAIN) and
render 2D shapes and text.
#include <oops/oops.h>
int main(void) {
oops_klog("APP", "Starting 2D display demo...\n");
// 1. Open primary display (1080p, double-buffered)
oops_display_t *disp = oops_display_open(OOPS_DISPLAY_BACKEND_AUTO, 1920, 1080);
if (!oops_display_is_ready(disp)) {
oops_klog("APP", "Failed to acquire display!\n");
return -1;
}
int frame = 0;
while (1) {
// 2. Wrap back buffer into a 2D drawing surface
oops_surface_t surf = oops_display_get_surface(disp);
// 3. Clear background
oops_draw_clear(&surf, OOPS_COLOR_BLACK);
// 4. Draw shapes. oops_draw_rect() fills the whole span
oops_draw_rect(&surf, 100, 100, 400, 200, OOPS_COLOR_BLUE);
oops_draw_circle(&surf, 800, 300, 80, OOPS_COLOR_RED, 1);
oops_draw_circle(&surf, 800, 300, 90, OOPS_COLOR_WHITE, 0);
// 5. Render text using built-in bitmap font
oops_draw_text(&surf, 120, 140, "Hello from oops-sdk!", OOPS_COLOR_WHITE, 2);
// 6. Flip back buffer to HDMI output (sync with VBLANK)
oops_display_flip(disp);
frame++;
}
oops_display_close(disp);
return 0;
}4. Tutorial 2: DualSense Controller Input & Feedback
Poll button states, analog sticks, and control lightbars and haptic vibration.
#include <oops/oops.h>
void handle_input(void) {
oops_input_init();
oops_pad_state_t pad;
while (1) {
// Poll gamepad 0
if (oops_input_poll(0, &pad) == 0) {
if (pad.buttons & OOPS_BUTTON_CROSS) {
oops_klog("PAD", "Cross pressed!\n");
// Turn lightbar green and pulse strong motor
oops_input_set_lightbar(0, 0, 255, 0);
oops_input_set_rumble(0, 0, 128);
}
if (pad.buttons & OOPS_BUTTON_CIRCLE) {
// Turn lightbar red and stop rumble
oops_input_set_lightbar(0, 255, 0, 0);
oops_input_set_rumble(0, 0, 0);
}
// Analog sticks: -128 to 127
int lx = pad.left_stick_x;
int ly = pad.left_stick_y;
}
oops_time_sleep_ms(16); // 60 Hz poll rate
}
}5.
Tutorial 3: 3D Hardware Graphics (oops-gl vs
oops-mesa)
The build system lets you select the 3D graphics pipeline through one
line in your Makefile:
OOPS_RENDERER = gl1 # OpenGL 1.1–1.5 Fixed Function (oops-gl, freestanding)
OOPS_RENDERER = gl2 # OpenGL 2.0 / GLSL 1.20 Programmable (oops-gl, freestanding)
OOPS_RENDERER = mesa # OpenGL 3.3 Core / GLES 2/3 (oops-mesa, hosted)A. OpenGL 1.x / 2.0 with
oops-gl
oops-gl is a freestanding 3D engine running directly on
RDNA2 AGC hardware without desktop OS dependencies. On Prospero,
fragment shaders compile directly into gfx1030 machine instructions.
#include <oops/oops.h>
#include <GL/gl.h>
void render_cube(void) {
oops_display_t *disp = oops_display_open(OOPS_DISPLAY_BACKEND_AUTO, 1920, 1080);
// 1. Create OpenGL context on top of the display
void *ctx = glContextCreate(disp);
glContextMakeCurrent(ctx);
// If using GLSL shaders, unlock GL 2.0:
// glContextSetVersion(2, 0);
// 2. Configure 3D state
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
// 3. Set projection matrix
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glFrustum(-1.777f * 0.1f, 1.777f * 0.1f, -0.1f, 0.1f, 0.1f, 100.0f);
float angle = 0.0f;
while (1) {
glClearColor(0.1f, 0.1f, 0.15f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0f, 0.0f, -4.0f);
glRotatef(angle, 1.0f, 1.0f, 0.0f);
// Render colored triangle
glBegin(GL_TRIANGLES);
glColor3f(1.0f, 0.0f, 0.0f); glVertex3f( 0.0f, 1.0f, 0.0f);
glColor3f(0.0f, 1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 0.0f);
glColor3f(0.0f, 0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, 0.0f);
glEnd();
// Swap buffers and submit AGC command stream to GPU
glSwapBuffers();
angle += 1.0f;
}
glContextDestroy(ctx);
oops_display_close(disp);
}B. Modern OpenGL 3.3+ with
oops-mesa
When porting modern PC games requiring modern GLSL, compute shaders,
or multiple render targets (FBOs), set
OOPS_RENDERER = mesa. It links against upstream Mesa
(Gallium / Radv) backed by a hosted FreeBSD libc sysroot.
6.
Tutorial 4: Cross-Platform Development with SDL2
(oops-sdl)
For cross-platform engines and emulators, oops-sdl wraps
the console's display, DualSense controllers, and audio streams into
standard SDL 2.0.22 APIs:
Makefile
APP_NAME := sdl-game
TITLE_ID := SDLG00001
TITLE_NAME := "SDL2 Homebrew Game"
OOPS_RENDERER := gl1
ENTRY_POINT := sdl_game_start
PAYLOAD_SRCS := src/entry.c src/game.c
include $(OOPS_APPS_ROOT)/src/oops-deps/sdl2/oops-sdl2.mk
include $(OOPS_APPS_ROOT)/common/posix/posix.mk
include $(OOPS_APPS_ROOT)/common/app.mksrc/game.c
#include <SDL2/SDL.h>
#include <GL/gl.h>
int main(int argc, char **argv) {
(void)argc; (void)argv;
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_AUDIO) != 0) {
return -1;
}
SDL_Window *window = SDL_CreateWindow("My Game", 0, 0, 1920, 1080, SDL_WINDOW_OPENGL);
SDL_GLContext gl_ctx = SDL_GL_CreateContext(window);
// Open first connected DualSense controller
SDL_GameController *controller = NULL;
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (SDL_IsGameController(i)) {
controller = SDL_GameControllerOpen(i);
break;
}
}
int running = 1;
SDL_Event event;
while (running) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = 0;
} else if (event.type == SDL_CONTROLLERBUTTONDOWN) {
if (event.cbutton.button == SDL_CONTROLLER_BUTTON_A) {
// Cross button pressed
}
}
}
glClearColor(0.2f, 0.3f, 0.4f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// ... render scene ...
SDL_GL_SwapWindow(window);
}
if (controller) SDL_GameControllerClose(controller);
SDL_GL_DeleteContext(gl_ctx);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}7. Tutorial 5: Stereo PCM Audio Output
Output uncompressed 16-bit stereo PCM audio (48,000 Hz).
#include <oops/oops.h>
void play_audio(void) {
// Open 48kHz, 2-channel stereo port, 1024-frame hardware chunks
oops_audio_port_t *audio = oops_audio_open(48000, 2, 1024);
if (!audio) return;
// Buffer for 1024 frames (2048 int16 samples)
int16_t samples[2048];
for (int i = 0; i < 1024; i++) {
int16_t val = (i % 64 < 32) ? 8000 : -8000; // Square wave
samples[i * 2 + 0] = val; // Left channel
samples[i * 2 + 1] = val; // Right channel
}
// Stream audio continuously
for (int loop = 0; loop < 100; loop++) {
oops_audio_write(audio, samples, 1024);
}
oops_audio_close(audio);
}8. Tutorial 6: Dynamic Code Execution & JIT Allocation
When building recompilers, emulators, or dynamic runtime translation engines, allocate executable memory with W^X safety:
#include <oops/oops.h>
void test_jit(void) {
if (!oops_jit_is_available()) {
oops_klog("JIT", "JIT not available: fallback to CPU interpreter\n");
return;
}
oops_jit_memory_t jit;
if (oops_jit_alloc(64 * 1024, &jit) != 0) {
return;
}
// Emit machine code into the writable view (rw_addr):
// x86_64: int add_five(int x) { return x + 5; }
// 8d 47 05 : lea eax, [rdi + 5]
// c3 : ret
uint8_t *code = (uint8_t *)jit.rw_addr;
code[0] = 0x8d;
code[1] = 0x47;
code[2] = 0x05;
code[3] = 0xc3;
// Flush instruction cache and serialize memory fences
oops_jit_flush_icache(jit.rx_addr, 4);
// Call the generated function via the executable view (rx_addr)
typedef int (*jit_fn_t)(int);
jit_fn_t add_five = (jit_fn_t)(uintptr_t)jit.rx_addr;
int result = add_five(37); // returns 42
// Release memory
oops_jit_free(&jit);
}9. Tutorial 7: System Dialogs, On-Screen Keyboard & Persistent Save Data
A. On-Screen Virtual Keyboard (IME)
oops_ime_param_t param = {
.user_id = -1,
.title = "Player Name",
.max_text_len = 32
};
oops_dialog_ime_open(¶m);
// Wait for user to submit or cancel keyboard
while (oops_dialog_ime_poll() == OOPS_IME_STATUS_RUNNING) {
oops_display_flip(disp);
}
char name_buf[32];
oops_ime_result_t res;
oops_dialog_ime_get_result(name_buf, sizeof(name_buf), &res);
if (res == OOPS_IME_RESULT_OK) {
oops_klog("IME", name_buf);
}
oops_dialog_ime_close();B. Persistent
Save Data & Preferences (<oops/savedata.h>)
oops-sdk provides transparent save data mounting. It
attempts vendor encrypted containers when retail entitlements exist, and
automatically falls back to persistent internal SSD
storage
(/data/savedata/<APP_ID>/<DIR_NAME>) for
homebrew:
#include <oops/savedata.h>
// Save binary settings or game progress in 1 line
void save_preferences(void) {
user_config_t cfg = { .theme = 2, .volume = 80 };
oops_savedata_save_file("SETTINGS", "config.bin", &cfg, sizeof(cfg));
}
// Load binary settings back on startup
void load_preferences(void) {
void *data = NULL;
size_t sz = 0;
if (oops_savedata_load_file("SETTINGS", "config.bin", &data, &sz) == 0) {
user_config_t *cfg = (user_config_t *)data;
// Apply cfg...
oops_fs_free_data(data);
}
}10.
Tutorial 8: Standard Runtime Shimming, Directory Traversal &
oops-deps
A. Dynamic Memory
Allocation (<oops/heap.h>)
Standard console SDKs require reserving fixed Direct Memory (DMEM)
pools, which fail outright in Category 65536 System Apps where the OS
grants 0 bytes of DMEM. oops-sdk's heap allocator works
identically everywhere:
#include <oops/heap.h>
// Allocate 16-byte aligned dynamic memory
char *buffer = (char *)oops_malloc(4096);
if (buffer) {
const char *msg = "Dynamic allocation successful";
obs_strncpy(buffer, msg, obs_strlen(msg) + 1);
oops_free(buffer);
}
// Zero-initialized array allocation
int *scores = (int *)oops_calloc(64, sizeof(int));
oops_free(scores);B.
High-Level Filesystem & Directory Traversal
(<oops/fs.h>)
Easily read assets and enumerate directories from /app0
(read-only package directory) or /data (writable SSD
partition):
#include <oops/fs.h>
// 1. Read entire asset file into memory
void *file_data = NULL;
size_t file_size = 0;
if (oops_fs_read_all("/app0/assets/level1.dat", &file_data, &file_size) == 0) {
oops_kprintf("FS", "Loaded level: %u bytes\n", (unsigned int)file_size);
oops_fs_free_data(file_data);
}
// 2. Directory traversal
oops_dir_t *dir = oops_fs_opendir("/app0/music");
if (dir) {
oops_dirent_t ent;
while (oops_fs_readdir(dir, &ent) == 1) {
if (!ent.is_directory) {
oops_kprintf("MUSIC", "Track: %s\n", ent.name);
}
}
oops_fs_closedir(dir);
}C.
Freestanding 3D Math & Matrix Transforms
(<oops/math.h>)
#include <oops/math.h>
// 1. Setup projection matrix (60 deg FOV, 16:9 aspect ratio, near 0.1, far 100.0)
oops_mat4_t proj;
oops_mat4_perspective(&proj, 60.0f * OOPS_DEG2RAD, 16.0f / 9.0f, 0.1f, 100.0f);
// 2. Setup camera view looking at origin from (0, 2, 5)
oops_vec3_t eye = oops_vec3_make(0.0f, 2.0f, 5.0f);
oops_vec3_t center = oops_vec3_make(0.0f, 0.0f, 0.0f);
oops_vec3_t up = oops_vec3_make(0.0f, 1.0f, 0.0f);
oops_mat4_t view;
oops_mat4_lookat(&view, eye, center, up);
// 3. Model transformation: rotate 45 degrees around Y-axis
oops_mat4_t model;
oops_mat4_identity(&model);
oops_mat4_rotate(&model, 45.0f * OOPS_DEG2RAD, 0.0f, 1.0f, 0.0f);D.
Real-Time Telemetry & Formatted Logging
(<oops/system.h>)
#include <oops/system.h>
oops_klog("INIT", "Subsystem online\n");
oops_kprintf("FRAME", "Delta: %u ms | Heap active: %u KB\n",
(unsigned int)frame_ms, (unsigned int)(stats.allocated_bytes / 1024));11. Special & Advanced Subsystems (Escalation & PKGs)
Privilege Escalation
(<oops/escalate.h>)
Standard native Big Apps run with full GPU and display privileges out of the box. Privilege escalation is only needed for system maintenance utilities, FTP daemons, or debugger background services:
// Jailbreak current process: zero UIDs, set SYSTEM_AUTHID, escape sandbox jail
if (oops_jailbreak_process(0) == 0) {
oops_klog("ESCALATE", "Now running as root with full filesystem access\n");
}Background PKG
Installation (<oops/pkg.h>)
oops_pkg_init();
oops_pkg_install("/data/homebrew/myapp.pkg");
uint32_t pct = 0;
while (pct < 100) {
oops_pkg_get_progress("UP0000-MYHB00001_00-0000000000000000", &pct);
oops_time_sleep_ms(500);
}
oops_pkg_term();12. Hardware Safety & Clean-Room Invariants
- Hardware Failsafe Ring Protection:
- All RDNA2 AGC submissions must handle fence timeouts safely. Never lock the physical hardware command ring in a busy loop.
- Clean-Room Verification:
- Never copy or inspect proprietary Sony SDK headers
(
kernel.h,pad.h,agc.h). - All interfaces in
oops-sdkare derived from mathematically verified reverse-engineering, open-source references, and physical silicon probes (CONVENTIONS §1).
- Never copy or inspect proprietary Sony SDK headers
(