Files
MrLowPr--overdrive/ps5-react/native/shared/image_api.cpp
T
2026-10-14 04:20:24 +02:00

135 lines
5.5 KiB
C++

// 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 <climits>
#include <cstdio>
#include <cstring>
#include <string>
#include <utility>
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<unsigned>(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<images::Fit>(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<std::string> 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<const char*, JSCFunction*>{"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;
}