// Copyright (C) 2026 half144 and PS5 React contributors // SPDX-License-Identifier: GPL-3.0-or-later // Additional attribution term: see LICENSE-ATTRIBUTION. // Native API contract (ABI v17) between the hosts and `@ps5-react/core`. The input actions hosts // pass to `__ps5ReactDispatch` are a separate contract with its own version (runtime/js/input.js). // // host_api.cpp (shared) installs `globalThis.__ps5ReactNative` through // ErRuntimeConfig.install_host_globals and implements the filesystem with POSIX // (mount listing excepted). // Each platform implements the `host::` functions below. Everything runs on the // render thread; the functions are synchronous and must return quickly. // // JavaScript shape (runtime/js/native.js wraps it): // platform 'ps5' | 'desktop' // fs.readDir(path) [{name, isDirectory, isFile, size, modified, device, inode}] // fs.stat(path) {name, isDirectory, isFile, size, modified, device, inode} | null // device/inode are decimal strings, preserving 64-bit IDs // fs.readFile(path) string (UTF-8; at most kMaxReadBytes) // fs.writeFile(path, text, append, sync) sync: fsync before close // fs.mkdir(path, recursive) // fs.remove(path) file or empty directory // fs.removeTree(path) a file, or a directory and everything in it; symbolic links are // unlinked, never followed; synchronous, for small app-owned trees // fs.removeAsync(path) ABI v16; task ID; removes as removeTree does, on a worker thread, in // the order asked; a path already gone counts as removed // removals.poll() [{id, state, error}]; error is the system error; consumes terminal // fs.rename(from, to) // fs.chmod(path, mode) sets permission bits // fs.mounts() [{device, path, type}], visible to the process // console access is opt-in via app.json // fs.diskUsage(path) {total, free} in bytes; free available to the app // device.info() {model, firmware, cpuTemperature, socTemperature, // cpuFrequency, freeMemory, processTime, language}; unknown = null // language: the system language as a BCP 47 tag ('pt-BR') // device.utcOffset(ms) ABI v14; minutes east of UTC at that instant in the system time zone // users.foreground() {id, name} | null // users.loggedIn() [{id, name}] // notify(message, subMessage) boolean // openURL(url) boolean // pad.setLightBar(r, g, b) 0..255 each // pad.resetLightBar() // pad.vibrate(strength, seconds) strength 0..1 // pad.setTriggerEffect(mask, mode, a, b, c) ABI v12, replacing v8's setTriggerResistance (mode 1); a // DualSense adaptive trigger effect on the masked triggers (1 L2, 2 R2): // 0 off; 1 feedback (a position 0..9, b strength 1..8); 2 weapon (a start // 2..7, b end a+1..8, c strength 1..8); 3 vibration (a position 0..9, // b amplitude 1..8, c frequency 1..255 Hz). Unused parameters are ignored // pad.state() {connected, leftX, leftY, rightX, rightY, l2, r2, buttons} // power.keepAwake(enabled) ABI v4; while true, the console does not enter rest mode // for inactivity (the desktop display does not sleep) // display.refreshRate() ABI v11; the rate in Hz the display presents at; on the PS5 the mode // the TV accepted, known after the first frame at that rate // display.setRefreshRate(hz) ABI v11; 60 or 120, listed in app.json "refreshRates"; the host // switches after this frame (PS5: restarts EGL, the TV resyncs) // network.download(url, path, options) task ID; queues native binary file I/O // options: expectedBytes, storageRoot, pieces[{url, offset, size, sha1}], // sha256, connections, adaptive, rangeBytes, resume, recoverCompleted, headers, rejectHtml, // unlimited (ABI v17; exempt from setRateLimit) // network.request(url, options) task ID; queues bounded HTTP text I/O // options: method, body, maxBytes, headers, followRedirects (default true) // terminal snapshots: status, body, effective url, allowlisted lowercase headers // network.cancel(id) requests cancellation // network.poll() progress/results; consumes terminal snapshots // network.setRateLimit(bytes) ABI v17; bytes per second all file downloads share, 0 for no limit; // applies at once, running downloads included, except unlimited ones // archives.extract(sources, destination, maxBytes) task ID; bounded native archive worker // archives.inspect(sources) {kind, refusal}; file type from leading bytes and, for an archive, // a decoder-memory refusal its headers so far justify; partial files ok // archives.cancel(id) requests cancellation; source volumes remain intact // archives.poll() state/error/written/entries/artifacts; consumes terminal results // browser.capture(prefix, suffix, timeoutSeconds) ABI v7; Vikingfile capture task bound to a filename, or to any file when suffix is "/" // browser.cancel(id) stops the read-only payload by closing its connection // browser.poll() state/error/url; consumes terminal results // lan.broadcast(port, message, timeoutMs, maxReplies) ABI v9; task ID; one UDP datagram to // 255.255.255.255:port, replies gathered off-thread for 100..10000 ms // (at most 64, 4 KiB each); four queries at a time; needs networking // lan.cancel(id) stops gathering; the replies so far are kept // lan.poll() [{id, state, error, replies: [{address, port, body}]}] finished // since the last poll; state 'completed' | 'failed' | 'cancelled' // packages.install(path, name) task ID; installs a .pkg under /data, /mnt/usb or /mnt/ext through the // console's install service (PS5 only, needs the payload loader on 9021) // packages.cancel(id) stops reporting; an install the console accepted carries on // packages.poll() state/error/contentId/status/written/total; consumes terminal results // network.version() transport version string // image.load(url, width, height, fit, prefetch) {id, state, name, width, height, error, color}; takes a // reference; fit: 0 cover, 1 contain, 2 stretch, 3 none; prefetch // (optional) loads after drawn images; state 'loading' | // 'ready' | 'failed'; name is the engine image name when ready; // color is '#rrggbb', the art's most prominent vivid hue, or null // image.release(id, prefetch) drops a reference taken by a load with the same prefetch; // cancels unfinished work without references // image.warm(urls) fills the disk cache with `urls` in the background, after loads; // replaces the previous list // image.poll() [{id, state, name, width, height, error, color}] finished since last poll // sound.play(name, volume, pan) ABI v5; boolean; posts an imported WAV to the mixer (volume // 0..1, pan -1..1); false without audio or while it still plays // sound.setVolume(volume) master gain 0..1 for every sound // audioOutput.capabilities() ABI v13; {known, name, formats: [{codec, channels, sampleRates, bitDepths?, // maxBitrate?}], passthrough: ['ac3' | 'eac3' | 'dts'], pcmChannels}: what the // connected sink takes, from its EDID (PS5: the HDMI sink); known false without one // video.open(source, options) ABI v10; {id, name}: plays an app path or http(s) URL on its own // threads; options: paused, muted, loop, volume, start, audio, headers, // audioOutput (ABI v13: 'pcm' | 'auto' | 'passthrough'). // `name` is the placeholder image an draws, which the // presenter replaces with the video // video.close(id) stops it and frees it off the render thread // video.setPaused(id, paused) / seek(id, seconds) / setVolume(id, 0..1) / setMuted(id, muted) / // setLoop(id, loop) / setResizeMode(id, 0 contain | 1 cover | 2 stretch) / selectAudioTrack(id, index) / // selectSubtitleTrack(id, index) / setAudioDelay(id, seconds, -2..2: the sound later than the picture) / // setUpscale(id, model: '' for none, 'artcnn', 'anime4k-s' / -m / -l / -vl / -ul; where compute shaders run) / // setAudioOutput(id, 'pcm' | 'auto' | 'passthrough') ABI v13; read again from the playhead / // setCompare(id, model | null, split) ABI v15; right of split (0..1 across the picture) through // `model` ('' for none) instead; null ends it // video.addSubtitle(id, source, language, title) the track index of a text subtitle file (path or URL), // loaded off the render thread // video.poll() [{id, state, paused, seeking, position, duration, buffered, width, // height, aspect, error, videoCodec, audioCodec, decoder, audioTrack, // audioTracks: [{index, language, title, codec, channels}], // subtitleTracks: [{index, language, title, codec, external}], // subtitleTrack, subtitle (the text showing now), // shownFrames, droppedFrames, lateMs, audioClock, audioPassthrough (ABI v13: the // sink decodes the track)}]; state 'loading' | 'ready' | // 'buffering' | 'ended' | 'failed' // text.setLanguage(tag) ABI v6; BCP 47 language for runtime text (Han glyph forms, // shaping); defaults to the system language // exit(relaunch) asks the host to close after this frame; with relaunch true it // opens the app again once closed and returns true, or the reason // it cannot (PS5: needs the payload loader on 127.0.0.1:9021) // Failures throw a JS Error whose message names the call, the path, and strerror. #pragma once #include #include #include namespace hui::audio { class Mixer; } namespace iec61937 { enum class Codec : std::uint8_t; } extern "C" { #include "quickjs.h" } constexpr std::size_t kMaxReadBytes = 8 * 1024 * 1024; // Shared (host_api.cpp). void ps5_react_install_host_api(JSContext* ctx); bool ps5_react_exit_requested(); // ABI v11: the host reports the rate it presents at, and after each frame takes the rate // display.setRefreshRate asked for, 0 when there is none, to switch to before the next one. void ps5_react_set_refresh_rate(int hz); int ps5_react_take_refresh_request(); // Calls `globalThis.__ps5ReactFrame(elapsedMs)` (runtime/js/frame.js) once per frame, after // er_runtime_pump() and before er_commit(): per-frame motion steps by the time the frame advances // on screen. False when it threw. bool ps5_react_frame(JSContext* ctx, double elapsed_ms); // Latest controller state, set by the host once per frame before er_runtime_pump(). struct GamepadState { bool connected = false; float left_x = 0, left_y = 0, right_x = 0, right_y = 0; // -1..1, deadzone applied float l2 = 0, r2 = 0; // 0..1 std::uint32_t buttons = 0; // bit i = kButtonNames[i] held }; inline constexpr const char* kButtonNames[] = { "up", "down", "left", "right", "cross", "circle", "triangle", "square", "l1", "r1", "l2", "r2", "l3", "r3", "options", "touchpad", }; void ps5_react_set_gamepad(const GamepadState& state); // Per platform (native/ps5/host_platform.cpp, native/desktop/host_platform.cpp). namespace host { const char* platform_name(); // Maps an app path (/app0, /download0, /temp0, ...) to a real path. False rejects it. bool resolve_path(const char* path, char* out, std::size_t size); struct MountEntry { char device[128] = ""; char path[128] = ""; char type[16] = ""; }; // Fills at most max entries; returns the count, or -1 with errno set. int list_mounts(MountEntry* mounts, int max); // Calls visit for each entry name of a directory, skipping "." and ".."; // false with errno set when the directory cannot be read. bool read_dir(const char* real_path, void (*visit)(const char* name, void* user), void* user); // Total and free bytes of the filesystem holding real_path; false with errno set. bool disk_usage(const char* real_path, double& total, double& free); // Records the first use of each native call (PS5: kernel log), to locate native faults. void trace(const char* call); // The native call running on the render thread, or nullptr: the stall watchdog names it. extern std::atomic current_call; struct CallScope { const char* outer; explicit CallScope(const char* name) : outer(current_call.exchange(name)) {} ~CallScope() { current_call = outer; } }; // Opens the app again once it has closed: PS5 sends relauncher.elf to the payload loader, the desktop // preview runs itself again. False with a reason in `error` (at most `size` bytes) when it cannot. bool arrange_relaunch(char* error, std::size_t size); struct DeviceInfo { char model[64] = ""; char firmware[32] = ""; int cpu_temperature = -1; // °C, -1 unknown int soc_temperature = -1; // °C, -1 unknown std::int64_t cpu_frequency = -1; // Hz std::int64_t free_memory = -1; // bytes available to the app std::int64_t process_time = -1; // µs of CPU time used by this process char language[16] = ""; // system language, BCP 47 ("en-US", "zh-Hans") }; void device_info(DeviceInfo& info); struct User { int id = -1; char name[32] = ""; }; bool foreground_user(User& user); int logged_in_users(User* users, int max); bool notify(const char* message, const char* sub_message); bool open_url(const char* url); void set_light_bar(std::uint8_t r, std::uint8_t g, std::uint8_t b); void reset_light_bar(); void vibrate(float strength, float seconds); // A validated pad.setTriggerEffect: `params` are the mode's bytes in the order listed above. void set_trigger_effect(unsigned mask, int mode, const std::uint8_t* params, std::size_t count); // While enabled, keeps the system from sleeping for inactivity; the app's last call wins. void keep_awake(bool enabled); // Renders the mixer on an audio thread (PS5: sceAudioOut, desktop: SDL); false leaves the app silent. bool start_audio(hui::audio::Mixer& mixer); void stop_audio(); // IEC 61937 bitstream output, for the sink to decode (PS5: the HDMI port the console's disc player uses). // Opening switches the HDMI audio mode, which blocks for a while: not on the render thread. Its own thread // calls `fill` for each grain of `frames` carrier frames (two words each), which writes zeros where it has // nothing. False where there is no such output (the desktop preview) or the console refuses it. using BitstreamFill = void (*)(std::uint16_t* words, std::size_t frames); bool has_bitstream(); bool open_bitstream(iec61937::Codec codec, BitstreamFill fill); void close_bitstream(); } // namespace host // ABI v2: synchronous native task submission/polling; I/O runs off-thread. JSValue ps5_react_network_api(JSContext* ctx); // ABI v3: remote images fetched and decoded off-thread, registered with the engine on poll. JSValue ps5_react_image_api(JSContext* ctx); // Starts the image workers (after network::start) with the disk cache in the app's data directory. bool ps5_react_start_images(); // Unregisters every remote image from the engine, then stops the image workers and frees them. void ps5_react_stop_images(); // ABI v5: sounds the app imports (baked by tools/bundle.mjs), mixed on an audio thread. JSValue ps5_react_sound_api(JSContext* ctx); // Decodes the baked sounds and opens the audio output; false (silent) when the app has no sounds or // the output fails. Start before the bundle runs, stop after the runtime shuts down. bool ps5_react_start_sound(); void ps5_react_stop_sound(); // ABI v5 additive archive tasks: extract(sources, destination, maxBytes, password?), inspect(sources), cancel(id), poll(). JSValue ps5_react_archive_api(JSContext* ctx); // ABI v5 additive package installs: install(path, name), cancel(id), poll(). JSValue ps5_react_package_api(JSContext* ctx); // ABI v7: filename-bound browser capture tasks (PS5 only). JSValue ps5_react_browser_capture_api(JSContext* ctx); // ABI v9: UDP broadcast queries on the local network (lan.hpp). JSValue ps5_react_lan_api(JSContext* ctx); // ABI v10: video playback (video_api.hpp). JSValue ps5_react_video_api(JSContext* ctx); // ABI v13: the connected sink's audio capabilities (audio_output_api.cpp). JSValue ps5_react_audio_output_api(JSContext* ctx);