Files
PS5_RetroArch/tests/esde_freeimage_test.cpp
T
Mihawk eaba7a8114 Run EmulationStation (ES-DE 3.5.0) on the PS5 beside RetroArch
ES-DE is its own executable of the title, /app0/es-de/es-de.bin, started
through LoadExec. It draws through ../PS5_OpenGL and shows RetroArch's
content folders as its systems (frontends/es-de/systems.tsv), with the
Alekfull NX theme by default.

- tools/build-esde.sh: the dependencies (FreeType, HarfBuzz, ICU, pugixml,
  curl, FFmpeg and now libwebp, tools/build-libwebp.sh), the pinned stb
  headers, ES-DE at 50e4b600 with frontends/es-de/patches, CMake, and the
  pinned Alekfull NX theme.
- frontends/es-de: the port's main, a FreeImage stand-in over stb_image
  and libwebp, an armed framebuffer capture, and the staging of
  resources, themes and the systems file.
- tools/build-frontend.sh binds every import the SDK's platform layer
  replaces (readlink was a null import), wraps the thread functions as
  eboot.bin does, and fails on an import only libkernel_sys exports.
- frontends/common: iswctype_l with FreeBSD masks (the console's
  Dinkumware iswctype faulted), pathconf, lstat answered as stat (refused
  to a title), and a getcwd that can name a working directory.
- tools/run-title.sh --frontend-capture; tools/esde-settings.py.

Evidence: evidence/esde-first-light/ (capture.json, expectation.json,
capture-test.jsonl). On the base PS5 (13.40): ES-DE loads 24 systems and
the Alekfull NX theme in 1.4 s, with no fatal signal and no image it
could not load; the 30 s capture is the SNES system view at 1920x1080.
Command:
  PS5_FRONTENDS=es-de tools/build-title.sh
  tools/esde-settings.py Theme=alekfull-nx-es-de StartupSystem=snes
  tools/run-title.sh --no-build --relaunch-test=1 \
    --relaunch-image=es-de/es-de.bin --frontend-capture=15,30,50 --watch 70
Gates: tools/verify.sh PASS (format unit build integration evidence).
Known: no audio (the SDL2 build has none), games do not start yet.
2026-10-04 16:31:50 -04:00

130 lines
6.4 KiB
C++

/* The FreeImage functions EmulationStation uses, as frontends/es-de/ps5 provides
* them over stb_image: FreeImage's layout (bottom-up rows, B, G, R, A), loading,
* conversion, rescaling, saving, a GIF's frames with their times and a WebP.
* argv: a scratch directory, a PNG, a two-frame GIF (red then blue, 20 ms) and a
* lossless 3x2 WebP (red, green, blue; white at half alpha, clear, 10/20/30). */
#include <cassert>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include "../frontends/es-de/ps5/freeimage_stb.cpp"
static unsigned read_proc(void *buffer, unsigned size, unsigned count, fi_handle handle)
{
return unsigned(std::fread(buffer, size, count, static_cast<std::FILE *>(handle)));
}
static unsigned write_proc(void *, unsigned, unsigned, fi_handle)
{
return 0;
}
static int seek_proc(fi_handle handle, long offset, int origin)
{
return std::fseek(static_cast<std::FILE *>(handle), offset, origin);
}
static long tell_proc(fi_handle handle)
{
return std::ftell(static_cast<std::FILE *>(handle));
}
int main(int argc, char **argv)
{
assert(argc == 5);
const std::string dir = argv[1], png = argv[2], gif = argv[3], webp = argv[4];
FreeImage_Initialise();
// Detection by content and by name.
assert(FreeImage_GetFileType(png.c_str()) == FIF_PNG);
assert(FreeImage_GetFileType(gif.c_str()) == FIF_GIF);
assert(FreeImage_GetFIFFromFilename("a/b/Cover.JPeg") == FIF_JPEG);
assert(FreeImage_GetFIFFromFilename("x.webp") == FIF_WEBP);
// A WebP, through libwebp: FreeImage's layout, the bottom row first.
assert(FreeImage_GetFileType(webp.c_str()) == FIF_WEBP &&
FreeImage_FIFSupportsReading(FIF_WEBP));
FIBITMAP *webp_image = FreeImage_Load(FIF_WEBP, webp.c_str());
assert(webp_image && FreeImage_GetWidth(webp_image) == 3 &&
FreeImage_GetHeight(webp_image) == 2 && FreeImage_GetBPP(webp_image) == 32);
const BYTE *upper = FreeImage_GetScanLine(webp_image, 1),
*lower = FreeImage_GetScanLine(webp_image, 0);
assert(upper[FI_RGBA_RED] == 255 && upper[FI_RGBA_GREEN] == 0 && upper[FI_RGBA_ALPHA] == 255);
assert(upper[4 + FI_RGBA_GREEN] == 255 && upper[8 + FI_RGBA_BLUE] == 255);
assert(lower[FI_RGBA_RED] == 255 && lower[FI_RGBA_ALPHA] == 128);
assert(lower[8 + FI_RGBA_RED] == 10 && lower[8 + FI_RGBA_GREEN] == 20 &&
lower[8 + FI_RGBA_BLUE] == 30);
FreeImage_Unload(webp_image);
// A PNG: FreeImage's layout, checked against stb_image's own top-down RGBA.
FIBITMAP *image = FreeImage_Load(FIF_PNG, png.c_str());
assert(image && FreeImage_GetBPP(image) == 32);
const unsigned width = FreeImage_GetWidth(image), height = FreeImage_GetHeight(image);
int w = 0, h = 0, n = 0;
unsigned char *rgba = stbi_load(png.c_str(), &w, &h, &n, 4);
assert(rgba && unsigned(w) == width && unsigned(h) == height);
const BYTE *bottom = FreeImage_GetScanLine(image, 0);
const unsigned char *last_row = rgba + size_t(h - 1) * w * 4;
assert(bottom[FI_RGBA_RED] == last_row[0] && bottom[FI_RGBA_GREEN] == last_row[1] &&
bottom[FI_RGBA_BLUE] == last_row[2] && bottom[FI_RGBA_ALPHA] == last_row[3]);
RGBQUAD top_left;
assert(FreeImage_GetPixelColor(image, 0, height - 1, &top_left));
assert(top_left.rgbRed == rgba[0] && top_left.rgbGreen == rgba[1] &&
top_left.rgbBlue == rgba[2]);
// Raw bits out top-down and back: the same pixels.
std::vector<BYTE> raw(size_t(width) * height * 4);
FreeImage_ConvertToRawBits(raw.data(), image, int(width * 4), 32, FI_RGBA_RED, FI_RGBA_GREEN,
FI_RGBA_BLUE, 1);
assert(raw[0] == rgba[2] && raw[1] == rgba[1] && raw[2] == rgba[0]);
FIBITMAP *back =
FreeImage_ConvertFromRawBits(raw.data(), int(width), int(height), int(width * 4), 32,
FI_RGBA_RED, FI_RGBA_GREEN, FI_RGBA_BLUE, 1);
assert(back && std::memcmp(FreeImage_GetScanLine(back, 0), bottom, width * 4) == 0);
stbi_image_free(rgba);
// 24 bits, premultiplied alpha, a rescale, and a PNG written and read again.
FIBITMAP *rgb = FreeImage_ConvertTo24Bits(image);
assert(rgb && FreeImage_GetBPP(rgb) == 24 && FreeImage_GetPitch(rgb) % 4 == 0);
assert(FreeImage_PreMultiplyWithAlpha(image));
FIBITMAP *small = FreeImage_Rescale(image, int(width / 4), int(height / 4), FILTER_LANCZOS3);
assert(small && FreeImage_GetWidth(small) == width / 4);
const std::string saved = dir + "/small.png";
assert(FreeImage_Save(FIF_PNG, small, saved.c_str()));
FIBITMAP *reloaded = FreeImage_Load(FreeImage_GetFileType(saved.c_str()), saved.c_str());
assert(reloaded && FreeImage_GetHeight(reloaded) == height / 4);
assert(!FreeImage_Save(FIF_WEBP, small, (dir + "/small.webp").c_str()));
for (FIBITMAP *dib : {image, back, rgb, small, reloaded})
FreeImage_Unload(dib);
// A GIF as ES-DE plays one: opened from its FILE*, a frame locked, its time read.
FreeImageIO io{read_proc, write_proc, seek_proc, tell_proc};
std::FILE *file = std::fopen(gif.c_str(), "rb");
assert(file);
FIMULTIBITMAP *animation =
FreeImage_OpenMultiBitmapFromHandle(FIF_GIF, &io, file, GIF_PLAYBACK);
assert(animation && FreeImage_GetPageCount(animation) == 2);
for (int page = 0; page < 2; page++)
{
FIBITMAP *frame = FreeImage_LockPage(animation, page);
assert(frame);
FITAG *tag = nullptr;
assert(FreeImage_GetMetadata(FIMD_ANIMATION, frame, "FrameTime", &tag) && tag);
assert(FreeImage_GetTagCount(tag) == 1 &&
*static_cast<const uint32_t *>(FreeImage_GetTagValue(tag)) == 200);
RGBQUAD pixel;
assert(FreeImage_GetPixelColor(frame, 0, 0, &pixel));
assert(page == 0 ? (pixel.rgbRed == 255 && pixel.rgbBlue == 0)
: (pixel.rgbBlue == 255 && pixel.rgbRed == 0));
FreeImage_UnlockPage(animation, frame, FALSE);
}
FreeImage_CloseMultiBitmap(animation);
// Opened again for the next frame: decoded once, kept.
FIMULTIBITMAP *again = FreeImage_OpenMultiBitmapFromHandle(FIF_GIF, &io, file, GIF_PLAYBACK);
assert(again && gif_cache.size() == 1 && FreeImage_GetPageCount(again) == 2);
FreeImage_CloseMultiBitmap(again);
std::fclose(file);
FreeImage_DeInitialise();
std::puts("esde_freeimage: layout, conversions, rescale, PNG save, GIF frames with times and "
"WebP PASS");
return 0;
}