feat(jpegenc): honor the encode restart interval
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5895ea721a
commit
263cb6626c
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue