diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index b65255a01..17828222a 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -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 diff --git a/core/libs/prx/libSceJpegEnc/CMakeLists.txt b/core/libs/prx/libSceJpegEnc/CMakeLists.txt index 16a34878c..4ddeab93a 100644 --- a/core/libs/prx/libSceJpegEnc/CMakeLists.txt +++ b/core/libs/prx/libSceJpegEnc/CMakeLists.txt @@ -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) diff --git a/core/libs/prx/libSceJpegEnc/Export.cpp b/core/libs/prx/libSceJpegEnc/Export.cpp index 051a6d2fe..c12cbfe12 100644 --- a/core/libs/prx/libSceJpegEnc/Export.cpp +++ b/core/libs/prx/libSceJpegEnc/Export.cpp @@ -1,6 +1,11 @@ +#include #include #include +#include #include +#include +#include +#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::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(u) - 128.0f; + const float cr = static_cast(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 toPackedPixels(const JpegEncEncodeParam& param, std::uint32_t channels) { + const auto* image = static_cast(param.image); + const std::size_t width = param.image_width; + std::vector 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 pixels = toPackedPixels(*param, channels); + const std::vector 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(jpeg.size()); + output_info->height = param->image_height; + } return 0; } diff --git a/core/libs/tests/GuestJpegEnc.cpp b/core/libs/tests/GuestJpegEnc.cpp index 923b98d22..7f5983747 100644 --- a/core/libs/tests/GuestJpegEnc.cpp +++ b/core/libs/tests/GuestJpegEnc.cpp @@ -1,7 +1,11 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "Decoder/Jpeg.hpp" #include #include +#include +#include +#include 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& expected, const std::vector& 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(total / static_cast(expected.size())); +} + +static std::vector 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 +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 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(x * 16), static_cast(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(16 * 16 * 3, 100), DecodeOutput(info)) < 3); + + static unsigned char gray[16 * 16]; + std::vector expectedGray(16 * 16 * 3); + for (std::size_t i = 0; i < sizeof(gray); ++i) { + gray[i] = static_cast(((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; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 8468605e0..710931005 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -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.