diff --git a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp index 330aaeb77..275217fb2 100644 --- a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp +++ b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp @@ -15,9 +15,17 @@ struct Image { std::vector pixels; }; +struct Header { + std::uint32_t width; + std::uint32_t height; + std::uint32_t channels; +}; + std::vector Encode(std::span pixels, std::uint32_t width, std::uint32_t height, std::uint32_t channels, int quality); +std::optional
ParseHeader(std::span jpeg); + std::optional Decode(std::span jpeg); } // namespace Decoder::Jpeg diff --git a/core/Decoder/Jpeg/src/Jpeg.cpp b/core/Decoder/Jpeg/src/Jpeg.cpp index c9f71a731..74a50c4f8 100644 --- a/core/Decoder/Jpeg/src/Jpeg.cpp +++ b/core/Decoder/Jpeg/src/Jpeg.cpp @@ -47,6 +47,17 @@ std::vector Encode(std::span pixels, std::uint return output; } +std::optional
ParseHeader(std::span jpeg) { + if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt; + + int width = 0; + int height = 0; + int channels = 0; + if (!stbi_info_from_memory(jpeg.data(), static_cast(jpeg.size()), &width, &height, &channels)) return std::nullopt; + if (width <= 0 || height <= 0 || channels <= 0) return std::nullopt; + return Header{static_cast(width), static_cast(height), static_cast(channels)}; +} + std::optional Decode(std::span jpeg) { if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt; diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 86bd95651..b5da3de4f 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1468,6 +1468,35 @@ struct JpegEncOutputInfo { std::uint32_t height; }; +struct JpegDecCreateParam { + std::uint32_t size; + std::uint32_t attribute; +}; + +struct JpegDecParseParam { + const void* jpeg_mem_addr; + std::uint32_t jpeg_mem_size; + std::uint32_t reserved0; +}; + +struct JpegDecDecodeParam { + const void* jpeg_mem_addr; + void* image_mem_addr; + std::uint32_t jpeg_mem_size; + std::uint32_t image_mem_size; + std::uint16_t pixel_format; + std::uint16_t reserved0; + std::uint32_t image_pitch; +}; + +struct JpegDecImageInfo { + std::uint32_t image_width; + std::uint32_t image_height; + std::uint16_t color_space; + std::uint16_t bit_depth; + std::uint32_t image_flag; +}; + struct PlayGoInitParams { const void* buf_addr; std::uint32_t buf_size; diff --git a/core/libs/prx/libSceJpegDec/CMakeLists.txt b/core/libs/prx/libSceJpegDec/CMakeLists.txt new file mode 100644 index 000000000..20f556165 --- /dev/null +++ b/core/libs/prx/libSceJpegDec/CMakeLists.txt @@ -0,0 +1,27 @@ +if(NOT TARGET decoder_jpeg) + add_jpeg_decoder(decoder_jpeg libc) +endif() + +add_library(libSceJpegDec SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) +target_include_directories(libSceJpegDec PRIVATE ${LIBS_INCLUDE_DIR}) +set_target_properties(libSceJpegDec PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(libSceJpegDec) +target_link_libraries(libSceJpegDec PRIVATE decoder_jpeg) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_jpeg_dec_tests ${LIBS_INCLUDE_DIR}/tests/GuestJpegDec.cpp) + target_include_directories(guest_jpeg_dec_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_jpeg_dec_tests PRIVATE libSceJpegDec decoder_jpeg libc) + configure_windows_unwind(guest_jpeg_dec_tests) + add_test(NAME guest_jpeg_dec COMMAND guest_jpeg_dec_tests) + if(WIN32) + set_tests_properties(guest_jpeg_dec PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp new file mode 100644 index 000000000..d38764bf7 --- /dev/null +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "Decoder/Jpeg.hpp" +#include "SceTypes.hpp" + +static_assert(sizeof(JpegDecCreateParam) == 0x8); +static_assert(sizeof(JpegDecParseParam) == 0x10); +static_assert(sizeof(JpegDecDecodeParam) == 0x20); +static_assert(sizeof(JpegDecImageInfo) == 0x10); + +namespace { + +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_ADDR = static_cast(0x80690001); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_SIZE = static_cast(0x80690002); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_PARAM = static_cast(0x80690003); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_HANDLE = static_cast(0x80690004); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_WORK_MEMORY = static_cast(0x80690005); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_DATA = static_cast(0x80690010); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_DECODE_ERROR = static_cast(0x80690012); + +constexpr std::uint32_t ATTRIBUTE_NONE = 0; +constexpr std::uint32_t MEMORY_SIZE = 0x20; +constexpr std::uintptr_t HANDLE_ALIGNMENT = 8; + +constexpr std::uint16_t PIXEL_FORMAT_R8G8B8A8 = 0; +constexpr std::uint16_t PIXEL_FORMAT_B8G8R8A8 = 1; + +constexpr std::uint16_t COLOR_SPACE_RGB = 3; + +constexpr std::uint32_t BYTES_PER_PIXEL = 4; +constexpr std::uint32_t MAX_PACKED_DIMENSION = 32767; + +struct Context { + Context* self; + std::uint32_t attribute; +}; + +static_assert(sizeof(Context) + HANDLE_ALIGNMENT - 1 <= MEMORY_SIZE); + +std::int32_t validateCreateParam(const JpegDecCreateParam* param) { + if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (param->size != sizeof(JpegDecCreateParam)) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + if (param->attribute != ATTRIBUTE_NONE) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + return 0; +} + +Context* toContext(void* handle) { + const auto address = reinterpret_cast(handle); + if (address == 0 || address % HANDLE_ALIGNMENT != 0) return nullptr; + auto* context = reinterpret_cast(handle); + return context->self == context ? context : nullptr; +} + +void fillImageInfo(const Decoder::Jpeg::Header& header, JpegDecImageInfo* info) { + info->image_width = header.width; + info->image_height = header.height; + info->color_space = COLOR_SPACE_RGB; + info->bit_depth = 8; + info->image_flag = 0; +} + +std::span toBytes(const void* data, std::uint32_t size) { + return {static_cast(data), size}; +} + +} // namespace + +extern "C" { + +int32_t APS5_VABI sceJpegDecCreate(const JpegDecCreateParam* param, void* memory_address, uint32_t memory_size, void** handle) { + const std::int32_t result = validateCreateParam(param); + if (result != 0) return result; + if (!memory_address || !handle) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (memory_size < MEMORY_SIZE) return SCE_JPEG_DEC_ERROR_INVALID_WORK_MEMORY; + const auto address = reinterpret_cast(memory_address); + const std::uintptr_t aligned = (address + HANDLE_ALIGNMENT - 1) & ~(HANDLE_ALIGNMENT - 1); + auto* context = new (reinterpret_cast(aligned)) Context{}; + context->self = context; + context->attribute = param->attribute; + *handle = context; + return 0; +} + +int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param, JpegDecImageInfo* image_info) { + const Context* context = toContext(handle); + if (!context) return SCE_JPEG_DEC_ERROR_INVALID_HANDLE; + (void)context; + if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (!param->jpeg_mem_addr || !param->image_mem_addr) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (param->jpeg_mem_size == 0 || param->image_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + + const std::span jpeg = toBytes(param->jpeg_mem_addr, param->jpeg_mem_size); + const std::optional header = Decoder::Jpeg::ParseHeader(jpeg); + if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; + + const std::uint64_t rowSize = static_cast(header->width) * BYTES_PER_PIXEL; + const std::uint64_t pitch = param->image_pitch == 0 ? rowSize : param->image_pitch; + if (pitch < rowSize) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if ((header->height - 1) * pitch + rowSize > param->image_mem_size) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + + const std::optional image = Decoder::Jpeg::Decode(jpeg); + if (!image || image->width != header->width || image->height != header->height) return SCE_JPEG_DEC_ERROR_DECODE_ERROR; + + const bool swapRedBlue = param->pixel_format == PIXEL_FORMAT_B8G8R8A8; + auto* output = static_cast(param->image_mem_addr); + for (std::uint32_t y = 0; y < image->height; ++y) { + std::uint8_t* out = output + y * pitch; + for (std::uint32_t x = 0; x < image->width; ++x) { + const std::uint8_t* in = image->pixels.data() + (static_cast(y) * image->width + x) * image->channels; + std::uint8_t* pixel = out + x * BYTES_PER_PIXEL; + if (image->channels == 1) { + pixel[0] = in[0]; + pixel[1] = in[0]; + pixel[2] = in[0]; + } else { + pixel[0] = in[0]; + pixel[1] = in[1]; + pixel[2] = in[2]; + } + if (swapRedBlue) std::swap(pixel[0], pixel[2]); + pixel[3] = 0xFF; + } + } + + if (image_info) fillImageInfo(*header, image_info); + if (header->width > MAX_PACKED_DIMENSION || header->height > MAX_PACKED_DIMENSION) return 0; + return static_cast(header->width << 16 | header->height); +} + +int32_t APS5_VABI sceJpegDecDelete(void* handle) { + Context* context = toContext(handle); + if (!context) return SCE_JPEG_DEC_ERROR_INVALID_HANDLE; + context->self = nullptr; + return 0; +} + +int32_t APS5_VABI sceJpegDecParseHeader(const JpegDecParseParam* param, JpegDecImageInfo* image_info) { + if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (!param->jpeg_mem_addr || !image_info) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (param->jpeg_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + const std::optional header = Decoder::Jpeg::ParseHeader(toBytes(param->jpeg_mem_addr, param->jpeg_mem_size)); + if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; + fillImageInfo(*header, image_info); + return 0; +} + +int32_t APS5_VABI sceJpegDecQueryMemorySize(const JpegDecCreateParam* param) { + const std::int32_t result = validateCreateParam(param); + if (result != 0) return result; + return static_cast(MEMORY_SIZE); +} + +} diff --git a/core/libs/prx/libSceJpegEnc/CMakeLists.txt b/core/libs/prx/libSceJpegEnc/CMakeLists.txt index fd2c49277..1a5f75a53 100644 --- a/core/libs/prx/libSceJpegEnc/CMakeLists.txt +++ b/core/libs/prx/libSceJpegEnc/CMakeLists.txt @@ -1,4 +1,6 @@ -add_jpeg_decoder(decoder_jpeg libc) +if(NOT TARGET decoder_jpeg) + add_jpeg_decoder(decoder_jpeg libc) +endif() add_library(libSceJpegEnc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) target_include_directories(libSceJpegEnc PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp new file mode 100644 index 000000000..1268bd57d --- /dev/null +++ b/core/libs/tests/GuestJpegDec.cpp @@ -0,0 +1,106 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "Decoder/Jpeg.hpp" +#include +#include +#include +#include +#include + +extern "C" { +std::int32_t APS5_VABI sceJpegDecQueryMemorySize(const JpegDecCreateParam*); +std::int32_t APS5_VABI sceJpegDecCreate(const JpegDecCreateParam*, void*, std::uint32_t, void**); +std::int32_t APS5_VABI sceJpegDecDelete(void*); +std::int32_t APS5_VABI sceJpegDecParseHeader(const JpegDecParseParam*, JpegDecImageInfo*); +std::int32_t APS5_VABI sceJpegDecDecode(void*, const JpegDecDecodeParam*, JpegDecImageInfo*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static std::vector TestJpeg(std::uint32_t width, std::uint32_t height) { + std::vector rgb(static_cast(width) * height * 3); + for (std::size_t i = 0; i < rgb.size(); ++i) rgb[i] = static_cast(10 + i * 13); + return Decoder::Jpeg::Encode(rgb, width, height, 3, 90); +} + +static JpegDecDecodeParam DecodeParam(const std::vector& jpeg, std::vector& image) { + JpegDecDecodeParam param{}; + param.jpeg_mem_addr = jpeg.data(); + param.image_mem_addr = image.data(); + param.jpeg_mem_size = static_cast(jpeg.size()); + param.image_mem_size = static_cast(image.size()); + param.pixel_format = 0; + param.reserved0 = 0; + param.image_pitch = 0; + return param; +} + +int main() { + constexpr std::int32_t invalidAddr = static_cast(0x80690001); + constexpr std::int32_t invalidSize = static_cast(0x80690002); + constexpr std::int32_t invalidParam = static_cast(0x80690003); + constexpr std::int32_t invalidHandle = static_cast(0x80690004); + constexpr std::int32_t invalidWorkMemory = static_cast(0x80690005); + constexpr std::int32_t invalidData = static_cast(0x80690010); + + const JpegDecCreateParam param{sizeof(JpegDecCreateParam), 0}; + const std::int32_t memorySize = sceJpegDecQueryMemorySize(¶m); + Require(memorySize > 0); + Require(sceJpegDecQueryMemorySize(nullptr) == invalidParam); + const JpegDecCreateParam badSize{sizeof(JpegDecCreateParam) - 1, 0}; + Require(sceJpegDecQueryMemorySize(&badSize) == invalidSize); + const JpegDecCreateParam badAttribute{sizeof(JpegDecCreateParam), 1}; + Require(sceJpegDecQueryMemorySize(&badAttribute) == invalidParam); + + std::vector memory(static_cast(memorySize) + 8); + void* handle = nullptr; + Require(sceJpegDecCreate(¶m, memory.data(), 0, &handle) == invalidWorkMemory); + Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), nullptr) == invalidAddr); + Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), &handle) == 0); + Require(handle != nullptr); + Require(sceJpegDecDelete(nullptr) == invalidHandle); + Require(sceJpegDecDelete(handle) == 0); + + Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), &handle) == 0); + + const std::vector jpeg = TestJpeg(7, 5); + JpegDecImageInfo info{}; + const JpegDecParseParam parseParam{jpeg.data(), static_cast(jpeg.size()), 0}; + Require(sceJpegDecParseHeader(nullptr, &info) == invalidParam); + Require(sceJpegDecParseHeader(&parseParam, nullptr) == invalidAddr); + const JpegDecParseParam emptyParam{nullptr, 0, 0}; + Require(sceJpegDecParseHeader(&emptyParam, &info) == invalidAddr); + Require(sceJpegDecParseHeader(&parseParam, &info) == 0); + Require(info.image_width == 7); + Require(info.image_height == 5); + + std::vector image(7 * 5 * 4, 0xCC); + JpegDecDecodeParam decodeParam = DecodeParam(jpeg, image); + Require(sceJpegDecDecode(nullptr, &decodeParam, &info) == invalidHandle); + Require(sceJpegDecDecode(handle, nullptr, &info) == invalidParam); + JpegDecDecodeParam badFormat = decodeParam; + badFormat.pixel_format = 2; + Require(sceJpegDecDecode(handle, &badFormat, &info) == invalidParam); + const std::int32_t packed = sceJpegDecDecode(handle, &decodeParam, &info); + Require(packed == static_cast(7 << 16 | 5)); + Require(info.image_width == 7); + Require(info.image_height == 5); + Require(image[3] == 0xFF); + + std::vector bgra(7 * 5 * 4, 0); + JpegDecDecodeParam bgraParam = DecodeParam(jpeg, bgra); + bgraParam.pixel_format = 1; + Require(sceJpegDecDecode(handle, &bgraParam, nullptr) == static_cast(7 << 16 | 5)); + Require(bgra[0] == image[2]); + Require(bgra[2] == image[0]); + + const std::vector garbage{0x00, 0x01, 0x02, 0x03}; + JpegDecParseParam garbageParam{garbage.data(), static_cast(garbage.size()), 0}; + Require(sceJpegDecParseHeader(&garbageParam, &info) == invalidData); + std::vector garbageOut(64, 0); + JpegDecDecodeParam garbageDecode = DecodeParam(garbage, garbageOut); + Require(sceJpegDecDecode(handle, &garbageDecode, &info) == invalidData); + + Require(sceJpegDecDelete(handle) == 0); + return 0; +}