From bb92f955c8051f9e6a8a1dea3252a82d940ea4a6 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Sat, 28 Mar 2026 18:29:58 +0100 Subject: [PATCH] Make the movie player work somewhat. THPs are *almost* just a bunch of JPEGs so the TL;DR is BE fixes and replacing the decoding with libjpeg-turbo. Needs changes to thp.h which should be removed from Aurora, will do that later. Also audio not implemented yet. --- CMakeLists.txt | 4 +- cmake/libjpeg.cmake | 12 ++ include/dusk/map_loader_definitions.h | 3 + libs/JSystem/src/JAudio2/JASAiCtrl.cpp | 4 + src/d/actor/d_a_movie_player.cpp | 191 +++++++++++++++++++++++-- src/dusk/audio/DuskAudioSystem.cpp | 5 + 6 files changed, 203 insertions(+), 16 deletions(-) create mode 100644 cmake/libjpeg.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 583b5efa94..0fea651943 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ endif () include(FetchContent) +include(ExternalProject) message(STATUS "dusk: Fetching cxxopts") FetchContent_Declare( @@ -62,6 +63,7 @@ FetchContent_Declare( FetchContent_MakeAvailable(cxxopts) +include(cmake/libjpeg.cmake) include(files.cmake) # TODO: version handling for res includes @@ -107,7 +109,7 @@ add_library(game SHARED ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${SSYSTEM_FILES} ${J src/dusk/imgui/ImGuiStubLog.cpp src/dusk/imgui/ImGuiAudio.cpp) -target_link_libraries(game PRIVATE game_debug cxxopts::cxxopts) +target_link_libraries(game PRIVATE game_debug cxxopts::cxxopts libjpeg-turbo-lib) target_compile_definitions(game PRIVATE TARGET_PC AVOID_UB=1 VERSION=0 NDEBUG=1 NDEBUG_DEFINED=1 DEBUG_DEFINED=0 DUSK_TP_VERSION="${DUSK_TP_VERSION}" DUSK_GAME_NAME="${DUSK_GAME_NAME}" DUSK_GAME_VERSION="${DUSK_GAME_VERSION}") add_executable(dusk src/dusk/main.cpp) diff --git a/cmake/libjpeg.cmake b/cmake/libjpeg.cmake new file mode 100644 index 0000000000..b7f719f003 --- /dev/null +++ b/cmake/libjpeg.cmake @@ -0,0 +1,12 @@ +ExternalProject_Add( + libjpeg-turbo + URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.1.90.tar.gz + URL_HASH SHA256=076ef1431f2803a91f07e0f92433d4dcf39bc9113226c4f46ba3d3d54f514c9d + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX= +) + +ExternalProject_Get_Property(libjpeg-turbo INSTALL_DIR) +add_library(libjpeg-turbo-lib STATIC IMPORTED GLOBAL) +set_target_properties(libjpeg-turbo-lib PROPERTIES IMPORTED_LOCATION ${INSTALL_DIR}/lib/turbojpeg-static.lib) +target_include_directories(libjpeg-turbo-lib INTERFACE ${INSTALL_DIR}/include) diff --git a/include/dusk/map_loader_definitions.h b/include/dusk/map_loader_definitions.h index c2c9e4fedb..48c04d6d4d 100644 --- a/include/dusk/map_loader_definitions.h +++ b/include/dusk/map_loader_definitions.h @@ -629,5 +629,8 @@ constexpr auto gameRegions = std::to_array({ MapEntry("Cutscene: Hyrule Castle Throne Room", "R_SP301", { {0, {0, 20, 100}}, }), + MapEntry("Title screen movie map", "S_MV000", { + {0, {0, 1}}, + }), }) }); diff --git a/libs/JSystem/src/JAudio2/JASAiCtrl.cpp b/libs/JSystem/src/JAudio2/JASAiCtrl.cpp index 1285d68359..ede7c1d267 100644 --- a/libs/JSystem/src/JAudio2/JASAiCtrl.cpp +++ b/libs/JSystem/src/JAudio2/JASAiCtrl.cpp @@ -260,6 +260,10 @@ void JASDriver::finishDSPFrame() { } void JASDriver::registerMixCallback(MixCallback param_0, JASMixMode param_1) { +#if TARGET_PC + JASCriticalSection section; +#endif + extMixCallback = param_0; sMixMode = param_1; } diff --git a/src/d/actor/d_a_movie_player.cpp b/src/d/actor/d_a_movie_player.cpp index 0f9f68fb50..eca4bdb9e1 100644 --- a/src/d/actor/d_a_movie_player.cpp +++ b/src/d/actor/d_a_movie_player.cpp @@ -14,21 +14,29 @@ #pragma optimization_level 4 #pragma optimize_for_size off -#include "JSystem/JKernel/JKRExpHeap.h" +#include +#include #include "JSystem/JAudio2/JASAiCtrl.h" #include "JSystem/JAudio2/JASDriverIF.h" -#include "d/actor/d_a_movie_player.h" +#include "JSystem/JKernel/JKRExpHeap.h" #include "Z2AudioLib/Z2Instances.h" +#include "d/actor/d_a_movie_player.h" + +#include + #include "f_op/f_op_overlap_mng.h" -#include #include "dusk/gx_helper.h" +#include "dusk/memory.h" +#include "dusk/os.h" + +#include "turbojpeg.h" inline s32 daMP_NEXT_READ_SIZE(daMP_THPReadBuffer* readBuf) { - return *(s32*)readBuf->ptr; + return *(BE(s32)*)readBuf->ptr; } -#ifdef __cplusplus +#if defined(__cplusplus) && !TARGET_PC extern "C" { #endif @@ -196,6 +204,7 @@ static void __THPAudioInitialize(THPAudioDecodeInfo* info, u8* ptr) { info->encodeData++; } +#if !TARGET_PC static u8 THPStatistics[1120] ATTRIBUTE_ALIGN(32); static THPHuffmanTab* Ydchuff ATTRIBUTE_ALIGN(32); @@ -2562,8 +2571,116 @@ static void __THPHuffDecodeDCTCompV(__REGISTER THPFileInfo* info, THPCoeff* bloc } } } +#else // !TARGET_PC + +static std::vector FixJpeg(const std::span data) { + std::vector fixedData; + fixedData.reserve(data.size()); + + size_t startOfScanLocation = 0; + for (; startOfScanLocation < data.size() - 1; startOfScanLocation++) { + if (data[startOfScanLocation] == 0xFF && data[startOfScanLocation + 1] == 0xDA) { + goto sosFound; + } + } + + CRASH("Unable to find SOS marker!"); + + sosFound: + + startOfScanLocation += 2; // TODO: Skip entire SOS header? + + size_t endOfImage = data.size() - 1; + for (; endOfImage > startOfScanLocation; endOfImage--) { + if (data[endOfImage] == 0xFF && data[endOfImage + 1] == 0xD9) { + goto eoiFound; + } + } + + CRASH("Unable to find EOI marker!"); + eoiFound: + + // Copy data before SOS + for (size_t i = 0; i < startOfScanLocation; i++) { + fixedData.push_back(data[i]); + } + + // Copy data inside SOS, fixing up lacking of "byte shuffling" + for (size_t i = startOfScanLocation; i < endOfImage; i++) { + u8 value = data[i]; + fixedData.push_back(value); + if (value == 0xFF) { + fixedData.push_back(0x00); + } + } + + // Copy data after SOS. + for (size_t i = endOfImage; i < data.size(); i++) { + fixedData.push_back(data[i]); + } + + return fixedData; +} + +struct TjHandle { + tjhandle mHandle; + + explicit TjHandle(const tjhandle handle) : mHandle(handle) {} + ~TjHandle() { + tj3Destroy(mHandle); + } + + operator tjhandle() const { + return mHandle; + } +}; + +extern daMP_THPPlayer daMP_ActivePlayer; + +static s32 THPVideoDecode(void* file, size_t fileSize, void* tileY, void* tileU, void* tileV, void* work) { + const TjHandle handle(tj3Init(TJINIT_DECOMPRESS)); + if (handle == nullptr) { + OSReport_Error("Failed to create turbojpeg handle: %s", tj3GetErrorStr(nullptr)); + return 1; + } + + const auto fixedData = FixJpeg(std::span(static_cast(file), fileSize)); + + auto ret = tj3DecompressHeader(handle, fixedData.data(), fixedData.size()); + if (ret == -1) { + OSReport_Error("Parsing JPEG header failed: %s", tj3GetErrorStr(handle)); + return 1; + } + + if (tj3Get(handle, TJPARAM_JPEGWIDTH) != daMP_ActivePlayer.videoInfo.xSize) { + OSReport_Error("Invalid width in video frame!"); + return 1; + } + + if (tj3Get(handle, TJPARAM_JPEGHEIGHT) != daMP_ActivePlayer.videoInfo.ySize) { + OSReport_Error("Invalid height in video frame!"); + return 1; + } + + ret = tj3Set(handle, TJPARAM_SUBSAMP, TJSAMP_420); + if (ret != 0) { + OSReport_Error("Failed to set subsampling mode: %s", tj3GetErrorStr(handle)); + return 1; + } + + u8* planes[3] = {static_cast(tileY), static_cast(tileU), static_cast(tileV)}; + ret = tj3DecompressToYUVPlanes8(handle, fixedData.data(), fixedData.size(), planes, nullptr); + if (ret != 0) { + OSReport_Error("Image decompression failed: %s", tj3GetErrorStr(handle)); + return 1; + } + + return 0; +} +#endif static BOOL THPInit() { +#if !TARGET_PC u8* base; base = (u8*)(0xE000 << 16); @@ -2585,10 +2702,11 @@ static BOOL THPInit() { OSInitFastCast(); __THPInitFlag = TRUE; +#endif return TRUE; } -#ifdef __cplusplus +#if defined(__cplusplus) && !TARGET_PC } #endif @@ -2654,6 +2772,10 @@ void daMP_ReadThreadCancel() { } void* daMP_Reader(void*) { +#if TARGET_PC + OSSetCurrentThreadName("movie player reader"); +#endif + daMP_THPReadBuffer* buf; s32 curFrame; s32 status; @@ -2751,10 +2873,10 @@ static BOOL daMP_First; static void daMP_VideoDecode(daMP_THPReadBuffer* readBuffer) { THPTextureSet* textureSet; s32 i; - u32* tileOffsets; + BE(u32)* tileOffsets; u8* tile; - tileOffsets = (u32*)(readBuffer->ptr + 8); + tileOffsets = (BE(u32)*)(readBuffer->ptr + 8); tile = &readBuffer->ptr[daMP_ActivePlayer.compInfo.numComponents * 4] + 8; textureSet = (THPTextureSet*)daMP_PopFreeTextureSet(); @@ -2762,8 +2884,13 @@ static void daMP_VideoDecode(daMP_THPReadBuffer* readBuffer) { switch (daMP_ActivePlayer.compInfo.frameComp[i]) { case 0: { if ((daMP_ActivePlayer.videoError = THPVideoDecode( - tile, textureSet->ytexture, textureSet->utexture, - textureSet->vtexture, daMP_ActivePlayer.thpWork))) { + tile, +#if TARGET_PC + *tileOffsets, +#endif + textureSet->ytexture, textureSet->utexture, + textureSet->vtexture, + daMP_ActivePlayer.thpWork))) { if (daMP_First) { daMP_PrepareReady(FALSE); daMP_First = FALSE; @@ -2789,6 +2916,10 @@ static void daMP_VideoDecode(daMP_THPReadBuffer* readBuffer) { } static void* daMP_VideoDecoder(void* param_0) { +#if TARGET_PC + OSSetCurrentThreadName("video decoder"); +#endif + daMP_THPReadBuffer* thpBuffer; while (TRUE) { @@ -2820,6 +2951,10 @@ static void* daMP_VideoDecoder(void* param_0) { } static void* daMP_VideoDecoderForOnMemory(void* param_0) { +#if TARGET_PC + OSSetCurrentThreadName("video decoder"); +#endif + daMP_THPReadBuffer readBuffer; s32 readSize; s32 frame; @@ -2944,10 +3079,10 @@ static void daMP_PushDecodedAudioBuffer(void* buffer) { static void daMP_AudioDecode(daMP_THPReadBuffer* readBuffer) { THPAudioBuffer* audioBuf; s32 i; - u32* offsets; + BE(u32)* offsets; u8* audioData; - offsets = (u32*)(readBuffer->ptr + 8); + offsets = (BE(u32)*)(readBuffer->ptr + 8); audioData = &readBuffer->ptr[daMP_ActivePlayer.compInfo.numComponents * 4] + 8; audioBuf = (THPAudioBuffer*)daMP_PopFreeAudioBuffer(); @@ -2969,6 +3104,10 @@ static void daMP_AudioDecode(daMP_THPReadBuffer* readBuffer) { } static void* daMP_AudioDecoder(void* param_0) { +#if TARGET_PC + OSSetCurrentThreadName("movie audio decoder"); +#endif + daMP_THPReadBuffer* buf; while (TRUE) { @@ -2979,6 +3118,10 @@ static void* daMP_AudioDecoder(void* param_0) { } static void* daMP_AudioDecoderForOnMemory(void* param_0) { +#if TARGET_PC + OSSetCurrentThreadName("movie audio decoder"); +#endif + s32 size; s32 readSize; daMP_THPReadBuffer readBuffer; @@ -3168,19 +3311,26 @@ static void daMP_THPGXYuv2RgbDraw(u8* y_data, u8* u_data, u8* v_data, s16 x, TGXTexObj tobj0; TGXTexObj tobj1; TGXTexObj tobj2; +#if TARGET_PC +#define FMT (GXTexFmt)GX_TF_R8_PC +#else +#define FMT GX_TF_I8 +#endif - GXInitTexObj(&tobj0, y_data, textureWidth, textureHeight, GX_TF_I8, GX_CLAMP, GX_CLAMP, GX_FALSE); + GXInitTexObj(&tobj0, y_data, textureWidth, textureHeight, FMT, GX_CLAMP, GX_CLAMP, GX_FALSE); GXInitTexObjLOD(&tobj0, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, 0, 0, GX_ANISO_1); GXLoadTexObj(&tobj0, GX_TEXMAP0); - GXInitTexObj(&tobj1, u_data, textureWidth >> 1, textureHeight >> 1, GX_TF_I8, GX_CLAMP, GX_CLAMP, GX_FALSE); + GXInitTexObj(&tobj1, u_data, textureWidth >> 1, textureHeight >> 1, FMT, GX_CLAMP, GX_CLAMP, GX_FALSE); GXInitTexObjLOD(&tobj1, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, 0, 0, GX_ANISO_1); GXLoadTexObj(&tobj1, GX_TEXMAP1); - GXInitTexObj(&tobj2, v_data, textureWidth >> 1, textureHeight >> 1, GX_TF_I8, GX_CLAMP, GX_CLAMP, GX_FALSE); + GXInitTexObj(&tobj2, v_data, textureWidth >> 1, textureHeight >> 1, FMT, GX_CLAMP, GX_CLAMP, GX_FALSE); GXInitTexObjLOD(&tobj2, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, 0, 0, GX_ANISO_1); GXLoadTexObj(&tobj2, GX_TEXMAP2); +#undef FMT + GXBegin(GX_QUADS, GX_VTXFMT7, 4); GXPosition3s16(x, y, 0); GXTexCoord2u16(0, 0); @@ -3191,6 +3341,12 @@ static void daMP_THPGXYuv2RgbDraw(u8* y_data, u8* u_data, u8* v_data, s16 x, GXPosition3s16(x, y + polygonHeight, 0); GXTexCoord2u16(0, 1); GXEnd(); + +#if TARGET_PC + GXDestroyTexObj(&tobj0); + GXDestroyTexObj(&tobj1); + GXDestroyTexObj(&tobj2); +#endif } static u16 daMP_VolumeTable[] = { @@ -3546,6 +3702,11 @@ static BOOL daMP_THPPlayerSetBuffer(u8* buffer) { ysize = ALIGN_NEXT(daMP_ActivePlayer.videoInfo.xSize * daMP_ActivePlayer.videoInfo.ySize, 32); uvsize = ALIGN_NEXT(daMP_ActivePlayer.videoInfo.xSize * daMP_ActivePlayer.videoInfo.ySize / 4, 32); +#if TARGET_PC + assert(ysize >= tj3YUVPlaneSize(0, daMP_ActivePlayer.videoInfo.xSize, 0, daMP_ActivePlayer.videoInfo.ySize, TJSAMP_420)); + assert(uvsize >= tj3YUVPlaneSize(1, daMP_ActivePlayer.videoInfo.xSize, 0, daMP_ActivePlayer.videoInfo.ySize, TJSAMP_420)); + assert(uvsize >= tj3YUVPlaneSize(2, daMP_ActivePlayer.videoInfo.xSize, 0, daMP_ActivePlayer.videoInfo.ySize, TJSAMP_420)); +#endif for (i = 0; i < ARRAY_SIZE(daMP_ActivePlayer.textureSet); i++) { daMP_ActivePlayer.textureSet[i].ytexture = ptr; diff --git a/src/dusk/audio/DuskAudioSystem.cpp b/src/dusk/audio/DuskAudioSystem.cpp index 0ed2101dcf..522f4c9d60 100644 --- a/src/dusk/audio/DuskAudioSystem.cpp +++ b/src/dusk/audio/DuskAudioSystem.cpp @@ -111,6 +111,11 @@ int RenderNewAudioFrame() { outRaw.flush(); #endif + if (JASDriver::extMixCallback != nullptr) { + // TODO: actually mix this data in. + JASDriver::extMixCallback(countSubframes * DSP_SUBFRAME_SIZE); + } + return static_cast(countSubframes) * DSP_SUBFRAME_SIZE; }