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

  1. Freestanding C Architecture
  2. Project Setup & Makefile Integration
  3. Tutorial 1: Display Output & 2D Software Drawing
  4. Tutorial 2: DualSense Controller Input & Feedback
  5. Tutorial 3: 3D Hardware Graphics (oops-gl vs oops-mesa)
  6. Tutorial 4: Cross-Platform Development with SDL2 (oops-sdl)
  7. Tutorial 5: Stereo PCM Audio Output
  8. Tutorial 6: Dynamic Code Execution & JIT Allocation
  9. Tutorial 7: System Dialogs, On-Screen Keyboard & Persistent Save Data
  10. Tutorial 8: Standard Runtime Shimming, Directory Traversal & oops-deps
  11. Special & Advanced Subsystems (Escalation & PKGs)
  12. Hardware Safety & Clean-Room Invariants

1. Freestanding C Architecture

oops-sdk operates strictly freestanding (-ffreestanding -nostdlib):


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.mk

Building Targets:


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.mk

src/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(&param);

// 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

  1. Hardware Failsafe Ring Protection:
    • All RDNA2 AGC submissions must handle fence timeouts safely. Never lock the physical hardware command ring in a busy loop.
  2. Clean-Room Verification:
    • Never copy or inspect proprietary Sony SDK headers (kernel.h, pad.h, agc.h).
    • All interfaces in oops-sdk are derived from mathematically verified reverse-engineering, open-source references, and physical silicon probes (CONVENTIONS §1).