feat(jpegenc): encode images through the shared JPEG encoder

This commit is contained in:
Qznad 2026-09-29 15:08:51 +01:00
parent 94c7319202
commit aad41aa907
5 changed files with 191 additions and 3 deletions

View File

@ -409,7 +409,7 @@ if(BUILD_TESTING)
endif()
add_executable(guest_jpeg_enc_tests tests/GuestJpegEnc.cpp)
target_include_directories(guest_jpeg_enc_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_jpeg_enc_tests PRIVATE libSceJpegEnc)
target_link_libraries(guest_jpeg_enc_tests PRIVATE libSceJpegEnc decoder_jpeg)
add_test(NAME guest_jpeg_enc COMMAND guest_jpeg_enc_tests)
if(WIN32)
set_tests_properties(guest_jpeg_enc PROPERTIES ENVIRONMENT_MODIFICATION

View File

@ -2,3 +2,4 @@ add_library(libSceJpegEnc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Ex
target_include_directories(libSceJpegEnc PRIVATE ${LIBS_INCLUDE_DIR})
set_target_properties(libSceJpegEnc PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(libSceJpegEnc)
target_link_libraries(libSceJpegEnc PRIVATE decoder_jpeg)

View File

@ -1,6 +1,11 @@
#include <algorithm>
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <new>
#include <stdexcept>
#include <vector>
#include "Decoder/Jpeg.hpp"
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
@ -37,6 +42,7 @@ constexpr std::uint8_t SAMPLING_TYPE_420 = 2;
constexpr std::uint32_t MAX_IMAGE_DIMENSION = 0xFFFF;
constexpr std::uint32_t MAX_IMAGE_PITCH = 0xFFFFFFF;
constexpr std::uint64_t MAX_IMAGE_SIZE = 0x7FFFFFFF;
constexpr int SUBSAMPLED_MAX_QUALITY = 90;
struct Encoder {
Encoder* self;
@ -65,6 +71,7 @@ std::int32_t validateEncodeParam(const JpegEncEncodeParam* param) {
if (param->image_size == 0 || param->jpeg_size == 0) return SCE_JPEG_ENC_ERROR_INVALID_SIZE;
if (param->image_width == 0 || param->image_height == 0) return SCE_JPEG_ENC_ERROR_INVALID_PARAM;
if (param->image_width > MAX_IMAGE_DIMENSION || param->image_height > MAX_IMAGE_DIMENSION) return SCE_JPEG_ENC_ERROR_INVALID_PARAM;
if (param->image_pitch == 0 || param->image_pitch > MAX_IMAGE_PITCH) return SCE_JPEG_ENC_ERROR_INVALID_PARAM;
if (!grayscaleInput && param->image_pitch % 4 != 0) return SCE_JPEG_ENC_ERROR_INVALID_PARAM;
@ -96,6 +103,57 @@ std::int32_t validateEncodeParam(const JpegEncEncodeParam* param) {
}
}
std::uint8_t clampToByte(float value) {
return static_cast<std::uint8_t>(std::clamp(value + 0.5f, 0.0f, 255.0f));
}
void yuvToRgb(std::uint8_t y, std::uint8_t u, std::uint8_t v, std::uint8_t* rgb) {
const float cb = static_cast<float>(u) - 128.0f;
const float cr = static_cast<float>(v) - 128.0f;
rgb[0] = clampToByte(y + 1.402f * cr);
rgb[1] = clampToByte(y - 0.344136f * cb - 0.714136f * cr);
rgb[2] = clampToByte(y + 1.772f * cb);
}
std::vector<std::uint8_t> toPackedPixels(const JpegEncEncodeParam& param, std::uint32_t channels) {
const auto* image = static_cast<const std::uint8_t*>(param.image);
const std::size_t width = param.image_width;
std::vector<std::uint8_t> pixels(width * param.image_height * channels);
for (std::size_t y = 0; y < param.image_height; ++y) {
const std::uint8_t* row = image + y * param.image_pitch;
std::uint8_t* out = pixels.data() + y * width * channels;
for (std::size_t x = 0; x < width; ++x) {
switch (param.pixel_format) {
case PIXEL_FORMAT_R8G8B8A8:
out[x * 3 + 0] = row[x * 4 + 0];
out[x * 3 + 1] = row[x * 4 + 1];
out[x * 3 + 2] = row[x * 4 + 2];
break;
case PIXEL_FORMAT_B8G8R8A8:
out[x * 3 + 0] = row[x * 4 + 2];
out[x * 3 + 1] = row[x * 4 + 1];
out[x * 3 + 2] = row[x * 4 + 0];
break;
case PIXEL_FORMAT_Y8U8Y8V8: {
const std::uint8_t* pair = row + (x / 2) * 4;
yuvToRgb(pair[(x % 2) * 2], pair[1], pair[3], out + x * 3);
break;
}
default:
out[x] = row[x];
break;
}
}
}
return pixels;
}
int toQuality(const JpegEncEncodeParam& param) {
int quality = 100 - param.compression_ratio * 99 / 255;
if (param.sampling_type != SAMPLING_TYPE_FULL) quality = std::min(quality, SUBSAMPLED_MAX_QUALITY);
return quality;
}
} // namespace
extern "C" {
@ -125,8 +183,19 @@ int32_t APS5_VABI sceJpegEncEncode(void* handle, const JpegEncEncodeParam* param
if (!toEncoder(handle)) return SCE_JPEG_ENC_ERROR_INVALID_HANDLE;
const std::int32_t result = validateEncodeParam(param);
if (result != 0) return result;
(void)output_info;
NotImplemented_nid_no_patch(__func__);
if (param->encode_mode == ENCODE_MODE_MJPEG) throw std::runtime_error("sceJpegEncEncode: MJPEG encode mode is not implemented");
if (param->restart_interval > 0) throw std::runtime_error("sceJpegEncEncode: restart interval is not implemented");
const std::uint32_t channels = param->pixel_format == PIXEL_FORMAT_Y8 ? 1 : 3;
const std::vector<std::uint8_t> pixels = toPackedPixels(*param, channels);
const std::vector<std::uint8_t> jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param));
if (jpeg.size() > param->jpeg_size) return SCE_JPEG_ENC_ERROR_INVALID_SIZE;
std::memcpy(param->jpeg, jpeg.data(), jpeg.size());
if (output_info) {
output_info->size = static_cast<std::uint32_t>(jpeg.size());
output_info->height = param->image_height;
}
return 0;
}

View File

@ -1,7 +1,11 @@
#include "SceTypes.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "Decoder/Jpeg.hpp"
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
#include <vector>
extern "C" {
std::int32_t APS5_VABI sceJpegEncQueryMemorySize(const JpegEncCreateParam*);
@ -15,6 +19,34 @@ static void Require(bool value) { if (!value) std::abort(); }
alignas(4) static unsigned char image[16 * 16 * 4];
static unsigned char jpeg[4096];
static bool IsJpeg(const unsigned char* data, std::uint32_t size) {
return size > 4 && data[0] == 0xFF && data[1] == 0xD8 && data[size - 2] == 0xFF && data[size - 1] == 0xD9;
}
static int AverageError(const std::vector<std::uint8_t>& expected, const std::vector<std::uint8_t>& actual) {
long total = 0;
for (std::size_t i = 0; i < expected.size(); ++i) total += expected[i] > actual[i] ? expected[i] - actual[i] : actual[i] - expected[i];
return static_cast<int>(total / static_cast<long>(expected.size()));
}
static std::vector<std::uint8_t> DecodeOutput(const JpegEncOutputInfo& info) {
Require(IsJpeg(jpeg, info.size));
const auto decoded = Decoder::Jpeg::Decode({jpeg, info.size});
Require(decoded.has_value());
Require(decoded->width == 16 && decoded->height == 16 && decoded->channels == 3);
return decoded->pixels;
}
template<typename TFunction>
static bool ThrowsRuntimeError(TFunction function) {
try {
function();
} catch (const std::runtime_error&) {
return true;
}
return false;
}
static JpegEncEncodeParam ValidEncodeParam() {
JpegEncEncodeParam param{};
param.image = image;
@ -113,6 +145,91 @@ int main() {
Require(encodeWith([](JpegEncEncodeParam& p) { p.pixel_format = 11; p.color_space = 2; p.sampling_type = 2; }) == invalidParam);
Require(encodeWith([](JpegEncEncodeParam& p) { p.pixel_format = 11; p.sampling_type = 0; }) == invalidParam);
constexpr std::uint32_t pitch = 16 * 4 + 16;
alignas(4) static unsigned char padded[16 * pitch];
std::vector<std::uint8_t> expected(16 * 16 * 3);
for (std::uint32_t y = 0; y < 16; ++y) {
for (std::uint32_t x = 0; x < 16; ++x) {
const std::uint8_t color[3] = {static_cast<std::uint8_t>(x * 16), static_cast<std::uint8_t>(y * 16), 96};
unsigned char* pixel = padded + y * pitch + x * 4;
pixel[0] = color[0];
pixel[1] = color[1];
pixel[2] = color[2];
pixel[3] = 255;
for (int c = 0; c < 3; ++c) expected[(y * 16 + x) * 3 + c] = color[c];
}
for (std::uint32_t x = 16 * 4; x < pitch; ++x) padded[y * pitch + x] = 0xEE;
}
JpegEncEncodeParam rgba = ValidEncodeParam();
rgba.image = padded;
rgba.image_size = sizeof(padded);
rgba.image_pitch = pitch;
rgba.compression_ratio = 0;
info = {};
Require(sceJpegEncEncode(handle, &rgba, &info) == 0);
Require(info.height == 16);
Require(AverageError(expected, DecodeOutput(info)) < 6);
for (std::uint32_t y = 0; y < 16; ++y) {
for (std::uint32_t x = 0; x < 16; ++x) {
unsigned char* pixel = padded + y * pitch + x * 4;
const unsigned char red = pixel[0];
pixel[0] = pixel[2];
pixel[2] = red;
}
}
JpegEncEncodeParam bgra = rgba;
bgra.pixel_format = 1;
info = {};
Require(sceJpegEncEncode(handle, &bgra, &info) == 0);
Require(AverageError(expected, DecodeOutput(info)) < 6);
alignas(4) static unsigned char yuyv[16 * 16 * 2];
for (std::size_t i = 0; i < sizeof(yuyv); i += 4) {
yuyv[i + 0] = 100;
yuyv[i + 1] = 128;
yuyv[i + 2] = 100;
yuyv[i + 3] = 128;
}
JpegEncEncodeParam yuv = ValidEncodeParam();
yuv.image = yuyv;
yuv.image_size = sizeof(yuyv);
yuv.image_pitch = 16 * 2;
yuv.pixel_format = 10;
info = {};
Require(sceJpegEncEncode(handle, &yuv, &info) == 0);
Require(AverageError(std::vector<std::uint8_t>(16 * 16 * 3, 100), DecodeOutput(info)) < 3);
static unsigned char gray[16 * 16];
std::vector<std::uint8_t> expectedGray(16 * 16 * 3);
for (std::size_t i = 0; i < sizeof(gray); ++i) {
gray[i] = static_cast<unsigned char>(((i % 16) + (i / 16)) * 8);
for (int c = 0; c < 3; ++c) expectedGray[i * 3 + c] = gray[i];
}
JpegEncEncodeParam y8 = ValidEncodeParam();
y8.image = gray;
y8.image_size = sizeof(gray);
y8.image_pitch = 16;
y8.pixel_format = 11;
y8.color_space = 2;
y8.sampling_type = 0;
info = {};
Require(sceJpegEncEncode(handle, &y8, &info) == 0);
Require(AverageError(expectedGray, DecodeOutput(info)) < 6);
std::memset(jpeg, 0, sizeof(jpeg));
Require(sceJpegEncEncode(handle, &rgba, nullptr) == 0);
Require(jpeg[0] == 0xFF && jpeg[1] == 0xD8);
JpegEncEncodeParam tooSmall = rgba;
tooSmall.jpeg_size = 16;
Require(sceJpegEncEncode(handle, &tooSmall, &info) == invalidSize);
Require(encodeWith([](JpegEncEncodeParam& p) { p.image_width = 0; }) == invalidParam);
Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.encode_mode = 1; }); }));
Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = 1; }); }));
Require(sceJpegEncDelete(handle) == 0);
return 0;

View File

@ -32,6 +32,7 @@ Throughout the project, every function at every stage either **does exactly what
- [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx).
- The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability.
- [libSceJpegEnc](../../core/libs/prx/libSceJpegEnc/Export.cpp) encodes 4:2:2 sampling requests with 4:2:0 chroma subsampling: the [JPEG encoder](../../core/Decoder/Jpeg/src/Jpeg.cpp) (stb) only produces 4:2:0 and 4:4:4.
- [Relinker](../../core/relinker/elfpatcher/src) doesn't add an icon to the generated executable. This should be done without adding dependencies (only standard).
- `--to-intel` does not lower the register form of INSERTQ (`F2 0F 79`) nor MONITORX/MWAITX/CLZERO/RDPRU/MCOMMIT; the [lowering](../../core/relinker/codegen/src/x86/Sse4aLowering.cpp) fails the relink instead. SHA-NI is not substituted.
- The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust VEX/0F38/0F3A RIP-relative operands, data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length.