diff --git a/.gitmodules b/.gitmodules index 0a5f83fb7..fc2e48631 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "3rdparty/stb"] path = 3rdparty/stb url = https://github.com/nothings/stb.git +[submodule "3rdparty/libjpeg-turbo"] + path = 3rdparty/libjpeg-turbo + url = https://github.com/libjpeg-turbo/libjpeg-turbo.git diff --git a/3rdparty/libjpeg-turbo b/3rdparty/libjpeg-turbo new file mode 160000 index 000000000..c85e6b905 --- /dev/null +++ b/3rdparty/libjpeg-turbo @@ -0,0 +1 @@ +Subproject commit c85e6b905bf237038faa936dab160ebfc5da0344 diff --git a/core/Decoder/Jpeg/CMakeLists.txt b/core/Decoder/Jpeg/CMakeLists.txt index 2280f398b..fcb338b8c 100644 --- a/core/Decoder/Jpeg/CMakeLists.txt +++ b/core/Decoder/Jpeg/CMakeLists.txt @@ -3,10 +3,56 @@ function(add_jpeg_decoder target runtimeTarget) message(FATAL_ERROR "JPEG decoder requires an existing runtime target: ${runtimeTarget}") endif() + include(ExternalProject) + if(NOT TARGET jpeg_turbo_external) + set(jpeg_turbo_prefix "${CMAKE_BINARY_DIR}/libjpeg-turbo") + if(WIN32) + if(MSVC) + set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/turbojpeg-static.lib") + else() + set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/libturbojpeg.a") + endif() + else() + set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/libturbojpeg.a") + endif() + file(MAKE_DIRECTORY "${jpeg_turbo_prefix}/include") + ExternalProject_Add(jpeg_turbo_external + SOURCE_DIR "${PROJECT_SOURCE_DIR}/3rdparty/libjpeg-turbo" + BINARY_DIR "${CMAKE_BINARY_DIR}/libjpeg-turbo-build" + INSTALL_DIR "${jpeg_turbo_prefix}" + CMAKE_ARGS + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + -DCMAKE_INSTALL_PREFIX=${jpeg_turbo_prefix} + -DBUILD_SHARED_LIBS=OFF + -DENABLE_STATIC=ON + -DENABLE_SHARED=OFF + -DWITH_SIMD=OFF + -DWITH_TURBOJPEG=ON + -DWITH_TOOLS=OFF + -DWITH_TESTS=OFF + -DCMAKE_INSTALL_LIBDIR=lib + -DCMAKE_C_VISIBILITY_PRESET=hidden + -DCMAKE_VISIBILITY_INLINES_HIDDEN=ON + -DCMAKE_POSITION_INDEPENDENT_CODE=ON + -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} + -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} + -DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER} + -DCMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER} + BUILD_BYPRODUCTS + "${jpeg_turbo_library}" + ) + add_library(jpeg_turbo STATIC IMPORTED GLOBAL) + set_target_properties(jpeg_turbo PROPERTIES + IMPORTED_LOCATION "${jpeg_turbo_library}" + INTERFACE_INCLUDE_DIRECTORIES "${PROJECT_SOURCE_DIR}/3rdparty/libjpeg-turbo/src;${jpeg_turbo_prefix}/include" + ) + add_dependencies(jpeg_turbo jpeg_turbo_external) + endif() + add_library(${target} STATIC ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/src/Jpeg.cpp) target_include_directories(${target} PUBLIC ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/include) target_include_directories(${target} SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/3rdparty/stb) - target_link_libraries(${target} PRIVATE ${runtimeTarget}) + target_link_libraries(${target} PRIVATE ${runtimeTarget} jpeg_turbo) set_target_properties(${target} PROPERTIES CXX_EXTENSIONS OFF CXX_VISIBILITY_PRESET hidden diff --git a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp index 275217fb2..60142fc36 100644 --- a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp +++ b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp @@ -21,8 +21,14 @@ struct Header { std::uint32_t channels; }; +enum class Sampling { + Yuv444, + Yuv422, + Yuv420, +}; + std::vector Encode(std::span pixels, std::uint32_t width, std::uint32_t height, - std::uint32_t channels, int quality); + std::uint32_t channels, int quality, Sampling sampling = Sampling::Yuv444); std::optional
ParseHeader(std::span jpeg); diff --git a/core/Decoder/Jpeg/src/Jpeg.cpp b/core/Decoder/Jpeg/src/Jpeg.cpp index 74a50c4f8..c8f1e1eca 100644 --- a/core/Decoder/Jpeg/src/Jpeg.cpp +++ b/core/Decoder/Jpeg/src/Jpeg.cpp @@ -4,16 +4,12 @@ #include #include -#define STB_IMAGE_WRITE_STATIC -#define STB_IMAGE_WRITE_IMPLEMENTATION -#define STBI_WRITE_NO_STDIO -#include "stb_image_write.h" - #define STB_IMAGE_STATIC #define STB_IMAGE_IMPLEMENTATION #define STBI_ONLY_JPEG #define STBI_NO_STDIO #include "stb_image.h" +#include "turbojpeg.h" namespace Decoder::Jpeg { @@ -21,16 +17,39 @@ namespace { constexpr std::uint32_t MAX_DIMENSION = 0xFFFF; -void appendBytes(void* context, void* data, int size) { - auto* output = static_cast*>(context); - const auto* bytes = static_cast(data); - output->insert(output->end(), bytes, bytes + size); +class Handle { +public: + Handle() : handle(tj3Init(TJINIT_COMPRESS)) { + if (!handle) throw std::runtime_error("Jpeg::Encode: encoder initialization failed"); + } + ~Handle() { tj3Destroy(handle); } + Handle(const Handle&) = delete; + Handle& operator=(const Handle&) = delete; + operator tjhandle() const { return handle; } + +private: + tjhandle handle; +}; + +class Buffer { +public: + ~Buffer() { tj3Free(data); } + unsigned char* data = nullptr; +}; + +int toSubsampling(Sampling sampling) { + switch (sampling) { + case Sampling::Yuv444: return TJSAMP_444; + case Sampling::Yuv422: return TJSAMP_422; + case Sampling::Yuv420: return TJSAMP_420; + } + throw std::invalid_argument("Jpeg::Encode: invalid sampling"); } } // namespace std::vector Encode(std::span pixels, std::uint32_t width, std::uint32_t height, - std::uint32_t channels, int quality) { + std::uint32_t channels, int quality, Sampling sampling) { if (width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) { throw std::invalid_argument("Jpeg::Encode: unsupported image size"); } @@ -39,12 +58,18 @@ std::vector Encode(std::span pixels, std::uint if (pixels.size() < static_cast(width) * height * channels) { throw std::invalid_argument("Jpeg::Encode: pixel buffer is too small"); } - - std::vector output; - const int written = stbi_write_jpg_to_func(appendBytes, &output, static_cast(width), static_cast(height), - static_cast(channels), pixels.data(), quality); - if (written == 0) throw std::runtime_error("Jpeg::Encode: encoding failed"); - return output; + const int subsampling = toSubsampling(sampling); + Handle handle; + if (tj3Set(handle, TJPARAM_QUALITY, quality) < 0 || tj3Set(handle, TJPARAM_SUBSAMP, channels == 1 ? TJSAMP_GRAY : subsampling) < 0) { + throw std::runtime_error("Jpeg::Encode: encoder configuration failed"); + } + Buffer buffer; + std::size_t size = 0; + const int pixelFormat = channels == 1 ? TJPF_GRAY : TJPF_RGB; + if (tj3Compress8(handle, pixels.data(), static_cast(width), 0, static_cast(height), pixelFormat, &buffer.data, &size) < 0) { + throw std::runtime_error("Jpeg::Encode: encoding failed"); + } + return {buffer.data, buffer.data + size}; } std::optional
ParseHeader(std::span jpeg) { diff --git a/core/Decoder/Jpeg/tests/Jpeg.cpp b/core/Decoder/Jpeg/tests/Jpeg.cpp index 38e2aa42d..d396a00f2 100644 --- a/core/Decoder/Jpeg/tests/Jpeg.cpp +++ b/core/Decoder/Jpeg/tests/Jpeg.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include static void Require(bool value) { if (!value) std::abort(); } @@ -25,6 +26,25 @@ static int Difference(std::uint8_t left, std::uint8_t right) { return left > right ? left - right : right - left; } +struct FrameInfo { int components; std::uint8_t sampling; }; + +static FrameInfo ReadFrame(const std::vector& jpeg) { + std::size_t offset = 2; + while (offset + 3 < jpeg.size()) { + Require(jpeg[offset++] == 0xFF); + while (offset < jpeg.size() && jpeg[offset] == 0xFF) ++offset; + Require(offset < jpeg.size()); + const std::uint8_t marker = jpeg[offset++]; + if (marker == 0xDA || marker == 0xD9) break; + Require(offset + 1 < jpeg.size()); + const std::size_t length = (static_cast(jpeg[offset]) << 8) | jpeg[offset + 1]; + Require(length >= 2 && offset + length <= jpeg.size()); + if (marker >= 0xC0 && marker <= 0xC3) return {jpeg[offset + 7], jpeg[offset + 9]}; + offset += length; + } + std::abort(); +} + int main() { constexpr std::uint32_t width = 32; constexpr std::uint32_t height = 24; @@ -41,6 +61,7 @@ int main() { const std::vector rgbJpeg = Decoder::Jpeg::Encode(rgb, width, height, 3, 90); Require(IsJpeg(rgbJpeg)); + Require(ReadFrame(rgbJpeg).components == 3 && ReadFrame(rgbJpeg).sampling == 0x11); const auto rgbImage = Decoder::Jpeg::Decode(rgbJpeg); Require(rgbImage.has_value()); Require(rgbImage->width == width && rgbImage->height == height && rgbImage->channels == 3); @@ -55,6 +76,7 @@ int main() { } const std::vector grayJpeg = Decoder::Jpeg::Encode(gray, width, height, 1, 90); Require(IsJpeg(grayJpeg)); + Require(ReadFrame(grayJpeg).components == 1); const auto grayImage = Decoder::Jpeg::Decode(grayJpeg); Require(grayImage.has_value()); Require(grayImage->width == width && grayImage->height == height); @@ -69,6 +91,16 @@ int main() { Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, 0x10000, 1, 3, 90); })); Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height + 1, 3, 90); })); + for (const auto [sampling, factor] : {std::pair{Decoder::Jpeg::Sampling::Yuv444, std::uint8_t{0x11}}, + std::pair{Decoder::Jpeg::Sampling::Yuv422, std::uint8_t{0x21}}, + std::pair{Decoder::Jpeg::Sampling::Yuv420, std::uint8_t{0x22}}}) { + const auto encoded = Decoder::Jpeg::Encode(rgb, 31, 23, 3, 1, sampling); + Require(ReadFrame(encoded).components == 3 && ReadFrame(encoded).sampling == factor); + const auto highQuality = Decoder::Jpeg::Encode(rgb, 31, 23, 3, 100, sampling); + Require(ReadFrame(highQuality).components == 3 && ReadFrame(highQuality).sampling == factor); + } + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, static_cast(3)); })); + Require(!Decoder::Jpeg::Decode({}).has_value()); const std::vector garbage{1, 2, 3, 4, 5, 6, 7, 8}; Require(!Decoder::Jpeg::Decode(garbage).has_value()); diff --git a/core/libs/prx/libSceJpegEnc/Export.cpp b/core/libs/prx/libSceJpegEnc/Export.cpp index d52ad86ac..2d4c66657 100644 --- a/core/libs/prx/libSceJpegEnc/Export.cpp +++ b/core/libs/prx/libSceJpegEnc/Export.cpp @@ -41,7 +41,6 @@ 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; @@ -148,9 +147,16 @@ std::vector toPackedPixels(const JpegEncEncodeParam& param, std::u } 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; + return 100 - param.compression_ratio * 99 / 255; +} + +Decoder::Jpeg::Sampling toSampling(const JpegEncEncodeParam& param) { + switch (param.sampling_type) { + case SAMPLING_TYPE_FULL: return Decoder::Jpeg::Sampling::Yuv444; + case SAMPLING_TYPE_422: return Decoder::Jpeg::Sampling::Yuv422; + case SAMPLING_TYPE_420: return Decoder::Jpeg::Sampling::Yuv420; + } + throw std::invalid_argument("sceJpegEncEncode: invalid sampling type"); } } // namespace @@ -187,7 +193,7 @@ int32_t APS5_VABI sceJpegEncEncode(void* handle, const JpegEncEncodeParam* param 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)); + const std::vector jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param), toSampling(*param)); if (jpeg.size() > param->jpeg_size) return SCE_JPEG_ENC_ERROR_INVALID_SIZE; std::memcpy(param->jpeg, jpeg.data(), jpeg.size()); diff --git a/core/libs/tests/GuestJpegEnc.cpp b/core/libs/tests/GuestJpegEnc.cpp index 7f5983747..5a49caa48 100644 --- a/core/libs/tests/GuestJpegEnc.cpp +++ b/core/libs/tests/GuestJpegEnc.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include extern "C" { @@ -23,17 +24,34 @@ 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 std::pair ReadFrame(const unsigned char* jpeg, std::uint32_t size) { + std::size_t offset = 2; + while (offset + 3 < size) { + Require(jpeg[offset++] == 0xFF); + while (offset < size && jpeg[offset] == 0xFF) ++offset; + Require(offset < size); + const std::uint8_t marker = jpeg[offset++]; + if (marker == 0xDA || marker == 0xD9) break; + Require(offset + 1 < size); + const std::size_t length = (static_cast(jpeg[offset]) << 8) | jpeg[offset + 1]; + Require(length >= 2 && offset + length <= size); + if (marker >= 0xC0 && marker <= 0xC3) return {jpeg[offset + 7], jpeg[offset + 9]}; + offset += length; + } + std::abort(); +} + 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) { +static std::vector DecodeOutput(const JpegEncOutputInfo& info, std::uint32_t channels = 3) { 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); + Require(decoded->width == 16 && decoded->height == 16 && decoded->channels == channels); return decoded->pixels; } @@ -169,6 +187,7 @@ int main() { info = {}; Require(sceJpegEncEncode(handle, &rgba, &info) == 0); Require(info.height == 16); + Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x22}}); Require(AverageError(expected, DecodeOutput(info)) < 6); for (std::uint32_t y = 0; y < 16; ++y) { @@ -183,6 +202,7 @@ int main() { bgra.pixel_format = 1; info = {}; Require(sceJpegEncEncode(handle, &bgra, &info) == 0); + Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x22}}); Require(AverageError(expected, DecodeOutput(info)) < 6); alignas(4) static unsigned char yuyv[16 * 16 * 2]; @@ -202,10 +222,8 @@ int main() { 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; @@ -216,7 +234,15 @@ int main() { y8.sampling_type = 0; info = {}; Require(sceJpegEncEncode(handle, &y8, &info) == 0); - Require(AverageError(expectedGray, DecodeOutput(info)) < 6); + Require(ReadFrame(jpeg, info.size) == std::pair{1, std::uint8_t{0x11}}); + const auto decodedGray = DecodeOutput(info, 1); + Require(AverageError(std::vector(gray, gray + sizeof(gray)), decodedGray) < 6); + + rgba.sampling_type = 1; + rgba.compression_ratio = 0; + info = {}; + Require(sceJpegEncEncode(handle, &rgba, &info) == 0); + Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x21}}); std::memset(jpeg, 0, sizeof(jpeg)); Require(sceJpegEncEncode(handle, &rgba, nullptr) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 7510c7afb..9f00e21f2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -141,7 +141,6 @@ Throughout the project, every function at every stage either **does exactly what - [v_mullit_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted, a positive denormal src2 counts as positive): the recompiler does not read MODE, so other IEEE/denormal settings are not modelled. - [v_fma_legacy_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140) is assumed to be fused, which has not been measured on hardware. LLVM decodes this encoding as `v_mad_legacy_f32` for gfx1013, and `v_mac_legacy_f32` (VOP2 0x06) was measured to round its product before the add, so 0x140 may do the same. - 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, and grayscale requests as a 3-component JPEG with neutral chroma instead of a single-component one: the [JPEG encoder](../../core/Decoder/Jpeg/src/Jpeg.cpp) (stb) only produces 3-component 4:2:0 and 4:4:4 images. - [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. v_sin_f32/v_cos_f32 with inputs beyond ±256 cycles were not compared with the console. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. SHA-1 and SHA-256 instructions with a RIP-relative operand fail the relink.