feat(jpegenc): encode images through the shared JPEG encoder
This commit is contained in:
parent
94c7319202
commit
aad41aa907
|
|
@ -409,7 +409,7 @@ if(BUILD_TESTING)
|
|||
endif()
|
||||
add_executable(guest_jpeg_enc_tests tests/GuestJpegEnc.cpp)
|
||||
target_include_directories(guest_jpeg_enc_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_jpeg_enc_tests PRIVATE libSceJpegEnc)
|
||||
target_link_libraries(guest_jpeg_enc_tests PRIVATE libSceJpegEnc decoder_jpeg)
|
||||
add_test(NAME guest_jpeg_enc COMMAND guest_jpeg_enc_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_jpeg_enc PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ add_library(libSceJpegEnc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Ex
|
|||
target_include_directories(libSceJpegEnc PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
set_target_properties(libSceJpegEnc PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(libSceJpegEnc)
|
||||
target_link_libraries(libSceJpegEnc PRIVATE decoder_jpeg)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include "Decoder/Jpeg.hpp"
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
|
|
@ -37,6 +42,7 @@ 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;
|
||||
|
|
@ -65,6 +71,7 @@ std::int32_t validateEncodeParam(const JpegEncEncodeParam* param) {
|
|||
|
||||
if (param->image_size == 0 || param->jpeg_size == 0) return SCE_JPEG_ENC_ERROR_INVALID_SIZE;
|
||||
|
||||
if (param->image_width == 0 || param->image_height == 0) return SCE_JPEG_ENC_ERROR_INVALID_PARAM;
|
||||
if (param->image_width > MAX_IMAGE_DIMENSION || param->image_height > MAX_IMAGE_DIMENSION) return SCE_JPEG_ENC_ERROR_INVALID_PARAM;
|
||||
if (param->image_pitch == 0 || param->image_pitch > MAX_IMAGE_PITCH) return SCE_JPEG_ENC_ERROR_INVALID_PARAM;
|
||||
if (!grayscaleInput && param->image_pitch % 4 != 0) return SCE_JPEG_ENC_ERROR_INVALID_PARAM;
|
||||
|
|
@ -96,6 +103,57 @@ std::int32_t validateEncodeParam(const JpegEncEncodeParam* param) {
|
|||
}
|
||||
}
|
||||
|
||||
std::uint8_t clampToByte(float value) {
|
||||
return static_cast<std::uint8_t>(std::clamp(value + 0.5f, 0.0f, 255.0f));
|
||||
}
|
||||
|
||||
void yuvToRgb(std::uint8_t y, std::uint8_t u, std::uint8_t v, std::uint8_t* rgb) {
|
||||
const float cb = static_cast<float>(u) - 128.0f;
|
||||
const float cr = static_cast<float>(v) - 128.0f;
|
||||
rgb[0] = clampToByte(y + 1.402f * cr);
|
||||
rgb[1] = clampToByte(y - 0.344136f * cb - 0.714136f * cr);
|
||||
rgb[2] = clampToByte(y + 1.772f * cb);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> toPackedPixels(const JpegEncEncodeParam& param, std::uint32_t channels) {
|
||||
const auto* image = static_cast<const std::uint8_t*>(param.image);
|
||||
const std::size_t width = param.image_width;
|
||||
std::vector<std::uint8_t> pixels(width * param.image_height * channels);
|
||||
for (std::size_t y = 0; y < param.image_height; ++y) {
|
||||
const std::uint8_t* row = image + y * param.image_pitch;
|
||||
std::uint8_t* out = pixels.data() + y * width * channels;
|
||||
for (std::size_t x = 0; x < width; ++x) {
|
||||
switch (param.pixel_format) {
|
||||
case PIXEL_FORMAT_R8G8B8A8:
|
||||
out[x * 3 + 0] = row[x * 4 + 0];
|
||||
out[x * 3 + 1] = row[x * 4 + 1];
|
||||
out[x * 3 + 2] = row[x * 4 + 2];
|
||||
break;
|
||||
case PIXEL_FORMAT_B8G8R8A8:
|
||||
out[x * 3 + 0] = row[x * 4 + 2];
|
||||
out[x * 3 + 1] = row[x * 4 + 1];
|
||||
out[x * 3 + 2] = row[x * 4 + 0];
|
||||
break;
|
||||
case PIXEL_FORMAT_Y8U8Y8V8: {
|
||||
const std::uint8_t* pair = row + (x / 2) * 4;
|
||||
yuvToRgb(pair[(x % 2) * 2], pair[1], pair[3], out + x * 3);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
out[x] = row[x];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return pixels;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
|
@ -125,8 +183,19 @@ int32_t APS5_VABI sceJpegEncEncode(void* handle, const JpegEncEncodeParam* param
|
|||
if (!toEncoder(handle)) return SCE_JPEG_ENC_ERROR_INVALID_HANDLE;
|
||||
const std::int32_t result = validateEncodeParam(param);
|
||||
if (result != 0) return result;
|
||||
(void)output_info;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
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");
|
||||
|
||||
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> jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param));
|
||||
if (jpeg.size() > param->jpeg_size) return SCE_JPEG_ENC_ERROR_INVALID_SIZE;
|
||||
|
||||
std::memcpy(param->jpeg, jpeg.data(), jpeg.size());
|
||||
if (output_info) {
|
||||
output_info->size = static_cast<std::uint32_t>(jpeg.size());
|
||||
output_info->height = param->image_height;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "Decoder/Jpeg.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
std::int32_t APS5_VABI sceJpegEncQueryMemorySize(const JpegEncCreateParam*);
|
||||
|
|
@ -15,6 +19,34 @@ 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 int AverageError(const std::vector<std::uint8_t>& expected, const std::vector<std::uint8_t>& 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<int>(total / static_cast<long>(expected.size()));
|
||||
}
|
||||
|
||||
static std::vector<std::uint8_t> DecodeOutput(const JpegEncOutputInfo& info) {
|
||||
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);
|
||||
return decoded->pixels;
|
||||
}
|
||||
|
||||
template<typename TFunction>
|
||||
static bool ThrowsRuntimeError(TFunction function) {
|
||||
try {
|
||||
function();
|
||||
} catch (const std::runtime_error&) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static JpegEncEncodeParam ValidEncodeParam() {
|
||||
JpegEncEncodeParam param{};
|
||||
param.image = image;
|
||||
|
|
@ -113,6 +145,91 @@ int main() {
|
|||
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<std::uint8_t> 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<std::uint8_t>(x * 16), static_cast<std::uint8_t>(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(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(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<std::uint8_t>(16 * 16 * 3, 100), DecodeOutput(info)) < 3);
|
||||
|
||||
static unsigned char gray[16 * 16];
|
||||
std::vector<std::uint8_t> expectedGray(16 * 16 * 3);
|
||||
for (std::size_t i = 0; i < sizeof(gray); ++i) {
|
||||
gray[i] = static_cast<unsigned char>(((i % 16) + (i / 16)) * 8);
|
||||
for (int c = 0; c < 3; ++c) expectedGray[i * 3 + c] = gray[i];
|
||||
}
|
||||
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(AverageError(expectedGray, DecodeOutput(info)) < 6);
|
||||
|
||||
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 = 1; }); }));
|
||||
|
||||
Require(sceJpegEncDelete(handle) == 0);
|
||||
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
|
||||
- [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx).
|
||||
- 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: the [JPEG encoder](../../core/Decoder/Jpeg/src/Jpeg.cpp) (stb) only produces 4:2:0 and 4:4:4.
|
||||
- [Relinker](../../core/relinker/elfpatcher/src) doesn't add an icon to the generated executable. This should be done without adding dependencies (only standard).
|
||||
- `--to-intel` does not lower the register form of INSERTQ (`F2 0F 79`) nor MONITORX/MWAITX/CLZERO/RDPRU/MCOMMIT; the [lowering](../../core/relinker/codegen/src/x86/Sse4aLowering.cpp) fails the relink instead. SHA-NI is not substituted.
|
||||
- The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust VEX/0F38/0F3A RIP-relative operands, data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length.
|
||||
|
|
|
|||
Loading…
Reference in New Issue