feat(libScePngEnc): implement PNG encoding

scePngEncQueryMemorySize, scePngEncCreate, scePngEncEncode and
scePngEncDelete encode R8G8B8A8 and B8G8R8A8 images with a pitch into
RGB or RGBA PNGs using the shared encoder in core/Decoder/Png.
Decoder::Png::Encode takes optional compression level and filter
options so the encode parameters are honoured.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Adrià Franch 2026-10-01 19:14:52 +02:00
parent 01eaae0bf9
commit 58f90eea9d
8 changed files with 317 additions and 14 deletions

View File

@ -35,8 +35,13 @@ std::optional<Header> ParseHeader(std::span<const std::uint8_t> png);
std::optional<Image> Decode(std::span<const std::uint8_t> png);
struct EncodeOptions {
int compressionLevel = 8;
int filter = -1;
};
std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint32_t width, std::uint32_t height,
std::uint32_t channels);
std::uint32_t channels, EncodeOptions options = {});
} // namespace Decoder::Png

View File

@ -4,6 +4,7 @@
#include <array>
#include <climits>
#include <cstddef>
#include <mutex>
#include <stdexcept>
#define STB_IMAGE_WRITE_STATIC
@ -102,8 +103,11 @@ std::optional<Image> Decode(std::span<const std::uint8_t> png) {
}
std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint32_t width, std::uint32_t height,
std::uint32_t channels) {
std::uint32_t channels, EncodeOptions options) {
if (channels < 1 || channels > 4) throw std::invalid_argument("Png::Encode: channels must be 1-4");
if (options.compressionLevel < 0 || options.compressionLevel > 9 || options.filter < -1 || options.filter > 4) {
throw std::invalid_argument("Png::Encode: unsupported options");
}
if (width == 0 || height == 0 || static_cast<std::uint64_t>(width) * channels > INT_MAX || height > INT_MAX) {
throw std::invalid_argument("Png::Encode: unsupported image size");
}
@ -113,6 +117,10 @@ std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint
std::vector<std::uint8_t> output;
const int stride = static_cast<int>(width * channels);
static std::mutex optionsMutex;
std::lock_guard lock(optionsMutex);
stbi_write_png_compression_level = options.compressionLevel;
stbi_write_force_png_filter = options.filter;
const int written = stbi_write_png_to_func(appendBytes, &output, static_cast<int>(width), static_cast<int>(height),
static_cast<int>(channels), pixels.data(), stride);
if (written == 0) throw std::runtime_error("Png::Encode: encoding failed");

View File

@ -140,6 +140,19 @@ int main() {
Require(ThrowsInvalidArgument([&] { Decoder::Png::Encode(pixels, 4, 4, 5); }));
Require(ThrowsInvalidArgument([&] { Decoder::Png::Encode(pixels, 0, 4, 4); }));
Require(ThrowsInvalidArgument([&] { Decoder::Png::Encode(pixels, 4, 5, 4); }));
Require(ThrowsInvalidArgument([&] { Decoder::Png::Encode(pixels, 4, 4, 4, {10, -1}); }));
Require(ThrowsInvalidArgument([&] { Decoder::Png::Encode(pixels, 4, 4, 4, {8, 5}); }));
std::vector<std::uint8_t> gradient(64 * 64 * 3);
for (std::size_t i = 0; i < gradient.size(); ++i) gradient[i] = static_cast<std::uint8_t>(i / 3 % 64 * 4);
const auto stored = Decoder::Png::Encode(gradient, 64, 64, 3, {0, 0});
const auto compressed = Decoder::Png::Encode(gradient, 64, 64, 3, {9, 1});
Require(compressed.size() < stored.size());
for (const auto& png : {stored, compressed}) {
const auto image = Decoder::Png::Decode(png);
Require(image.has_value() && image->width == 64 && image->height == 64);
for (std::size_t pixel = 0; pixel < 64 * 64; ++pixel) Require(image->pixels[pixel * 4] == gradient[pixel * 3]);
}
return 0;
}

View File

@ -1406,6 +1406,34 @@ struct ContentExportInitParam2 {
struct ContentSearchInitParam { std::size_t memory_size; };
struct PngEncCreateParam {
std::uint32_t this_size;
std::uint32_t attribute;
std::uint32_t max_image_width;
std::uint32_t max_filter_number;
};
struct PngEncEncodeParam {
const std::uint8_t* image_mem_addr;
std::uint8_t* png_mem_addr;
std::uint32_t image_mem_size;
std::uint32_t png_mem_size;
std::uint32_t image_width;
std::uint32_t image_height;
std::uint32_t image_pitch;
std::uint16_t pixel_format;
std::uint16_t color_space;
std::uint16_t bit_depth;
std::uint16_t clut_number;
std::uint16_t filter_type;
std::uint16_t compression_level;
};
struct PngEncOutputInfo {
std::uint32_t data_size;
std::uint32_t processed_height;
};
struct ContentDeleteInitParam {
char reserved1[4];
std::size_t heap_size;

View File

@ -0,0 +1,23 @@
add_library(libScePngEnc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp)
target_include_directories(libScePngEnc PRIVATE ${LIBS_INCLUDE_DIR})
set_target_properties(libScePngEnc PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(libScePngEnc)
target_link_libraries(libScePngEnc PRIVATE decoder_png)
if(MINGW)
foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static)
string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
endforeach()
endif()
if(BUILD_TESTING)
add_test_executable(guest_png_enc_tests ${LIBS_INCLUDE_DIR}/tests/GuestPngEnc.cpp)
target_include_directories(guest_png_enc_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_png_enc_tests PRIVATE libScePngEnc decoder_png libc)
configure_windows_unwind(guest_png_enc_tests)
add_test(NAME guest_png_enc COMMAND guest_png_enc_tests)
if(WIN32)
set_tests_properties(guest_png_enc PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libScePngEnc>")
endif()
endif()

View File

@ -1,28 +1,135 @@
#include <algorithm>
#include <cstdint>
#include <cstddef>
#include <exception>
#include <vector>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "Decoder/Png.hpp"
namespace {
constexpr int PNG_ENC_ERROR_INVALID_ADDR = static_cast<int>(0x80690101);
constexpr int PNG_ENC_ERROR_INVALID_SIZE = static_cast<int>(0x80690102);
constexpr int PNG_ENC_ERROR_INVALID_PARAM = static_cast<int>(0x80690103);
constexpr int PNG_ENC_ERROR_INVALID_HANDLE = static_cast<int>(0x80690104);
constexpr int PNG_ENC_ERROR_DATA_OVERFLOW = static_cast<int>(0x80690110);
constexpr int PNG_ENC_ERROR_FATAL = static_cast<int>(0x80690120);
constexpr std::uint32_t MAX_IMAGE_WIDTH = 1000000;
constexpr std::uint32_t MAX_IMAGE_HEIGHT = 1000000;
constexpr std::uint32_t MAX_FILTER_NUMBER = 5;
constexpr std::uint16_t COLOR_SPACE_RGB = 3;
constexpr std::uint16_t COLOR_SPACE_RGBA = 19;
constexpr std::uint16_t PIXEL_FORMAT_R8G8B8A8 = 0;
constexpr std::uint16_t PIXEL_FORMAT_B8G8R8A8 = 1;
constexpr std::uint16_t FILTER_SUB = 1;
constexpr std::uint16_t FILTER_UP = 2;
constexpr std::uint16_t FILTER_AVERAGE = 4;
constexpr std::uint16_t FILTER_PAETH = 8;
constexpr std::uint16_t FILTER_ALL = FILTER_SUB | FILTER_UP | FILTER_AVERAGE | FILTER_PAETH;
constexpr std::uint64_t CONTEXT_MAGIC = 0x434E45474E505341ull;
struct PngEncContext {
std::uint64_t magic;
std::uint32_t maxImageWidth;
std::uint32_t reserved;
};
static_assert(sizeof(PngEncCreateParam) == 16);
static_assert(sizeof(PngEncEncodeParam) == 48);
static_assert(sizeof(PngEncOutputInfo) == 8);
int validateCreateParam(const PngEncCreateParam* param) {
if (!param) return PNG_ENC_ERROR_INVALID_ADDR;
if (param->attribute != 0 || param->max_filter_number > MAX_FILTER_NUMBER) return PNG_ENC_ERROR_INVALID_PARAM;
if (param->max_image_width == 0 || param->max_image_width > MAX_IMAGE_WIDTH) return PNG_ENC_ERROR_INVALID_SIZE;
return 0;
}
PngEncContext* context(void* handle) {
auto* ctx = static_cast<PngEncContext*>(handle);
return ctx && ctx->magic == CONTEXT_MAGIC ? ctx : nullptr;
}
int filterMode(std::uint16_t filterType) {
if (filterType == FILTER_ALL) return -1;
if (filterType & FILTER_SUB) return 1;
if (filterType & FILTER_UP) return 2;
if (filterType & FILTER_AVERAGE) return 3;
if (filterType & FILTER_PAETH) return 4;
return 0;
}
}
extern "C" {
int APS5_VABI scePngEncCreate() {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI scePngEncQueryMemorySize(const PngEncCreateParam* param) {
if (const int result = validateCreateParam(param); result != 0) return result;
return sizeof(PngEncContext);
}
int APS5_VABI scePngEncDelete() {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI scePngEncCreate(const PngEncCreateParam* param, void* memoryAddress, uint32_t memorySize, void** handle) {
if (const int result = validateCreateParam(param); result != 0) return result;
if (!memoryAddress || !handle) return PNG_ENC_ERROR_INVALID_ADDR;
if (memorySize < sizeof(PngEncContext)) return PNG_ENC_ERROR_INVALID_SIZE;
*static_cast<PngEncContext*>(memoryAddress) = {CONTEXT_MAGIC, param->max_image_width, 0};
*handle = memoryAddress;
return 0;
}
int APS5_VABI scePngEncEncode() {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI scePngEncDelete(void* handle) {
PngEncContext* ctx = context(handle);
if (!ctx) return PNG_ENC_ERROR_INVALID_HANDLE;
ctx->magic = 0;
return 0;
}
int APS5_VABI scePngEncQueryMemorySize() {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI scePngEncEncode(void* handle, const PngEncEncodeParam* param, PngEncOutputInfo* outputInfo) {
const PngEncContext* ctx = context(handle);
if (!ctx) return PNG_ENC_ERROR_INVALID_HANDLE;
if (!param) return PNG_ENC_ERROR_INVALID_PARAM;
if (!param->image_mem_addr || !param->png_mem_addr) return PNG_ENC_ERROR_INVALID_ADDR;
if ((param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8)
|| (param->color_space != COLOR_SPACE_RGB && param->color_space != COLOR_SPACE_RGBA) || param->bit_depth != 8
|| param->clut_number != 0 || (param->filter_type & ~FILTER_ALL) != 0 || param->compression_level > 9) {
return PNG_ENC_ERROR_INVALID_PARAM;
}
const std::uint32_t width = param->image_width;
const std::uint32_t height = param->image_height;
if (width == 0 || height == 0 || width > ctx->maxImageWidth || height > MAX_IMAGE_HEIGHT || param->png_mem_size == 0
|| param->image_pitch < width * 4 || static_cast<std::uint64_t>(param->image_pitch) * (height - 1) + width * 4 > param->image_mem_size) {
return PNG_ENC_ERROR_INVALID_SIZE;
}
const std::uint32_t channels = param->color_space == COLOR_SPACE_RGBA ? 4 : 3;
const bool bgr = param->pixel_format == PIXEL_FORMAT_B8G8R8A8;
std::vector<std::uint8_t> pixels;
std::vector<std::uint8_t> png;
try {
pixels.resize(static_cast<std::size_t>(width) * height * channels);
std::uint8_t* destination = pixels.data();
for (std::uint32_t y = 0; y < height; ++y) {
const std::uint8_t* source = param->image_mem_addr + static_cast<std::size_t>(y) * param->image_pitch;
for (std::uint32_t x = 0; x < width; ++x, source += 4) {
*destination++ = source[bgr ? 2 : 0];
*destination++ = source[1];
*destination++ = source[bgr ? 0 : 2];
if (channels == 4) *destination++ = source[3];
}
}
png = Decoder::Png::Encode(pixels, width, height, channels, {param->compression_level, filterMode(param->filter_type)});
} catch (const std::exception&) {
return PNG_ENC_ERROR_FATAL;
}
const bool overflow = png.size() > param->png_mem_size;
if (outputInfo) *outputInfo = {overflow ? 0 : static_cast<std::uint32_t>(png.size()), overflow ? 0 : height};
if (overflow) return PNG_ENC_ERROR_DATA_OVERFLOW;
std::copy(png.begin(), png.end(), param->png_mem_addr);
return static_cast<int>(png.size());
}
}

View File

@ -0,0 +1,118 @@
#include "SceTypes.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "Decoder/Png.hpp"
#include <cstdint>
#include <cstdlib>
#include <vector>
extern "C" {
int APS5_VABI scePngEncQueryMemorySize(const PngEncCreateParam*);
int APS5_VABI scePngEncCreate(const PngEncCreateParam*, void*, std::uint32_t, void**);
int APS5_VABI scePngEncDelete(void*);
int APS5_VABI scePngEncEncode(void*, const PngEncEncodeParam*, PngEncOutputInfo*);
}
static void Require(bool value) { if (!value) std::abort(); }
int main() {
constexpr int invalidAddr = static_cast<int>(0x80690101);
constexpr int invalidSize = static_cast<int>(0x80690102);
constexpr int invalidParam = static_cast<int>(0x80690103);
constexpr int invalidHandle = static_cast<int>(0x80690104);
constexpr int dataOverflow = static_cast<int>(0x80690110);
constexpr std::uint32_t width = 5;
constexpr std::uint32_t height = 3;
constexpr std::uint32_t pitch = 24;
PngEncCreateParam create{sizeof(PngEncCreateParam), 0, 16, 5};
const int memorySize = scePngEncQueryMemorySize(&create);
Require(memorySize > 0);
Require(scePngEncQueryMemorySize(nullptr) == invalidAddr);
PngEncCreateParam bad = create;
bad.attribute = 1;
Require(scePngEncQueryMemorySize(&bad) == invalidParam);
bad = create;
bad.max_filter_number = 6;
Require(scePngEncQueryMemorySize(&bad) == invalidParam);
bad = create;
bad.max_image_width = 0;
Require(scePngEncQueryMemorySize(&bad) == invalidSize);
bad.max_image_width = 1000001;
Require(scePngEncQueryMemorySize(&bad) == invalidSize);
std::vector<std::uint64_t> memory((memorySize + 7) / 8);
void* handle = nullptr;
Require(scePngEncCreate(&create, nullptr, memorySize, &handle) == invalidAddr);
Require(scePngEncCreate(&create, memory.data(), memorySize - 1, &handle) == invalidSize);
Require(scePngEncCreate(&create, memory.data(), memorySize, &handle) == 0 && handle == memory.data());
std::vector<std::uint8_t> image(pitch * height, 0xEE);
for (std::uint32_t y = 0; y < height; ++y) {
for (std::uint32_t x = 0; x < width; ++x) {
std::uint8_t* pixel = &image[y * pitch + x * 4];
pixel[0] = static_cast<std::uint8_t>(x * 40);
pixel[1] = static_cast<std::uint8_t>(y * 80);
pixel[2] = static_cast<std::uint8_t>(200 - x * 10);
pixel[3] = static_cast<std::uint8_t>(100 + x + y);
}
}
std::vector<std::uint8_t> png(4096);
PngEncEncodeParam encode{image.data(), png.data(), static_cast<std::uint32_t>(image.size()), static_cast<std::uint32_t>(png.size()),
width, height, pitch, 0, 19, 8, 0, 15, 6};
for (const std::uint16_t pixelFormat : {0, 1}) {
for (const std::uint16_t colorSpace : {3, 19}) {
encode.pixel_format = pixelFormat;
encode.color_space = colorSpace;
PngEncOutputInfo info{};
const int size = scePngEncEncode(handle, &encode, &info);
Require(size > 0 && info.data_size == static_cast<std::uint32_t>(size) && info.processed_height == height);
const auto header = Decoder::Png::ParseHeader(std::span<const std::uint8_t>(png.data(), size));
Require(header.has_value());
Require(header->colorType == (colorSpace == 19 ? Decoder::Png::ColorType::Rgba : Decoder::Png::ColorType::Rgb));
const auto decoded = Decoder::Png::Decode(std::span<const std::uint8_t>(png.data(), size));
Require(decoded.has_value() && decoded->width == width && decoded->height == height);
for (std::uint32_t y = 0; y < height; ++y) {
for (std::uint32_t x = 0; x < width; ++x) {
const std::uint8_t* source = &image[y * pitch + x * 4];
const std::uint8_t* result = &decoded->pixels[(y * width + x) * 4];
Require(result[0] == source[pixelFormat == 1 ? 2 : 0] && result[1] == source[1]);
Require(result[2] == source[pixelFormat == 1 ? 0 : 2]);
Require(result[3] == (colorSpace == 19 ? source[3] : 0xFF));
}
}
}
}
encode.png_mem_size = 20;
PngEncOutputInfo info{1, 1};
Require(scePngEncEncode(handle, &encode, &info) == dataOverflow && info.data_size == 0 && info.processed_height == 0);
encode.png_mem_size = static_cast<std::uint32_t>(png.size());
PngEncEncodeParam invalid = encode;
Require(scePngEncEncode(handle, nullptr, nullptr) == invalidParam);
invalid.image_mem_addr = nullptr;
Require(scePngEncEncode(handle, &invalid, nullptr) == invalidAddr);
for (auto change : {+[](PngEncEncodeParam& p) { p.pixel_format = 2; }, +[](PngEncEncodeParam& p) { p.color_space = 4; },
+[](PngEncEncodeParam& p) { p.bit_depth = 16; }, +[](PngEncEncodeParam& p) { p.clut_number = 1; },
+[](PngEncEncodeParam& p) { p.filter_type = 16; }, +[](PngEncEncodeParam& p) { p.compression_level = 10; }}) {
invalid = encode;
change(invalid);
Require(scePngEncEncode(handle, &invalid, nullptr) == invalidParam);
}
for (auto change : {+[](PngEncEncodeParam& p) { p.image_width = 0; }, +[](PngEncEncodeParam& p) { p.image_width = 17; },
+[](PngEncEncodeParam& p) { p.image_pitch = 19; }, +[](PngEncEncodeParam& p) { p.image_mem_size = 67; },
+[](PngEncEncodeParam& p) { p.png_mem_size = 0; }}) {
invalid = encode;
change(invalid);
Require(scePngEncEncode(handle, &invalid, nullptr) == invalidSize);
}
invalid = encode;
invalid.image_mem_size = pitch * (height - 1) + width * 4;
Require(scePngEncEncode(handle, &invalid, nullptr) > 0);
Require(scePngEncDelete(handle) == 0);
Require(scePngEncDelete(handle) == invalidHandle);
Require(scePngEncEncode(handle, &encode, nullptr) == invalidHandle);
Require(scePngEncDelete(nullptr) == invalidHandle);
}

View File

@ -47,4 +47,5 @@ Throughout the project, every function at every stage either **does exactly what
- `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows.
- [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates only the 101-key (US) arrangement and throws for the 106-key (Japanese) one; Ctrl and Alt do not change the character.
- [libSceAudiodec](../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture.
- [libScePngEnc](../../core/libs/prx/libScePngEnc/Export.cpp) honours `filter_type` only as all filters (adaptive) or a single filter: the [PNG encoder](../../core/Decoder/Png/src/Png.cpp) (stb) cannot restrict adaptive filtering to a subset, so the first filter in the mask is used.
- [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations; its parameter ABI remains unverified.