#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "Decoder/Jpeg.hpp" #include #include #include #include #include #include extern "C" { std::int32_t APS5_VABI sceJpegEncQueryMemorySize(const JpegEncCreateParam*); std::int32_t APS5_VABI sceJpegEncCreate(const JpegEncCreateParam*, void*, std::uint32_t, void**); std::int32_t APS5_VABI sceJpegEncDelete(void*); std::int32_t APS5_VABI sceJpegEncEncode(void*, const JpegEncEncodeParam*, JpegEncOutputInfo*); } 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 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 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]; return static_cast(total / static_cast(expected.size())); } 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 == channels); 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; param.jpeg = jpeg; param.image_size = sizeof(image); param.jpeg_size = sizeof(jpeg); param.image_width = 16; param.image_height = 16; param.image_pitch = 16 * 4; param.pixel_format = 0; param.encode_mode = 0; param.color_space = 1; param.sampling_type = 2; param.compression_ratio = 80; param.restart_interval = 0; return param; } int main() { constexpr std::int32_t invalidAddr = static_cast(0x80650101); constexpr std::int32_t invalidSize = static_cast(0x80650102); constexpr std::int32_t invalidParam = static_cast(0x80650103); constexpr std::int32_t invalidHandle = static_cast(0x80650104); JpegEncCreateParam param{sizeof(JpegEncCreateParam), 0}; Require(sceJpegEncQueryMemorySize(¶m) == 0x800); Require(sceJpegEncQueryMemorySize(nullptr) == invalidAddr); JpegEncCreateParam badSize{sizeof(JpegEncCreateParam) - 1, 0}; Require(sceJpegEncQueryMemorySize(&badSize) == invalidSize); JpegEncCreateParam badAttr{sizeof(JpegEncCreateParam), 1}; Require(sceJpegEncQueryMemorySize(&badAttr) == invalidParam); alignas(32) static unsigned char memory[0x800 + 32]; unsigned char* unaligned = memory + 1; void* handle = nullptr; Require(sceJpegEncCreate(nullptr, unaligned, 0x800, &handle) == invalidAddr); Require(sceJpegEncCreate(&badSize, unaligned, 0x800, &handle) == invalidSize); Require(sceJpegEncCreate(&badAttr, unaligned, 0x800, &handle) == invalidParam); Require(sceJpegEncCreate(¶m, nullptr, 0x800, &handle) == invalidAddr); Require(sceJpegEncCreate(¶m, unaligned, 0x7FF, &handle) == invalidSize); Require(sceJpegEncCreate(¶m, unaligned, 0x800, nullptr) == invalidAddr); Require(handle == nullptr); Require(sceJpegEncCreate(¶m, unaligned, 0x800, &handle) == 0); const auto address = reinterpret_cast(handle); Require(address % 32 == 0); Require(address >= reinterpret_cast(unaligned)); Require(address < reinterpret_cast(unaligned) + 32); Require(sceJpegEncDelete(nullptr) == invalidHandle); Require(sceJpegEncDelete(static_cast(handle) + 1) == invalidHandle); alignas(32) static unsigned char garbage[64] = {}; Require(sceJpegEncDelete(garbage) == invalidHandle); Require(sceJpegEncDelete(handle) == 0); Require(sceJpegEncDelete(handle) == invalidHandle); JpegEncOutputInfo info{}; const JpegEncEncodeParam valid = ValidEncodeParam(); Require(sceJpegEncEncode(handle, &valid, &info) == invalidHandle); Require(sceJpegEncEncode(nullptr, &valid, &info) == invalidHandle); Require(sceJpegEncCreate(¶m, unaligned, 0x800, &handle) == 0); Require(sceJpegEncEncode(handle, nullptr, &info) == invalidAddr); auto encodeWith = [&](auto change) { JpegEncEncodeParam changed = ValidEncodeParam(); change(changed); return sceJpegEncEncode(handle, &changed, &info); }; Require(encodeWith([](JpegEncEncodeParam& p) { p.image = nullptr; }) == invalidAddr); Require(encodeWith([](JpegEncEncodeParam& p) { p.image = image + 1; }) == invalidAddr); Require(encodeWith([](JpegEncEncodeParam& p) { p.jpeg = nullptr; }) == invalidAddr); Require(encodeWith([](JpegEncEncodeParam& p) { p.image_size = 0; }) == invalidSize); Require(encodeWith([](JpegEncEncodeParam& p) { p.jpeg_size = 0; }) == invalidSize); Require(encodeWith([](JpegEncEncodeParam& p) { p.image_width = 0x10000; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.image_height = 0x10000; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.image_pitch = 0; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.image_pitch = 66; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.image_size = sizeof(image) - 1; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.image_height = 0xFFFF; p.image_pitch = 0xFFFFFFC; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.encode_mode = 2; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.color_space = 0; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.sampling_type = 3; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = 0x10000; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.pixel_format = 2; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.image_width = 17; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.color_space = 2; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.sampling_type = 0; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.pixel_format = 10; p.image_width = 33; }) == invalidParam); Require(encodeWith([](JpegEncEncodeParam& p) { p.pixel_format = 11; p.color_space = 2; p.sampling_type = 0; p.image_width = 65; }) == invalidParam); 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(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) { 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(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]; 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]; for (std::size_t i = 0; i < sizeof(gray); ++i) { gray[i] = static_cast(((i % 16) + (i / 16)) * 8); } 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(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); 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 = -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); return 0; }