// Copyright (C) 2026 half144 and PS5 React contributors // SPDX-License-Identifier: GPL-3.0-or-later // Additional attribution term: see LICENSE-ATTRIBUTION. #include "host_api.hpp" #include "image_loader.hpp" #include #include #include #include #include extern "C" { #include "er_scene.h" } namespace { // The engine name an Image node draws a loaded image under. void engine_name(std::uint32_t id, char (&out)[24]) { std::snprintf(out, sizeof out, "@image:%u", id); } void unregister(std::uint32_t id) { char name[24]; engine_name(id, name); er_image_unload(name); } // Registers a newly decoded image; returns false when the engine registry is full. bool publish(const images::Result& result) { char name[24]; engine_name(result.id, name); return er_image_load_argb(name, result.pixels, result.width, result.height, result.opaque); } JSValue state(JSContext* ctx, const images::Result& result) { JSValue out = JS_NewObject(ctx); char name[24]; engine_name(result.id, name); JS_SetPropertyStr(ctx, out, "id", JS_NewUint32(ctx, result.id)); JS_SetPropertyStr(ctx, out, "state", JS_NewString(ctx, result.ready ? "ready" : result.failed ? "failed" : "loading")); JS_SetPropertyStr(ctx, out, "name", JS_NewString(ctx, result.ready ? name : "")); JS_SetPropertyStr(ctx, out, "width", JS_NewInt32(ctx, result.width)); JS_SetPropertyStr(ctx, out, "height", JS_NewInt32(ctx, result.height)); JS_SetPropertyStr(ctx, out, "error", JS_NewStringLen(ctx, result.error.data(), result.error.size())); char color[8]; std::snprintf(color, sizeof color, "#%06x", static_cast(result.color)); JS_SetPropertyStr(ctx, out, "color", result.color < 0 ? JS_NULL : JS_NewString(ctx, color)); return out; } JSValue load(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) { if (argc < 4) return JS_ThrowTypeError(ctx, "image.load: expected url, width, height and fit"); std::size_t size = 0; const char* text = JS_ToCStringLen(ctx, &size, argv[0]); if (!text) return JS_EXCEPTION; std::string url(text, size); JS_FreeCString(ctx, text); if (!(url.starts_with("https://") || url.starts_with("http://")) || url.size() > 8192 || url.find_first_of(std::string("\r\n\0", 3)) != std::string::npos) return JS_ThrowTypeError(ctx, "image.load %s: expected an http:// or https:// URL (at most 8192 bytes)", url.c_str()); int width = 0, height = 0, fit = 0; if (JS_ToInt32(ctx, &width, argv[1]) || JS_ToInt32(ctx, &height, argv[2]) || JS_ToInt32(ctx, &fit, argv[3])) return JS_EXCEPTION; if (width < 1 || height < 1 || width > 8192 || height > 8192 || fit < 0 || fit > 3) return JS_ThrowRangeError(ctx, "image.load %s: box %dx%d must be 1..8192 pixels per side", url.c_str(), width, height); const bool prefetch = argc > 4 && JS_ToBool(ctx, argv[4]) == 1; images::Result result; std::string error; if (!images::load(url, width, height, static_cast(fit), prefetch, result, error)) return JS_ThrowPlainError(ctx, "%s", error.c_str()); return state(ctx, result); } JSValue release(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) { std::uint32_t id = 0; if (!argc || JS_ToUint32(ctx, &id, argv[0])) return JS_ThrowTypeError(ctx, "image.release: expected an image id"); images::release(id, argc > 1 && JS_ToBool(ctx, argv[1]) == 1); return JS_UNDEFINED; } JSValue warm(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) { if (!argc || !JS_IsArray(argv[0])) return JS_ThrowTypeError(ctx, "image.warm: expected an array of URLs"); std::int64_t length = 0; if (JS_GetLength(ctx, argv[0], &length)) return JS_EXCEPTION; std::vector urls; for (std::int64_t i = 0; i < length; ++i) { JSValue item = JS_GetPropertyInt64(ctx, argv[0], i); if (const char* text = JS_ToCString(ctx, item)) { std::string url(text); JS_FreeCString(ctx, text); if ((url.starts_with("https://") || url.starts_with("http://")) && url.size() <= 8192 && url.find_first_of(std::string("\r\n\0", 3)) == std::string::npos) urls.push_back(std::move(url)); } JS_FreeValue(ctx, item); } images::warm(std::move(urls)); return JS_UNDEFINED; } JSValue defer(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) { int ms = 0; if (!argc || JS_ToInt32(ctx, &ms, argv[0]) || ms < 0 || ms > 10000) return JS_ThrowRangeError(ctx, "image.defer: expected 0..10000 milliseconds"); images::defer(ms); return JS_UNDEFINED; } JSValue poll(JSContext* ctx, JSValueConst, int, JSValueConst*) { JSValue out = JS_NewArray(ctx); std::uint32_t index = 0; for (auto& result : images::poll(unregister)) { if (result.ready && !publish(result)) { images::discard(result.id, "the engine image registry is full"); result.ready = false; result.failed = true; result.pixels = nullptr; result.error = "Image.load: the engine image registry is full"; } JS_SetPropertyUint32(ctx, out, index++, state(ctx, result)); } return out; } } // namespace bool ps5_react_start_images() { char cache[PATH_MAX]; return images::start(host::resolve_path("/cache0/images", cache, sizeof cache) ? cache : ""); } void ps5_react_stop_images() { images::stop(unregister); } JSValue ps5_react_image_api(JSContext* ctx) { JSValue out = JS_NewObject(ctx); for (const auto& item : {std::pair{"load", &load}, {"release", &release}, {"warm", &warm}, {"defer", &defer}, {"poll", &poll}}) JS_SetPropertyStr(ctx, out, item.first, JS_NewCFunction(ctx, item.second, item.first, 0)); return out; }