From 0dbb79bcd93c1589ab7d83de6114ec2e169fb371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 24 Aug 2026 10:04:55 +0200 Subject: [PATCH] Document and wrap the PPSSPP "emulator:" devctls (API for homebrew) --- docs/emu-api/README.md | 25 +++++++ docs/emu-api/ppsspp_emu_api.h | 125 ++++++++++++++++++++++++++++++++++ docs/emu-api/protocol.md | 76 +++++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 docs/emu-api/README.md create mode 100644 docs/emu-api/ppsspp_emu_api.h create mode 100644 docs/emu-api/protocol.md diff --git a/docs/emu-api/README.md b/docs/emu-api/README.md new file mode 100644 index 0000000000..ea74eae835 --- /dev/null +++ b/docs/emu-api/README.md @@ -0,0 +1,25 @@ +# PPSSPP Emulator API + +`ppsspp_emu_api.h` is a small, header-only C wrapper around the emulator-only tricks PPSSPP exposes +to homebrew through the `"emulator:"` device of `sceIoDevctl` (implemented in `Core/HLE/sceIo.cpp`, +search for `"emulator:"`). + +None of this works on real PSP hardware - always check `ppsspp_is_emulator()` before relying on +anything else in the header, and keep a working fallback path for real hardware. + +C API documentation: https://www.ppsspp.org/docs/development/ppsspp-internals/emu-api/ + +Raw `sceIoDevctl` protocol (commands, layouts, quirks): see [protocol.md](protocol.md). + +## Usage + +Just copy `ppsspp_emu_api.h` into a PSPSDK-based homebrew project and `#include` it. It's entirely +`static inline` functions over `sceIoDevctl`, so there's no library to build or link. + +```c +#include "ppsspp_emu_api.h" + +if (ppsspp_is_emulator()) { + ppsspp_send_output_str("Hello from homebrew, running under PPSSPP!\n"); +} +``` diff --git a/docs/emu-api/ppsspp_emu_api.h b/docs/emu-api/ppsspp_emu_api.h new file mode 100644 index 0000000000..d064d2966c --- /dev/null +++ b/docs/emu-api/ppsspp_emu_api.h @@ -0,0 +1,125 @@ +// PPSSPP emulator API - header-only C wrapper. +// +// PPSSPP exposes a handful of emulator-only tricks to homebrew through the +// "emulator:"/"kemulator:" pseudo-devices of sceIoDevctl. None of this exists +// on real hardware - always check ppsspp_is_emulator() before depending on +// anything else here, and keep a working fallback path for real PSPs. +// +// C API docs: https://www.ppsspp.org/docs/development/ppsspp-internals/emu-api/ +// +// Raw sceIoDevctl protocol this wraps (cmd numbers, layouts, quirks): see protocol.md +// in this same directory, or Core/HLE/sceIo.cpp (search for "emulator:") in the main +// PPSSPP source tree, which is the source of truth. +// +// Usage: just #include this file in a PSPSDK-based homebrew project. It's +// entirely static inline functions over sceIoDevctl, so there's nothing to +// link. + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum PPSSPPEmulatorDevctlCmd { + PPSSPP_DEVCTL__GET_HAS_DISPLAY = 1, + PPSSPP_DEVCTL__SEND_OUTPUT = 2, + PPSSPP_DEVCTL__IS_EMULATOR = 3, + PPSSPP_DEVCTL__VERIFY_STATE = 4, + + PPSSPP_DEVCTL__EMIT_SCREENSHOT = 0x20, + + PPSSPP_DEVCTL__TOGGLE_FASTFORWARD = 0x30, + PPSSPP_DEVCTL__GET_ASPECT_RATIO = 0x31, + PPSSPP_DEVCTL__GET_SCALE = 0x32, + PPSSPP_DEVCTL__GET_AXIS = 0x33, + PPSSPP_DEVCTL__GET_VKEY = 0x34, +}; + +// Either name works - PPSSPP currently treats them identically. +#define PPSSPP_EMULATOR_DEVICE "emulator:" + +// Returns non-zero if running under PPSSPP. Call this before trusting any +// of the other functions below - on real hardware, or on an emulator that +// doesn't implement this API, sceIoDevctl will simply fail here. +static inline int ppsspp_is_emulator(void) { + return sceIoDevctl(PPSSPP_EMULATOR_DEVICE, PPSSPP_DEVCTL__IS_EMULATOR, NULL, 0, NULL, 0) == 0; +} + +// Returns non-zero if there's an actual display to render to (i.e. not +// running under PPSSPPHeadless). +static inline int ppsspp_has_display(void) { + unsigned int hasDisplay = 0; + sceIoDevctl(PPSSPP_EMULATOR_DEVICE, PPSSPP_DEVCTL__GET_HAS_DISPLAY, NULL, 0, &hasDisplay, sizeof(hasDisplay)); + return (int)hasDisplay; +} + +// Sends a block of text straight to PPSSPP's debug/log output. +static inline void ppsspp_send_output(const char *data, int len) { + sceIoDevctl(PPSSPP_EMULATOR_DEVICE, PPSSPP_DEVCTL__SEND_OUTPUT, (void *)data, len, NULL, 0); +} + +// Convenience wrapper for ppsspp_send_output() over a NUL-terminated string. +static inline void ppsspp_send_output_str(const char *str) { + ppsspp_send_output(str, (int)strlen(str)); +} + +// Asks PPSSPP to do an internal savestate round-trip as a consistency check. +// Asynchronous - the pass/fail result is only logged on the PPSSPP side, not +// reported back here. Mainly useful when testing the emulator itself. +static inline void ppsspp_verify_state(void) { + sceIoDevctl(PPSSPP_EMULATOR_DEVICE, PPSSPP_DEVCTL__VERIFY_STATE, NULL, 0, NULL, 0); +} + +// Delivers the current framebuffer through PPSSPP's internal debug-screenshot +// hook (used by the pspautotests/frametest infrastructure). This does not +// write a file to the memory stick - it's not a general screenshot feature. +static inline void ppsspp_emit_screenshot(void) { + sceIoDevctl(PPSSPP_EMULATOR_DEVICE, PPSSPP_DEVCTL__EMIT_SCREENSHOT, NULL, 0, NULL, 0); +} + +// Turns PPSSPP's fast-forward mode on or off. +static inline void ppsspp_toggle_fastforward(int enable) { + sceIoDevctl(PPSSPP_EMULATOR_DEVICE, PPSSPP_DEVCTL__TOGGLE_FASTFORWARD, enable ? (void *)1 : NULL, 0, NULL, 0); +} + +// Current display aspect ratio. Only correct in landscape orientation. +static inline float ppsspp_get_aspect_ratio(void) { + float ar = 0.0f; + sceIoDevctl(PPSSPP_EMULATOR_DEVICE, PPSSPP_DEVCTL__GET_ASPECT_RATIO, NULL, 0, &ar, sizeof(ar)); + return ar; +} + +// Current display scale factor. Only correct in landscape orientation. +static inline float ppsspp_get_scale(void) { + float scale = 0.0f; + sceIoDevctl(PPSSPP_EMULATOR_DEVICE, PPSSPP_DEVCTL__GET_SCALE, NULL, 0, &scale, sizeof(scale)); + return scale; +} + +// Reads an analog axis value injected by a PPSSPP-side input plugin, by +// JOYSTICK_AXIS_* index (see PPSSPP's Common/Input/KeyCodes.h). This is NOT +// normal controller input - use sceCtrl* for that. Returns 0 if no plugin +// has set this axis. +static inline float ppsspp_get_axis(int axisIndex) { + float value = 0.0f; + sceIoDevctl(PPSSPP_EMULATOR_DEVICE, PPSSPP_DEVCTL__GET_AXIS, (void *)(long)axisIndex, 0, &value, sizeof(value)); + return value; +} + +// Reads whether a virtual key injected by a PPSSPP-side input plugin is +// currently pressed, by NKCODE_* value (see PPSSPP's Common/Input/KeyCodes.h, +// codes mostly mirror Android's). This is NOT normal controller input - use +// sceCtrl* for that. Returns 0 if no plugin has set this key. +static inline unsigned char ppsspp_get_vkey(int keyCode) { + unsigned char value = 0; + sceIoDevctl(PPSSPP_EMULATOR_DEVICE, PPSSPP_DEVCTL__GET_VKEY, (void *)(long)keyCode, 0, &value, sizeof(value)); + return value; +} + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/docs/emu-api/protocol.md b/docs/emu-api/protocol.md new file mode 100644 index 0000000000..df48230225 --- /dev/null +++ b/docs/emu-api/protocol.md @@ -0,0 +1,76 @@ +# Emulator API - raw protocol + +This documents the raw `sceIoDevctl`-based protocol behind PPSSPP's emulator API. Most homebrew +should just use the C wrapper in [`ppsspp_emu_api.h`](ppsspp_emu_api.h) instead (see the +[README](README.md) and the [website docs](https://www.ppsspp.org/docs/development/ppsspp-internals/emu-api/) +for that) - read this page if you're extending the wrapper, writing a binding for another language, +or just curious how it works under the hood. + +Important: None of this will work on the real PSP! Always check `IS_EMULATOR` (see below) before +relying on any of it, and keep a working fallback path for real hardware. + +## How it works + +The whole API is exposed through a single, existing PSP syscall: `sceIoDevctl`. PPSSPP recognizes +two special, fake device names that don't correspond to any real device: + +* `"emulator:"` +* `"kemulator:"` + +Both names are currently handled identically (there's no user/kernel distinction enforced), so +either works from a user-mode homebrew app. The implementation lives in `sceIoDevctl()` in +[`Core/HLE/sceIo.cpp`](https://github.com/hrydgard/ppsspp/blob/master/Core/HLE/sceIo.cpp) - search +for `"emulator:"` if you want to see exactly what each command does, or if you're adding a new one. + +The standard `sceIoDevctl` signature is used, just with PPSSPP-specific `cmd` numbers and +argument/output block layouts: + +```c +int sceIoDevctl(const char *devicename, unsigned int cmd, void *indata, int inlen, void *outdata, int outlen); +``` + +* `devicename` - `"emulator:"` or `"kemulator:"`. +* `cmd` - one of the `EMULATOR_DEVCTL__*` values below. +* `indata`/`inlen` - input block, meaning depends on `cmd`. Some commands instead just check whether `indata` is NULL/non-NULL as a boolean flag. +* `outdata`/`outlen` - output block, meaning depends on `cmd`. Most commands write a single `u32` or `float` here. + +If `cmd` doesn't match any known command, `sceIoDevctl` returns an error (`UNKNOWN PARAMETERS`) +rather than crashing, so probing for support is safe. + +## Commands + +| Command | Value | Direction | Purpose | +|---|---|---|---| +| `EMULATOR_DEVCTL__GET_HAS_DISPLAY` | 1 | out: `u32` | Writes 1 if there's a real display (normal PPSSPP), 0 if running headless (`PPSSPPHeadless`). Useful to skip presentation/vblank-dependent work when there's nothing to show. | +| `EMULATOR_DEVCTL__SEND_OUTPUT` | 2 | in: bytes | Sends a raw block of text straight to PPSSPP's debug/log output (and to headless's collected output buffer, if any). Handy for logging from homebrew without going through `sceIoWrite` to a real file. | +| `EMULATOR_DEVCTL__IS_EMULATOR` | 3 | out: `u32` | Writes 1. This is the one to call first: if the `sceIoDevctl` call itself fails, you're not running under PPSSPP (or a build that doesn't implement this API), and none of the rest of this page applies. | +| `EMULATOR_DEVCTL__VERIFY_STATE` | 4 | none | Asks PPSSPP to do an internal savestate round-trip (save to memory, then verify it reads back correctly) as a consistency check. Runs asynchronously - it doesn't report the pass/fail result back to your code, it just gets logged on the PPSSPP side. Mainly useful for automated testing of the emulator itself. | +| `EMULATOR_DEVCTL__EMIT_SCREENSHOT` | 0x20 | none | Grabs the current framebuffer and delivers it through PPSSPP's internal debug-screenshot hook, which is used by things like the pspautotests/frametest infrastructure to collect result images. Not useful as a general "save a screenshot to memstick" feature - it doesn't write a file. | +| `EMULATOR_DEVCTL__TOGGLE_FASTFORWARD` | 0x30 | in: bool (NULL/non-NULL) | Turns PPSSPP's fast-forward mode on (non-NULL `indata`) or off (NULL). | +| `EMULATOR_DEVCTL__GET_ASPECT_RATIO` | 0x31 | out: `float` | Writes the display's current aspect ratio. Only correct in landscape orientation right now. | +| `EMULATOR_DEVCTL__GET_SCALE` | 0x32 | out: `float` | Writes the current display scale factor. Only correct in landscape orientation right now. | +| `EMULATOR_DEVCTL__GET_AXIS` | 0x33 | in: axis index (as `indata` value, not a pointer), out: `float` | Reads an analog axis value that a PPSSPP-side input plugin has injected (see below), by `JOYSTICK_AXIS_*` index. | +| `EMULATOR_DEVCTL__GET_VKEY` | 0x34 | in: key code (as `indata` value, not a pointer), out: `u8` | Reads whether a virtual key that a PPSSPP-side input plugin has injected is currently pressed, by PPSSPP's internal key code (see below). | + +A couple of notes on quirks that are easy to trip over: + +* For `GET_AXIS` and `GET_VKEY`, the "input" isn't the `indata` buffer contents - it's the `indata` pointer value itself, used directly as an integer index. This matches how the current PPSSPP implementation reads it (`argAddr` is compared against the axis/key range and used directly), so pass the index as if it were a pointer, e.g. `sceIoDevctl("emulator:", EMULATOR_DEVCTL__GET_AXIS, (void *)JOYSTICK_AXIS_X, 0, &value, sizeof(value))`. +* `GET_AXIS` and `GET_VKEY` don't read normal controller input (`sceCtrl*` already does that) - they read state from PPSSPP's HLE plugin system, i.e. values that a native PPSSPP-side plugin PRX has explicitly set for your homebrew to pick up. If no plugin is active, expect these to just come back as 0/unpressed. +* The axis index matches PPSSPP's internal `JOYSTICK_AXIS_*` enum (`Common/Input/KeyCodes.h`), and the key code matches PPSSPP's internal `NKCODE_*` enum (same file), not any PSP SDK enum. These mostly mirror Android's key/axis codes. A handful of the more useful ones: `NKCODE_DPAD_UP/DOWN/LEFT/RIGHT`, `NKCODE_BUTTON_CROSS/CIRCLE/SQUARE/TRIANGLE`, `JOYSTICK_AXIS_X/Y`. See the header for the full list if you need something more obscure. +* You'll sometimes see an `EMULATOR_DEVCTL__SEND_CTRLDATA` (0x10) constant referenced in older test code. It is not currently implemented by PPSSPP - calling it just gets you the generic "unknown parameters" error. Don't rely on it. + +## Raw example + +```c +#include + +#define EMULATOR_DEVCTL__IS_EMULATOR 3 +#define EMULATOR_DEVCTL__SEND_OUTPUT 2 + +int runningOnPPSSPP = sceIoDevctl("emulator:", EMULATOR_DEVCTL__IS_EMULATOR, NULL, 0, NULL, 0) == 0; + +if (runningOnPPSSPP) { + const char *msg = "Hello from homebrew, running under PPSSPP!\n"; + sceIoDevctl("emulator:", EMULATOR_DEVCTL__SEND_OUTPUT, (void *)msg, strlen(msg), NULL, 0); +} +```