// Vita3K emulator project // Copyright (C) 2025 Vita3K team // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. #include #include #include #include #include TRACY_MODULE_NAME(SceJpegUser); typedef std::shared_ptr DecoderPtr; struct MJpegState { bool initialized = false; DecoderPtr decoder; }; struct SceJpegMJpegInitInfo { SceSize size; int maxSplitDecoder; int option; }; typedef MJpegPitch SceJpegPitch; enum SceJpegColorSpace : int { SCE_JPEG_COLORSPACE_UNKNOWN = 0x00000, SCE_JPEG_COLORSPACE_GRAYSCALE = 0x10101, SCE_JPEG_COLORSPACE_YUV = 0x20000, SCE_JPEG_COLORSPACE_YUV444 = 0x20101, SCE_JPEG_COLORSPACE_YUV440 = 0x20102, SCE_JPEG_COLORSPACE_YUV441 = 0x20104, SCE_JPEG_COLORSPACE_YUV422 = 0x20201, SCE_JPEG_COLORSPACE_YUV420 = 0x20202, SCE_JPEG_COLORSPACE_YUV411 = 0x20401, }; enum SceJpegDHTMode : int { SCE_JPEG_MJPEG_WITH_DHT, SCE_JPEG_MJPEG_WITHOUT_DHT, SCE_JPEG_MJPEG_ANY_SAMPLING_WITHOUT_DHT, SCE_JPEG_MJPEG_ANY_SAMPLING }; enum SceJpegDownscaleMode : int { SCE_JPEG_MJPEG_DOWNSCALE_1_2 = 1 << 4, SCE_JPEG_MJPEG_DOWNSCALE_1_4 = 1 << 5, SCE_JPEG_MJPEG_DOWNSCALE_1_8 = 1 << 6, SCE_JPEG_MJPEG_DOWNSCALE_ANY = 0b111 << 4 }; enum SceJpegFormat : int { // YUV format SCE_JPEG_NO_CSC_OUTPUT = -1, SCE_JPEG_PIXEL_RGBA8888 = 0, SCE_JPEG_PIXEL_BGRA8888 = 4 }; enum SceJpegColorConversion : int { SCE_JPEG_COLORSPACE_JFIF = 0, SCE_JPEG_COLORSPACE_BT601 = 0x10 }; struct SceJpegOutputInfo { SceJpegColorSpace color_space; uint16_t width; uint16_t height; uint32_t output_size; uint32_t temp_buffer_size; uint32_t coef_buffer_size; SceJpegPitch pitch[4]; }; SceJpegColorSpace convert_color_space_decoder_to_jpeg(DecoderColorSpace color_space) { switch (color_space) { case COLORSPACE_GRAYSCALE: return SCE_JPEG_COLORSPACE_GRAYSCALE; case COLORSPACE_YUV444P: return SCE_JPEG_COLORSPACE_YUV444; case COLORSPACE_YUV440P: return SCE_JPEG_COLORSPACE_YUV440; case COLORSPACE_YUV441P: return SCE_JPEG_COLORSPACE_YUV441; case COLORSPACE_YUV422P: return SCE_JPEG_COLORSPACE_YUV422; case COLORSPACE_YUV420P: return SCE_JPEG_COLORSPACE_YUV420; case COLORSPACE_YUV411P: return SCE_JPEG_COLORSPACE_YUV411; default: return SCE_JPEG_COLORSPACE_UNKNOWN; } } DecoderColorSpace convert_color_space_jpeg_to_decoder(SceJpegColorSpace color_space) { switch (color_space) { case SCE_JPEG_COLORSPACE_GRAYSCALE: return COLORSPACE_GRAYSCALE; case SCE_JPEG_COLORSPACE_YUV444: return COLORSPACE_YUV444P; case SCE_JPEG_COLORSPACE_YUV440: return COLORSPACE_YUV440P; case SCE_JPEG_COLORSPACE_YUV441: return COLORSPACE_YUV441P; case SCE_JPEG_COLORSPACE_YUV422: return COLORSPACE_YUV422P; case SCE_JPEG_COLORSPACE_YUV420: return COLORSPACE_YUV420P; case SCE_JPEG_COLORSPACE_YUV411: return COLORSPACE_YUV411P; default: return COLORSPACE_UNKNOWN; } } // Helper functions SceJpegDHTMode get_DHT_mode(int decodeMode) { return static_cast(decodeMode & 0b111); } SceJpegDownscaleMode get_downscale_mode(int decodeMode) { return static_cast(decodeMode & (0b111 << 4)); } int get_downscale_ratio(SceJpegDownscaleMode downscaleMode) { return downscaleMode ? downscaleMode / 8 : 1; } bool is_standard_decoding(SceJpegDHTMode dhtMode) { return dhtMode == SCE_JPEG_MJPEG_WITH_DHT || dhtMode == SCE_JPEG_MJPEG_WITHOUT_DHT; } static bool is_unsupported_image_size(uint32_t width, uint32_t height) { /* Note: SCE_JPEG_ERROR_UNSUPPORT_IMAGE_SIZE is determined by the size of the decoded result, * not the actual image dimensions. It checks the pitch width and height, not the image's width and height. * If downscaling brings the image size within the supported range, this error won't occur. * Conversely, if downscaling results in dimensions outside the supported range, this error will be triggered. */ return width < 64 || height < 64 || width > 2032 || height > 1088; } // Common decoder configuration void configure_decoder(MJpegState *state, int decodeMode) { SceJpegDHTMode dhtMode = get_DHT_mode(decodeMode); SceJpegDownscaleMode downscaleMode = get_downscale_mode(decodeMode); MJpegDecoderOptions options = {}; options.use_standard_decoder = is_standard_decoding(dhtMode); options.downscale_ratio = get_downscale_ratio(downscaleMode); state->decoder->configure(&options); } EXPORT(int, sceJpegCreateSplitDecoder) { TRACY_FUNC(sceJpegCreateSplitDecoder); return UNIMPLEMENTED(); } EXPORT(int, sceJpegCsc, uint8_t *pRGBA, const uint8_t *pYCbCr, uint32_t xysize, int iFrameWidth, int colorOption, int sampling) { TRACY_FUNC(sceJpegCsc); if (colorOption & SCE_JPEG_COLORSPACE_BT601) { STUBBED("Unhandled BT601 color conversion"); colorOption &= ~SCE_JPEG_COLORSPACE_BT601; } if (colorOption != SCE_JPEG_PIXEL_RGBA8888) // sceJpegCsc only supports RGBA8888 return RET_ERROR(SCE_JPEG_ERROR_INVALID_COLOR_FORMAT); uint32_t width = xysize >> 16; uint32_t height = xysize & 0xFFFF; SceJpegColorSpace sceColorSpace = static_cast(SCE_JPEG_COLORSPACE_YUV | sampling); if (sceColorSpace != SCE_JPEG_COLORSPACE_GRAYSCALE && sceColorSpace != SCE_JPEG_COLORSPACE_YUV444) { // sceJpegCsc only supports YUV400 and YUV444 return RET_ERROR(SCE_JPEG_ERROR_INVALID_COLOR_FORMAT); } SceJpegPitch yuv_pitch[4]; auto colorSpace = convert_color_space_jpeg_to_decoder(static_cast(SCE_JPEG_COLORSPACE_YUV | sampling)); calculate_pitch_info(width, height, 0, colorSpace, false, yuv_pitch); convert_yuv_to_rgb(pYCbCr, pRGBA, iFrameWidth, colorSpace, false, yuv_pitch); return 0; } EXPORT(int, sceJpegDecodeMJpeg, const unsigned char *pJpeg, SceSize isize, uint8_t *pRGBA, SceSize osize, int decodeMode, void *pTempBuffer, SceSize tempBufferSize, void *pCoefBuffer, SceSize coefBufferSize) { TRACY_FUNC(sceJpegDecodeMJpeg, pJpeg, isize, pRGBA, osize, decodeMode, pTempBuffer, tempBufferSize, pCoefBuffer, coefBufferSize); const auto state = emuenv.kernel.obj_store.get(); configure_decoder(state, decodeMode); // the yuv data will always be smaller than the rgba data, so osize is an upper bound std::vector temporary(osize); DecoderSize size = {}; state->decoder->send(pJpeg, isize); state->decoder->receive(temporary.data(), &size); SceJpegPitch yuv_pitch[4]; state->decoder->get_pitch_info(yuv_pitch); if (is_unsupported_image_size(yuv_pitch[0].x, yuv_pitch[0].y)) { return RET_ERROR(SCE_JPEG_ERROR_UNSUPPORT_IMAGE_SIZE); } convert_yuv_to_rgb(temporary.data(), pRGBA, yuv_pitch[0].x, state->decoder->get_color_space(), false, yuv_pitch); // Top 16 bits = pitch_width, bottom 16 bits = pitch_height. return (yuv_pitch[0].x << 16u) | yuv_pitch[0].y; } EXPORT(int, sceJpegDecodeMJpegYCbCr, const uint8_t *pJpeg, SceSize isize, uint8_t *pYCbCr, SceSize osize, int decodeMode, void *pCoefBuffer, SceSize coefBufferSize) { TRACY_FUNC(sceJpegDecodeMJpegYCbCr, pJpeg, isize, pYCbCr, osize, decodeMode, pCoefBuffer, coefBufferSize); const auto state = emuenv.kernel.obj_store.get(); configure_decoder(state, decodeMode); DecoderSize size = {}; state->decoder->send(pJpeg, isize); state->decoder->receive(pYCbCr, &size); SceJpegPitch yuv_pitch[4]; state->decoder->get_pitch_info(yuv_pitch); // Top 16 bits = pitch_width, bottom 16 bits = pitch_height. return (yuv_pitch[0].x << 16u) | yuv_pitch[0].y; } EXPORT(int, sceJpegDeleteSplitDecoder) { TRACY_FUNC(sceJpegDeleteSplitDecoder); return UNIMPLEMENTED(); } EXPORT(int, sceJpegFinishMJpeg) { TRACY_FUNC(sceJpegFinishMJpeg); emuenv.kernel.obj_store.erase(); return 0; } EXPORT(int, sceJpegGetOutputInfo, const uint8_t *pJpeg, SceSize isize, SceJpegFormat format, int decodeMode, SceJpegOutputInfo *output) { /* Note: This implementation matches the basic functionality of PS Vita's sceJpegGetOutputInfo, * but with the following limitations: * 1. YUV441 is currently not supported due to FFmpeg limitations. While no games are known to use * YUV441, improvements may be necessary if such games are found. * 2. JPEG files with non-standard MCUs will be processed without errors in Vita3K, whereas they * would cause decoding errors on the PS Vita. * 3. The calculation of coefBufferSize is not supported. */ TRACY_FUNC(sceJpegGetOutputInfo, pJpeg, isize, format, decodeMode, output); if (!pJpeg || !output || !isize) return RET_ERROR(SCE_JPEG_ERROR_INVALID_POINTER); if (format != SCE_JPEG_NO_CSC_OUTPUT && format != SCE_JPEG_PIXEL_RGBA8888 && format != SCE_JPEG_PIXEL_BGRA8888) return RET_ERROR(SCE_JPEG_ERROR_INVALID_COLOR_FORMAT); const auto state = emuenv.kernel.obj_store.get(); configure_decoder(state, decodeMode); DecoderSize size = {}; state->decoder->send(pJpeg, isize); state->decoder->receive(nullptr, &size); memset(output, 0, sizeof(SceJpegOutputInfo)); output->color_space = convert_color_space_decoder_to_jpeg(state->decoder->get_color_space()); SceJpegDHTMode dhtMode = get_DHT_mode(decodeMode); bool isStandardDecodingMode = is_standard_decoding(dhtMode); // Check for unsupported color spaces in standard decoding if (isStandardDecodingMode && output->color_space == SCE_JPEG_COLORSPACE_GRAYSCALE) { return RET_ERROR(SCE_JPEG_ERROR_UNSUPPORT_COLORSPACE); } if (isStandardDecodingMode && (output->color_space != SCE_JPEG_COLORSPACE_YUV420 && output->color_space != SCE_JPEG_COLORSPACE_YUV422)) { return RET_ERROR(SCE_JPEG_ERROR_UNSUPPORT_SAMPLING); } // Check for unsupported color conversion in extended decoding if (!isStandardDecodingMode && format != SCE_JPEG_NO_CSC_OUTPUT) { return RET_ERROR(SCE_JPEG_ERROR_UNSUPPORT_DOWNSCALE); } // Fill basic output information output->width = size.width; output->height = size.height; // Should be 0 most of the time but I believe it causes more problems // for it to be 0 when it shouldn't than the opposite output->coef_buffer_size = 0x100; state->decoder->get_pitch_info(output->pitch); int totalYuvSize = 0; // Calculate total YUV buffer size for (int i = 0; i < 4; i++) { totalYuvSize += output->pitch[i].x * output->pitch[i].y; } // Adjust output information based on format if (format != SCE_JPEG_NO_CSC_OUTPUT) { // Adjust for RGBA or BGRA format output->pitch[0].x *= 4; output->pitch[1] = output->pitch[2] = output->pitch[3] = { 0, 0 }; output->temp_buffer_size = align(totalYuvSize, 256); output->output_size = align(output->pitch[0].x * output->pitch[0].y, 256); } else { // No color space conversion output->output_size = align(totalYuvSize, 256); } return 0; } EXPORT(int, sceJpegInitMJpeg, int maxSplitDecoder) { TRACY_FUNC(sceJpegInitMJpeg, maxSplitDecoder); if (maxSplitDecoder > 0) STUBBED("Ignoring non-zero maxSplitDecoder parameter"); emuenv.kernel.obj_store.create(); const auto state = emuenv.kernel.obj_store.get(); state->decoder = std::make_shared(); return 0; } EXPORT(int, sceJpegInitMJpegWithParam, const SceJpegMJpegInitInfo *param) { TRACY_FUNC(sceJpegInitMJpegWithParam, param); return CALL_EXPORT(sceJpegInitMJpeg, param->maxSplitDecoder); } EXPORT(int, sceJpegMJpegCsc, uint8_t *pRGBA, const uint8_t *pYCbCr, uint32_t xysize, int iFrameWidth, int colorOption, int sampling) { TRACY_FUNC(sceJpegMJpegCsc, pRGBA, pYCbCr, xysize, iFrameWidth, colorOption, sampling); if (colorOption & SCE_JPEG_COLORSPACE_BT601) { STUBBED("Unhandled BT601 color conversion"); colorOption &= ~SCE_JPEG_COLORSPACE_BT601; } if (colorOption != SCE_JPEG_PIXEL_RGBA8888 && colorOption != SCE_JPEG_PIXEL_BGRA8888) return RET_ERROR(SCE_JPEG_ERROR_INVALID_COLOR_FORMAT); uint32_t width = xysize >> 16; uint32_t height = xysize & 0xFFFF; if (is_unsupported_image_size(width, height)) { return RET_ERROR(SCE_JPEG_ERROR_UNSUPPORT_IMAGE_SIZE); } SceJpegPitch yuv_pitch[4]; auto colorSpace = convert_color_space_jpeg_to_decoder(static_cast(SCE_JPEG_COLORSPACE_YUV | sampling)); calculate_pitch_info(width, height, 0, colorSpace, true, yuv_pitch); convert_yuv_to_rgb(pYCbCr, pRGBA, iFrameWidth, colorSpace, colorOption == SCE_JPEG_PIXEL_BGRA8888, yuv_pitch); return 0; } EXPORT(int, sceJpegSplitDecodeMJpeg) { TRACY_FUNC(sceJpegSplitDecodeMJpeg); return UNIMPLEMENTED(); }