feat(jpegenc): honor the encode restart interval

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Adrià Franch 2026-10-06 14:16:07 +02:00
parent 5895ea721a
commit 263cb6626c
6 changed files with 99 additions and 5 deletions

View File

@ -28,7 +28,8 @@ enum class Sampling {
}; };
std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint32_t width, std::uint32_t height, 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, 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<Header> ParseHeader(std::span<const std::uint8_t> jpeg); std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg);

View File

@ -49,7 +49,8 @@ int toSubsampling(Sampling sampling) {
} // namespace } // namespace
std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint32_t width, std::uint32_t height, 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, 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) { if (width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
throw std::invalid_argument("Jpeg::Encode: unsupported image size"); throw std::invalid_argument("Jpeg::Encode: unsupported image size");
} }
@ -58,11 +59,18 @@ std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint
if (pixels.size() < static_cast<std::size_t>(width) * height * channels) { if (pixels.size() < static_cast<std::size_t>(width) * height * channels) {
throw std::invalid_argument("Jpeg::Encode: pixel buffer is too small"); 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); const int subsampling = toSubsampling(sampling);
Handle handle; Handle handle;
if (tj3Set(handle, TJPARAM_QUALITY, quality) < 0 || tj3Set(handle, TJPARAM_SUBSAMP, channels == 1 ? TJSAMP_GRAY : subsampling) < 0) { 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"); throw std::runtime_error("Jpeg::Encode: encoder configuration failed");
} }
if ((restartBlocks != 0 && tj3Set(handle, TJPARAM_RESTARTBLOCKS, static_cast<int>(restartBlocks)) < 0) ||
(restartRows != 0 && tj3Set(handle, TJPARAM_RESTARTROWS, static_cast<int>(restartRows)) < 0)) {
throw std::runtime_error("Jpeg::Encode: encoder configuration failed");
}
Buffer buffer; Buffer buffer;
std::size_t size = 0; std::size_t size = 0;
const int pixelFormat = channels == 1 ? TJPF_GRAY : TJPF_RGB; const int pixelFormat = channels == 1 ? TJPF_GRAY : TJPF_RGB;

View File

@ -100,6 +100,9 @@ int main() {
Require(ReadFrame(highQuality).components == 3 && ReadFrame(highQuality).sampling == factor); Require(ReadFrame(highQuality).components == 3 && ReadFrame(highQuality).sampling == factor);
} }
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, static_cast<Decoder::Jpeg::Sampling>(3)); })); Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, static_cast<Decoder::Jpeg::Sampling>(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()); Require(!Decoder::Jpeg::Decode({}).has_value());
const std::vector<std::uint8_t> garbage{1, 2, 3, 4, 5, 6, 7, 8}; const std::vector<std::uint8_t> garbage{1, 2, 3, 4, 5, 6, 7, 8};

View File

@ -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_422 = 1;
constexpr std::uint8_t SAMPLING_TYPE_420 = 2; 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_DIMENSION = 0xFFFF;
constexpr std::uint32_t MAX_IMAGE_PITCH = 0xFFFFFFF; constexpr std::uint32_t MAX_IMAGE_PITCH = 0xFFFFFFF;
constexpr std::uint64_t MAX_IMAGE_SIZE = 0x7FFFFFFF; 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); const std::int32_t result = validateEncodeParam(param);
if (result != 0) return result; 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->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::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> pixels = toPackedPixels(*param, channels);
const std::vector<std::uint8_t> 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<std::uint32_t>(param->restart_interval) : 0;
const std::uint32_t restartRows = param->restart_interval == RESTART_INTERVAL_PER_ROW ? 1 : 0;
const std::vector<std::uint8_t> 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; if (jpeg.size() > param->jpeg_size) return SCE_JPEG_ENC_ERROR_INVALID_SIZE;
std::memcpy(param->jpeg, jpeg.data(), jpeg.size()); std::memcpy(param->jpeg, jpeg.data(), jpeg.size());

View File

@ -41,6 +41,30 @@ static std::pair<int, std::uint8_t> ReadFrame(const unsigned char* jpeg, std::ui
std::abort(); std::abort();
} }
static std::pair<int, int> 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<std::size_t>(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<std::uint8_t>& expected, const std::vector<std::uint8_t>& actual) { static int AverageError(const std::vector<std::uint8_t>& expected, const std::vector<std::uint8_t>& actual) {
long total = 0; 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]; 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(encodeWith([](JpegEncEncodeParam& p) { p.image_width = 0; }) == invalidParam);
Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.encode_mode = 1; }); })); 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<unsigned char>((i * 37) ^ (i / 192));
static unsigned char wideGray[wideWidth * wideHeight];
for (std::size_t i = 0; i < sizeof(wideGray); ++i) wideGray[i] = static_cast<unsigned char>((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); Require(sceJpegEncDelete(handle) == 0);

View File

@ -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 - [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 - [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 - [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 - [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 - AudioIn, NpSessionSignaling and PlayerInvitationDialog exports added without an implementation have assumed signatures
- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature - [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature