diff --git a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp index 60142fc36..13c1d8acc 100644 --- a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp +++ b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp @@ -28,7 +28,8 @@ enum class Sampling { }; std::vector Encode(std::span pixels, std::uint32_t width, std::uint32_t height, - std::uint32_t channels, int quality, Sampling sampling = Sampling::Yuv444); + std::uint32_t channels, int quality, Sampling sampling = Sampling::Yuv444, + std::uint32_t restartBlocks = 0, std::uint32_t restartRows = 0); std::optional
ParseHeader(std::span jpeg); diff --git a/core/Decoder/Jpeg/src/Jpeg.cpp b/core/Decoder/Jpeg/src/Jpeg.cpp index c8f1e1eca..d446841cc 100644 --- a/core/Decoder/Jpeg/src/Jpeg.cpp +++ b/core/Decoder/Jpeg/src/Jpeg.cpp @@ -49,7 +49,8 @@ int toSubsampling(Sampling sampling) { } // namespace std::vector Encode(std::span pixels, std::uint32_t width, std::uint32_t height, - std::uint32_t channels, int quality, Sampling sampling) { + std::uint32_t channels, int quality, Sampling sampling, std::uint32_t restartBlocks, + std::uint32_t restartRows) { if (width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) { throw std::invalid_argument("Jpeg::Encode: unsupported image size"); } @@ -58,11 +59,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"); } + if (restartBlocks > MAX_DIMENSION || restartRows > MAX_DIMENSION || (restartBlocks != 0 && restartRows != 0)) { + throw std::invalid_argument("Jpeg::Encode: invalid restart interval"); + } 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"); } + if ((restartBlocks != 0 && tj3Set(handle, TJPARAM_RESTARTBLOCKS, static_cast(restartBlocks)) < 0) || + (restartRows != 0 && tj3Set(handle, TJPARAM_RESTARTROWS, static_cast(restartRows)) < 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; diff --git a/core/Decoder/Jpeg/tests/Jpeg.cpp b/core/Decoder/Jpeg/tests/Jpeg.cpp index d396a00f2..f5f9c309a 100644 --- a/core/Decoder/Jpeg/tests/Jpeg.cpp +++ b/core/Decoder/Jpeg/tests/Jpeg.cpp @@ -100,6 +100,9 @@ int main() { Require(ReadFrame(highQuality).components == 3 && ReadFrame(highQuality).sampling == factor); } Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, static_cast(3)); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, Decoder::Jpeg::Sampling::Yuv444, 1, 1); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, Decoder::Jpeg::Sampling::Yuv444, 0x10000, 0); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, Decoder::Jpeg::Sampling::Yuv444, 0, 0x10000); })); Require(!Decoder::Jpeg::Decode({}).has_value()); const std::vector garbage{1, 2, 3, 4, 5, 6, 7, 8}; diff --git a/core/libs/prx/libSceJpegEnc/Export.cpp b/core/libs/prx/libSceJpegEnc/Export.cpp index 2d4c66657..6de513f53 100644 --- a/core/libs/prx/libSceJpegEnc/Export.cpp +++ b/core/libs/prx/libSceJpegEnc/Export.cpp @@ -38,6 +38,8 @@ constexpr std::uint8_t SAMPLING_TYPE_FULL = 0; constexpr std::uint8_t SAMPLING_TYPE_422 = 1; constexpr std::uint8_t SAMPLING_TYPE_420 = 2; +constexpr std::int32_t RESTART_INTERVAL_PER_ROW = -1; + constexpr std::uint32_t MAX_IMAGE_DIMENSION = 0xFFFF; constexpr std::uint32_t MAX_IMAGE_PITCH = 0xFFFFFFF; constexpr std::uint64_t MAX_IMAGE_SIZE = 0x7FFFFFFF; @@ -189,11 +191,14 @@ int32_t APS5_VABI sceJpegEncEncode(void* handle, const JpegEncEncodeParam* param const std::int32_t result = validateEncodeParam(param); if (result != 0) return result; 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"); + if (param->restart_interval < RESTART_INTERVAL_PER_ROW) throw std::runtime_error("sceJpegEncEncode: unknown negative restart interval"); 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), toSampling(*param)); + const std::uint32_t restartBlocks = param->restart_interval > 0 ? static_cast(param->restart_interval) : 0; + const std::uint32_t restartRows = param->restart_interval == RESTART_INTERVAL_PER_ROW ? 1 : 0; + const std::vector jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param), toSampling(*param), + restartBlocks, restartRows); 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 5a49caa48..52437df7a 100644 --- a/core/libs/tests/GuestJpegEnc.cpp +++ b/core/libs/tests/GuestJpegEnc.cpp @@ -41,6 +41,30 @@ static std::pair ReadFrame(const unsigned char* jpeg, std::ui std::abort(); } +static std::pair ReadRestart(const unsigned char* jpeg, std::uint32_t size) { + int interval = 0; + std::size_t offset = 2; + while (true) { + Require(offset + 3 < size && jpeg[offset] == 0xFF); + const std::uint8_t marker = jpeg[offset + 1]; + const std::size_t length = (static_cast(jpeg[offset + 2]) << 8) | jpeg[offset + 3]; + Require(length >= 2 && offset + 2 + length <= size); + if (marker == 0xDD) { + Require(length == 4); + interval = (jpeg[offset + 4] << 8) | jpeg[offset + 5]; + } + offset += 2 + length; + if (marker == 0xDA) break; + } + int markers = 0; + for (; offset + 1 < size; ++offset) { + if (jpeg[offset] != 0xFF || jpeg[offset + 1] < 0xD0 || jpeg[offset + 1] > 0xD7) continue; + Require(jpeg[offset + 1] == 0xD0 + markers % 8); + ++markers; + } + return {interval, markers}; +} + 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]; @@ -254,7 +278,59 @@ int main() { 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(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = -2; }); })); + Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = INT32_MIN; }); })); + + constexpr std::uint32_t wideWidth = 48; + constexpr std::uint32_t wideHeight = 32; + alignas(4) static unsigned char wide[wideWidth * wideHeight * 4]; + for (std::size_t i = 0; i < sizeof(wide); ++i) wide[i] = static_cast((i * 37) ^ (i / 192)); + static unsigned char wideGray[wideWidth * wideHeight]; + for (std::size_t i = 0; i < sizeof(wideGray); ++i) wideGray[i] = static_cast((i * 53) ^ (i / 48)); + static unsigned char restarted[8192]; + auto encodeWide = [&](std::uint8_t sampling, std::int32_t restart, bool grayscale) { + JpegEncEncodeParam p = ValidEncodeParam(); + p.image = grayscale ? wideGray : wide; + p.image_size = grayscale ? sizeof(wideGray) : sizeof(wide); + p.image_width = wideWidth; + p.image_height = wideHeight; + p.image_pitch = grayscale ? wideWidth : wideWidth * 4; + p.pixel_format = grayscale ? 11 : 0; + p.color_space = grayscale ? 2 : 1; + p.sampling_type = sampling; + p.jpeg = restarted; + p.jpeg_size = sizeof(restarted); + p.restart_interval = restart; + JpegEncOutputInfo out{}; + Require(sceJpegEncEncode(handle, &p, &out) == 0); + Require(IsJpeg(restarted, out.size) && out.height == wideHeight); + const auto decoded = Decoder::Jpeg::Decode({restarted, out.size}); + Require(decoded.has_value() && decoded->width == wideWidth && decoded->height == wideHeight); + return std::pair{ReadRestart(restarted, out.size), decoded->pixels}; + }; + + const auto plain420 = encodeWide(2, 0, false); + Require(plain420.first == std::pair{0, 0}); + const auto blocks420 = encodeWide(2, 2, false); + Require(blocks420.first == std::pair{2, 2}); + Require(blocks420.second == plain420.second); + const auto rows420 = encodeWide(2, -1, false); + Require(rows420.first == std::pair{3, 1}); + Require(rows420.second == plain420.second); + const auto plain422 = encodeWide(1, 0, false); + const auto rows422 = encodeWide(1, -1, false); + Require(rows422.first == std::pair{3, 3}); + Require(rows422.second == plain422.second); + const auto blocks422 = encodeWide(1, 5, false); + Require(blocks422.first == std::pair{5, 2}); + Require(blocks422.second == plain422.second); + const auto plainGray = encodeWide(0, 0, true); + const auto rowsGray = encodeWide(0, -1, true); + Require(rowsGray.first == std::pair{6, 3}); + Require(rowsGray.second == plainGray.second); + const auto blocksGray = encodeWide(0, 0xFFFF, true); + Require(blocksGray.first == std::pair{0xFFFF, 0}); + Require(blocksGray.second == plainGray.second); Require(sceJpegEncDelete(handle) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..63a82289e 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -92,6 +92,7 @@ Throughout the project, every function at every stage either **does exactly what - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; only 0x8A810001 (not initialized) is returned - [BnMAMrsfVWo](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libc) - unknown name, signature - [scePngEncEncode](../../core/libs/prx/libScePngEnc/Export.cpp) (libScePngEnc) - whether rows may stay unfiltered when `filter_type` names a subset of the filters is unknown; as in shadPS4, they may only for 0 and for the all-filters mask +- [sceJpegEncEncode](../../core/libs/prx/libSceJpegEnc/Export.cpp) (libSceJpegEnc) - `restart_interval` follows SharpProspero's field doc (0 none, -1 every row of blocks, positive a block count); a positive value is taken as the JPEG DRI interval in MCUs and -1 as one restart per MCU row, which no reference confirms. Other negative values throw - [sceVoiceQoSInit](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - the error codes are unknown: a null or empty memory block, an app type other than 0x20000000 (PPSA14632) or 0x10000000 (PPSA26344) and a second initialization throw. The minimum memory size is unknown; the app type and the block are not used, as no endpoint is implemented - AudioIn, NpSessionSignaling and PlayerInvitationDialog exports added without an implementation have assumed signatures - [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature