diff --git a/.github/workflows/libdmdutil.yml b/.github/workflows/libdmdutil.yml index 7b67fdeb..281f651c 100644 --- a/.github/workflows/libdmdutil.yml +++ b/.github/workflows/libdmdutil.yml @@ -62,21 +62,25 @@ jobs: name: Add autoconf and automake (mac runner) run: | brew install autoconf automake libtool + - if: (matrix.platform == 'linux') + name: Add libudev and autotools (linux runner) + run: | + sudo apt-get update + sudo apt install libudev-dev autoconf automake libtool pkg-config - if: (matrix.platform == 'linux' && matrix.arch == 'aarch64') name: Add libgpiod (Raspberry Pi Linux) run: | - sudo apt-get update sudo apt-get install -y libgpiod-dev - name: Build libdmdutil-${{ matrix.platform }}-${{ matrix.arch }} run: | ./platforms/${{ matrix.platform }}/${{ matrix.arch }}/external.sh if [[ "${{ matrix.platform }}" == "win" ]]; then if [[ "${{ matrix.arch }}" == "x64" ]]; then - cmake -G "Visual Studio 17 2022" -DPLATFORM=${{ matrix.platform }} -DARCH=${{ matrix.arch }} -B build + cmake -G "Visual Studio 17 2022" -DPLATFORM=${{ matrix.platform }} -DARCH=${{ matrix.arch }} -B build elif [[ "${{ matrix.arch }}" == "x86" ]]; then - cmake -G "Visual Studio 17 2022" -A Win32 -DPLATFORM=${{ matrix.platform }} -DARCH=${{ matrix.arch }} -B build + cmake -G "Visual Studio 17 2022" -A Win32 -DPLATFORM=${{ matrix.platform }} -DARCH=${{ matrix.arch }} -B build elif [[ "${{ matrix.arch }}" == "arm64" ]]; then - cmake -G "Visual Studio 17 2022" -A ARM64 -DPLATFORM=${{ matrix.platform }} -DARCH=${{ matrix.arch }} -B build + cmake -G "Visual Studio 17 2022" -A ARM64 -DPLATFORM=${{ matrix.platform }} -DARCH=${{ matrix.arch }} -B build fi cmake --build build --config Release else diff --git a/.gitignore b/.gitignore index 6dc5fb3c..01809edc 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,7 @@ third-party/include/serum-decode.h third-party/include/ZeDMD.h third-party/include/LZ4Stream.h third-party/include/lz4 +third-party/include/TimeUtils.h +third-party/include/vni.h +third-party/include/libusb-1.0 .DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ad3b70e..e5a134c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,12 +95,31 @@ set(DMDUTIL_SOURCES src/DMDServer.cpp ) +set(DMDUTIL_COMPILE_DEFINITIONS "") +set(DMDUTIL_PIN2DMD_LIBS "") + if(PLATFORM STREQUAL "win" OR PLATFORM STREQUAL "macos" OR PLATFORM STREQUAL "linux") list(APPEND DMDUTIL_SOURCES src/PixelcadeDMD.cpp ) endif() +if(PLATFORM STREQUAL "win" OR PLATFORM STREQUAL "macos" OR PLATFORM STREQUAL "linux") + list(APPEND DMDUTIL_SOURCES + src/PIN2DMD.cpp + ) + list(APPEND DMDUTIL_COMPILE_DEFINITIONS DMDUTIL_ENABLE_PIN2DMD) + if(PLATFORM STREQUAL "win") + if(ARCH STREQUAL "x64") + list(APPEND DMDUTIL_PIN2DMD_LIBS libusb64-1.0) + else() + list(APPEND DMDUTIL_PIN2DMD_LIBS libusb-1.0) + endif() + else() + list(APPEND DMDUTIL_PIN2DMD_LIBS usb-1.0) + endif() +endif() + set(DMDUTIL_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include third-party/include @@ -110,6 +129,9 @@ if(BUILD_SHARED) add_library(dmdutil_shared SHARED ${DMDUTIL_SOURCES}) target_include_directories(dmdutil_shared PUBLIC ${DMDUTIL_INCLUDE_DIRS}) + if(DMDUTIL_COMPILE_DEFINITIONS) + target_compile_definitions(dmdutil_shared PUBLIC ${DMDUTIL_COMPILE_DEFINITIONS}) + endif() if(PLATFORM STREQUAL "win") target_link_directories(dmdutil_shared PUBLIC @@ -117,30 +139,34 @@ if(BUILD_SHARED) third-party/runtime-libs/${PLATFORM}/${ARCH} ) if(ARCH STREQUAL "x64") - target_link_libraries(dmdutil_shared PUBLIC cargs64 zedmd64 serum64 libserialport64 sockpp64 pupdmd64 ws2_32) + target_link_libraries(dmdutil_shared PUBLIC cargs64 zedmd64 serum64 vni64 libserialport64 sockpp64 pupdmd64 ws2_32) else() - target_link_libraries(dmdutil_shared PUBLIC cargs zedmd serum libserialport sockpp pupdmd ws2_32) + target_link_libraries(dmdutil_shared PUBLIC cargs zedmd serum vni libserialport sockpp pupdmd ws2_32) endif() elseif(PLATFORM STREQUAL "macos") target_link_directories(dmdutil_shared PUBLIC third-party/runtime-libs/${PLATFORM}/${ARCH} ) - target_link_libraries(dmdutil_shared PUBLIC cargs zedmd serum serialport sockpp pupdmd) + target_link_libraries(dmdutil_shared PUBLIC cargs zedmd serum vni serialport sockpp pupdmd) elseif(PLATFORM STREQUAL "linux") target_link_directories(dmdutil_shared PUBLIC third-party/runtime-libs/${PLATFORM}/${ARCH} ) - target_link_libraries(dmdutil_shared PUBLIC cargs zedmd serum serialport sockpp pupdmd) + target_link_libraries(dmdutil_shared PUBLIC cargs zedmd serum vni serialport sockpp pupdmd) elseif(PLATFORM STREQUAL "ios" OR PLATFORM STREQUAL "ios-simulator" OR PLATFORM STREQUAL "tvos") target_link_directories(dmdutil_shared PUBLIC third-party/build-libs/${PLATFORM}/${ARCH} ) - target_link_libraries(dmdutil_shared PUBLIC zedmd serum sockpp pupdmd) + target_link_libraries(dmdutil_shared PUBLIC zedmd serum vni sockpp pupdmd) elseif(PLATFORM STREQUAL "android") target_link_directories(dmdutil_shared PUBLIC third-party/runtime-libs/${PLATFORM}/${ARCH} ) - target_link_libraries(dmdutil_shared PUBLIC zedmd serum sockpp pupdmd) + target_link_libraries(dmdutil_shared PUBLIC zedmd serum vni sockpp pupdmd) + endif() + + if(DMDUTIL_PIN2DMD_LIBS) + target_link_libraries(dmdutil_shared PUBLIC ${DMDUTIL_PIN2DMD_LIBS}) endif() if(PLATFORM STREQUAL "win" AND ARCH STREQUAL "x64") @@ -181,11 +207,17 @@ if(BUILD_SHARED) ) target_link_libraries(dmdutil-generate-scenes PUBLIC dmdutil_shared) + add_executable(dmdutil-play-dump + src/playDump.cpp + ) + target_link_libraries(dmdutil-play-dump PUBLIC dmdutil_shared) + if(POST_BUILD_COPY_EXT_LIBS) add_dependencies(dmdserver copy_ext_libs) add_dependencies(dmdserver_test copy_ext_libs) add_dependencies(dmdutil_test copy_ext_libs) add_dependencies(dmdutil-generate-scenes copy_ext_libs) + add_dependencies(dmdutil-play-dump copy_ext_libs) endif() endif() endif() @@ -194,6 +226,13 @@ if(BUILD_STATIC) add_library(dmdutil_static STATIC ${DMDUTIL_SOURCES}) target_include_directories(dmdutil_static PUBLIC ${DMDUTIL_INCLUDE_DIRS}) + if(DMDUTIL_COMPILE_DEFINITIONS) + target_compile_definitions(dmdutil_static PUBLIC ${DMDUTIL_COMPILE_DEFINITIONS}) + endif() + + if(DMDUTIL_PIN2DMD_LIBS) + target_link_libraries(dmdutil_static PUBLIC ${DMDUTIL_PIN2DMD_LIBS}) + endif() if(PLATFORM STREQUAL "win") set_target_properties(dmdutil_static PROPERTIES @@ -221,20 +260,20 @@ if(BUILD_STATIC) third-party/runtime-libs/${PLATFORM}/${ARCH} ) if(ARCH STREQUAL "x64") - target_link_libraries(dmdutil_test_s PUBLIC dmdutil_static cargs64 zedmd64 serum64 libserialport64 sockpp64 pupdmd64 ws2_32) - else() - target_link_libraries(dmdutil_test_s PUBLIC dmdutil_static cargs zedmd serum libserialport sockpp pupdmd ws2_32) - endif() + target_link_libraries(dmdutil_test_s PUBLIC dmdutil_static cargs64 zedmd64 serum64 vni64 libserialport64 sockpp64 pupdmd64 ws2_32) + else() + target_link_libraries(dmdutil_test_s PUBLIC dmdutil_static cargs zedmd serum vni libserialport sockpp pupdmd ws2_32) + endif() elseif(PLATFORM STREQUAL "macos") target_link_directories(dmdutil_test_s PUBLIC third-party/runtime-libs/${PLATFORM}/${ARCH} ) - target_link_libraries(dmdutil_test_s PUBLIC dmdutil_static cargs zedmd serum serialport sockpp pupdmd) + target_link_libraries(dmdutil_test_s PUBLIC dmdutil_static cargs zedmd serum vni serialport sockpp pupdmd) elseif(PLATFORM STREQUAL "linux") target_link_directories(dmdutil_test_s PUBLIC third-party/runtime-libs/${PLATFORM}/${ARCH} ) - target_link_libraries(dmdutil_test_s PUBLIC dmdutil_static cargs zedmd serum serialport sockpp pupdmd) + target_link_libraries(dmdutil_test_s PUBLIC dmdutil_static cargs zedmd serum vni serialport sockpp pupdmd) endif() if(POST_BUILD_COPY_EXT_LIBS) diff --git a/README.md b/README.md index 28356314..9c8f0533 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A cross platform library for performing DMD tasks. -This library is currently used by [Visual Pinball Standalone](https://github.com/vpinball/vpinball/tree/standalone) for processing [PinMAME](https://github.com/vpinball/pinmame/tree/master/src/libpinmame) and [FlexDMD](https://github.com/vbousquet/flexdmd) DMD frames. It supports colorizing using [Serum](https://github.com/PPUC/libserum), outputing to [ZeDMD](https://github.com/PPUC/zedmd) and [Pixelcade](https://pixelcade.org) devices, and providing intensity and RGB24 buffers that can be used to render in table and external DMDs for [Visual Pinball](https://github.com/vpinball/vpinball). +This library is currently used by [Visual Pinball Standalone](https://github.com/vpinball/vpinball/tree/standalone) for processing [PinMAME](https://github.com/vpinball/pinmame/tree/master/src/libpinmame) and [FlexDMD](https://github.com/vbousquet/flexdmd) DMD frames. It supports colorizing using [Serum](https://github.com/PPUC/libserum), outputing to [ZeDMD](https://github.com/PPUC/zedmd), [Pixelcade](https://pixelcade.org) and [PIN2DMD](https://https://pin2dmd.com/)devices, and providing intensity and RGB24 buffers that can be used to render in table and external DMDs for [Visual Pinball](https://github.com/vpinball/vpinball). ## Usage: @@ -211,6 +211,10 @@ Height = 32 Enabled = 1 # Disable auto-detection and provide a fixed serial port Device = + +[PIN2DMD] +# Set to 1 if PIN2DMD is attached +Enabled = 0 ``` ## Serum PUP Scenes Generator @@ -231,6 +235,31 @@ The `dmdutil-generate-scenes` tool generates a dump of such scene frames accordi -h, --help Show help ``` +## DMD Dump Player + +`dmdutil-play-dump` plays an existing txt, rgb565, rgb888, or raw dump and sends the frames to all attached DMDs. Txt/raw inputs are sent as +2-bit or 4-bit data frames, while rgb565/rgb888 inputs are sent as color frames. The timestamps in the dump represent the absolute time +(ms since start). It can optionally connect to a remote DMD server and can dump txt/rgb565/rgb888 while playing (raw output is not supported). +Dump output uses the live DMD dumpers (same as libdmdutil), so colorized frames are preserved. By default, playback uses the original frame +timings from the dump. Use `--delay-ms` to cap the per-frame delay; if a frame's original duration is shorter, the original duration is used. + +`dmdutil-play-dump` accepts these command line options: +``` + -i, --input=FILE Input dump file (.txt, .565.txt, .888.txt, or .raw) + -a, --alt-color-path=PATH Alt color base path (optional, enables Serum colorization) + -d, --depth=VALUE Bit depth to send (2 or 4) (optional, default is 2) + -s, --server=HOST[:PORT] Connect to a DMD server (optional) + -L, --no-local Disable local displays + -t, --dump-txt Dump txt while playing + -5, --dump-565 Dump rgb565 while playing + -8, --dump-888 Dump rgb888 while playing + -w, --delay-ms[=MS] Fixed delay between frames in milliseconds (optional, default is 8 when specified without a value) + -o, --dump-path=PATH Output path for dumps (optional) + -r, --rom=NAME ROM name for dumps (optional) + -R, --raw Force raw dump parsing + -h, --help Show help +``` + ## Building: #### Windows (x64) diff --git a/dmdserver.ini b/dmdserver.ini index 7c6f8af6..cadc7ff2 100644 --- a/dmdserver.ini +++ b/dmdserver.ini @@ -59,3 +59,7 @@ Height = 32 Enabled = 0 # Disable auto-detection and provide a fixed serial port Device = + +[PIN2DMD] +# Set to 1 if PIN2DMD is attached +Enabled = 0 diff --git a/include/DMDUtil/Config.h b/include/DMDUtil/Config.h index 39151d86..33ecc133 100644 --- a/include/DMDUtil/Config.h +++ b/include/DMDUtil/Config.h @@ -45,6 +45,8 @@ class DMDUTILAPI Config void SetPUPCapture(bool pupCapture) { m_pupCapture = pupCapture; } bool IsSerumPUPTriggers() const { return m_serumPupTriggers; } void SetSerumPUPTriggers(bool serumPupTriggers) { m_serumPupTriggers = serumPupTriggers; } + void SetVniKey(const char* key) { m_vniKey = key ? key : ""; } + const char* GetVniKey() const { return m_vniKey.c_str(); } void SetPUPVideosPath(const char* path) { m_pupVideosPath = path; } const char* GetPUPVideosPath() const { return m_pupVideosPath.c_str(); } bool IsPUPExactColorMatch() const { return m_pupExactColorMatch; } @@ -92,6 +94,8 @@ class DMDUTILAPI Config void SetPixelcade(bool pixelcade) { m_pixelcade = pixelcade; } void SetPixelcadeDevice(const char* port) { m_pixelcadeDevice = port; } const char* GetPixelcadeDevice() const { return m_pixelcadeDevice.c_str(); } + bool IsPIN2DMD() const { return m_PIN2DMD; } + void SetPIN2DMD(bool PIN2DMD) { m_PIN2DMD = PIN2DMD; } void SetDMDServer(bool dmdServer) { m_dmdServer = dmdServer; @@ -125,6 +129,7 @@ class DMDUTILAPI Config std::string m_altColorPath; bool m_pupCapture; bool m_serumPupTriggers; + std::string m_vniKey; std::string m_pupVideosPath; bool m_pupExactColorMatch; int m_framesTimeout; @@ -151,6 +156,7 @@ class DMDUTILAPI Config int m_dmdServerPort; bool m_pixelcade; std::string m_pixelcadeDevice; + bool m_PIN2DMD; DMDUtil_LogLevel m_logLevel; DMDUtil_LogCallback m_logCallback; DMDUtil_PUPTriggerCallbackContext m_pupTriggerCallbackContext; diff --git a/include/DMDUtil/DMD.h b/include/DMDUtil/DMD.h index c954e33d..d878e9e9 100644 --- a/include/DMDUtil/DMD.h +++ b/include/DMDUtil/DMD.h @@ -32,6 +32,7 @@ class ZeDMD; struct _Serum_Frame_Struc; typedef _Serum_Frame_Struc SerumFrameStruct; +struct Vni_Context; namespace PUPDMD { @@ -89,12 +90,13 @@ class DMDUTILAPI DMD SerumV2_64 = 8, SerumV2_64_32 = 9, NotColorized = 10, + Vni = 11, }; bool IsSerumMode(Mode mode, bool showNotColorized = false) { return (mode == Mode::SerumV1 || mode == Mode::SerumV2_32 || mode == Mode::SerumV2_32_64 || - mode == Mode::SerumV2_64 || mode == Mode::SerumV2_64_32 || + mode == Mode::SerumV2_64 || mode == Mode::SerumV2_64_32 || mode == Mode::Vni || (showNotColorized && mode == Mode::NotColorized)); } @@ -163,6 +165,10 @@ class DMDUTILAPI DMD void SetPUPTrigger(const char source, const uint16_t id, const uint8_t value = 1); void DumpDMDTxt(); void DumpDMDRaw(); + void DumpDMDRgb565(); + void DumpDMDRgb888(); + uint16_t GetUpdateQueuePosition() const; + bool WaitForDumpers(uint16_t targetPosition, uint32_t timeoutMs); LevelDMD* CreateLevelDMD(uint16_t width, uint16_t height, bool sam); bool DestroyLevelDMD(LevelDMD* pLevelDMD); void AddRGB24DMD(RGB24DMD* pRGB24DMD); @@ -172,28 +178,55 @@ class DMDUTILAPI DMD bool DestroyConsoleDMD(ConsoleDMD* pConsoleDMD); void UpdateData(const uint8_t* pData, int depth, uint16_t width, uint16_t height, uint8_t r, uint8_t g, uint8_t b, bool buffered = false); + void UpdateDataWithTimestamp(const uint8_t* pData, int depth, uint16_t width, uint16_t height, uint8_t r, uint8_t g, + uint8_t b, uint32_t timestampMs, bool buffered = false); void UpdateRGB24Data(const uint8_t* pData, int depth, uint16_t width, uint16_t height, uint8_t r, uint8_t g, uint8_t b, bool buffer = false); void UpdateRGB24Data(const uint8_t* pData, uint16_t width, uint16_t height, bool buffered = false); + void UpdateRGB24DataWithTimestamp(const uint8_t* pData, int depth, uint16_t width, uint16_t height, uint8_t r, + uint8_t g, uint8_t b, uint32_t timestampMs, bool buffered = false); + void UpdateRGB24DataWithTimestamp(const uint8_t* pData, uint16_t width, uint16_t height, uint32_t timestampMs, + bool buffered = false); void UpdateRGB16Data(const uint16_t* pData, uint16_t width, uint16_t height, bool buffered = false); + void UpdateRGB16DataWithTimestamp(const uint16_t* pData, uint16_t width, uint16_t height, uint32_t timestampMs, + bool buffered = false); void UpdateAlphaNumericData(AlphaNumericLayout layout, const uint16_t* pData1, const uint16_t* pData2, uint8_t r, uint8_t g, uint8_t b); - void QueueUpdate(const std::shared_ptr dmdUpdate, bool buffered); + void QueueUpdate(const std::shared_ptr dmdUpdate, bool buffered, bool hasTimestamp = false, + uint32_t timestampMs = 0); bool QueueBuffer(); private: Update* m_pUpdateBufferQueue[DMDUTIL_FRAME_BUFFER_SIZE]; std::shared_ptr m_updateBuffered; + uint32_t m_updateBufferQueueTimestamp[DMDUTIL_FRAME_BUFFER_SIZE] = {0}; + bool m_updateBufferQueueHasTimestamp[DMDUTIL_FRAME_BUFFER_SIZE] = {false}; + uint32_t m_serumLastTimestampMs = 0; + bool m_serumHasTimestamp = false; + std::mutex m_dumpPositionMutex; + std::condition_variable m_dumpPositionCv; + std::atomic m_dumpTxtPosition{0}; + std::atomic m_dumpRawPosition{0}; + std::atomic m_dump565Position{0}; + std::atomic m_dump888Position{0}; + std::atomic m_dumpTxtActive{false}; + std::atomic m_dumpRawActive{false}; + std::atomic m_dump565Active{false}; + std::atomic m_dump888Active{false}; uint16_t GetNextBufferQueuePosition(uint16_t bufferPosition, const uint16_t updateBufferQueuePosition); bool ConnectDMDServer(); bool UpdatePalette(uint8_t* pPalette, uint8_t depth, uint8_t r, uint8_t g, uint8_t b); void UpdateData(const uint8_t* pData, int depth, uint16_t width, uint16_t height, uint8_t r, uint8_t g, uint8_t b, Mode mode, bool buffered = false); + void UpdateDataWithTimestampInternal(const uint8_t* pData, int depth, uint16_t width, uint16_t height, uint8_t r, + uint8_t g, uint8_t b, Mode mode, uint32_t timestampMs, bool buffered = false); void AdjustRGB24Depth(uint8_t* pData, uint8_t* pDstData, int length, uint8_t* palette, uint8_t depth); void HandleTrigger(uint16_t id); - void QueueSerumFrames(Update* dmdUpdate, bool render32 = true, bool render64 = true); + void QueueSerumFrames(Update* dmdUpdate, bool render32 = true, bool render64 = true, bool hasTimestamp = false, + uint32_t timestampMs = 0); void GenerateRandomSuffix(char* buffer, size_t length); + bool DumpersReached(uint16_t targetPosition) const; void DmdFrameThread(); void LevelDMDThread(); @@ -202,8 +235,13 @@ class DMDUTILAPI DMD void ZeDMDThread(); void DumpDMDTxtThread(); void DumpDMDRawThread(); + void DumpDMDRgb565Thread(); + void DumpDMDRgb888Thread(); + bool GetDumpSuffix(const char* romName, char* outSuffix, size_t outSize); + bool GetQueueTimestamp(uint8_t bufferPositionMod, uint32_t& timestampMs) const; void PupDMDThread(); void SerumThread(); + void VniThread(); char m_romName[DMDUTIL_MAX_NAME_SIZE] = {0}; char m_altColorPath[DMDUTIL_MAX_PATH_SIZE] = {0}; @@ -211,6 +249,7 @@ class DMDUTILAPI DMD char m_dumpPath[DMDUTIL_MAX_PATH_SIZE] = {0}; AlphaNumeric* m_pAlphaNumeric; SerumFrameStruct* m_pSerum; + Vni_Context* m_pVni; ZeDMD* m_pZeDMD; PUPDMD::DMD* m_pPUPDMD; std::vector m_levelDMDs; @@ -226,13 +265,20 @@ class DMDUTILAPI DMD std::thread* m_pDmdFrameThread; std::thread* m_pDumpDMDTxtThread; std::thread* m_pDumpDMDRawThread; + std::thread* m_pDumpDMDRgb565Thread; + std::thread* m_pDumpDMDRgb888Thread; std::thread* m_pPupDMDThread; std::thread* m_pSerumThread; + std::thread* m_pVniThread; std::shared_mutex m_dmdSharedMutex; std::condition_variable_any m_dmdCV; std::atomic m_stopFlag; std::atomic m_updateBufferQueuePosition; std::atomic m_pupSceneId; + std::mutex m_dumpSuffixMutex; + char m_dumpSuffixRom[DMDUTIL_MAX_NAME_SIZE] = {0}; + char m_dumpSuffix[9] = {0}; + bool m_dumpSuffixValid = false; bool m_hasUpdateBuffered = false; static bool m_finding; @@ -244,6 +290,18 @@ class DMDUTILAPI DMD PixelcadeDMD* m_pPixelcadeDMD; std::thread* m_pPixelcadeDMDThread; #endif + +#if defined(DMDUTIL_ENABLE_PIN2DMD) && \ + !( \ + (defined(__APPLE__) && \ + ((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_TV) && TARGET_OS_TV))) || \ + defined(__ANDROID__)) + void PIN2DMDThread(); + std::thread* m_pPIN2DMDThread; + bool m_PIN2DMDConnected; + uint16_t m_PIN2DMDWidth; + uint16_t m_PIN2DMDHeight; +#endif }; } // namespace DMDUtil diff --git a/platforms/android/arm64-v8a/external.sh b/platforms/android/arm64-v8a/external.sh index 28e1bb54..2427a872 100755 --- a/platforms/android/arm64-v8a/external.sh +++ b/platforms/android/arm64-v8a/external.sh @@ -8,6 +8,7 @@ echo "Building libraries..." echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" echo " LIBPUPDMD_SHA: ${LIBPUPDMD_SHA}" +echo " LIBVNI_SHA: ${LIBVNI_SHA}" echo "" if [[ $(uname) == "Linux" ]]; then @@ -91,3 +92,23 @@ cmake --build build -- -j${NUM_PROCS} cp src/pupdmd.h ../../third-party/include/ cp build/libpupdmd.so ../../third-party/runtime-libs/android/arm64-v8a/ cd .. + +# +# build libvni and copy to external +# + +curl -sL https://github.com/PPUC/libvni/archive/${LIBVNI_SHA}.tar.gz -o libvni-${LIBVNI_SHA}.tar.gz +tar xzf libvni-${LIBVNI_SHA}.tar.gz +mv libvni-${LIBVNI_SHA} libvni +cd libvni +cmake \ + -DPLATFORM=android \ + -DARCH=arm64-v8a \ + -DBUILD_SHARED=ON \ + -DBUILD_STATIC=OFF \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -B build +cmake --build build -- -j${NUM_PROCS} +cp src/vni.h ../../third-party/include/ +cp build/libvni.so ../../third-party/runtime-libs/android/arm64-v8a/ +cd .. diff --git a/platforms/config.sh b/platforms/config.sh index 60ffba65..4445d4ec 100755 --- a/platforms/config.sh +++ b/platforms/config.sh @@ -2,9 +2,11 @@ set -e -LIBZEDMD_SHA=ec736a2eb938ebccfc1c0c29918f906e920f480a -LIBSERUM_SHA=2da4ca73fb4f30f99b998345ff42cf426180fd57 +LIBZEDMD_SHA=2a1e5da64a8a2b991a57f971e62bd417a9766608 +LIBSERUM_SHA=e0f6937df82a434653aeb5d72ef33e95861519fd LIBPUPDMD_SHA=124f45e5ddd59ceb339591de88fcca72f8c54612 +LIBVNI_SHA=a8020e7a0968d377db7943db48486beb1aa49bbe +LIBUSB_SHA=15a7ebb4d426c5ce196684347d2b7cafad862626 if [ -z "${BUILD_TYPE}" ]; then BUILD_TYPE="Release" diff --git a/platforms/ios-simulator/arm64/external.sh b/platforms/ios-simulator/arm64/external.sh index 7243908f..0a476fe3 100755 --- a/platforms/ios-simulator/arm64/external.sh +++ b/platforms/ios-simulator/arm64/external.sh @@ -8,6 +8,7 @@ echo "Building libraries..." echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" echo " LIBPUPDMD_SHA: ${LIBPUPDMD_SHA}" +echo " LIBVNI_SHA: ${LIBVNI_SHA}" echo "" NUM_PROCS=$(sysctl -n hw.ncpu) @@ -86,3 +87,23 @@ cmake --build build -- -j${NUM_PROCS} cp src/pupdmd.h ../../third-party/include/ cp build/libpupdmd.a ../../third-party/build-libs/ios-simulator/arm64/ cd .. + +# +# build libvni and copy to external +# + +curl -sL https://github.com/PPUC/libvni/archive/${LIBVNI_SHA}.tar.gz -o libvni-${LIBVNI_SHA}.tar.gz +tar xzf libvni-${LIBVNI_SHA}.tar.gz +mv libvni-${LIBVNI_SHA} libvni +cd libvni +cmake \ + -DPLATFORM=ios-simulator \ + -DARCH=arm64 \ + -DBUILD_SHARED=OFF \ + -DBUILD_STATIC=ON \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -B build +cmake --build build -- -j${NUM_PROCS} +cp src/vni.h ../../third-party/include/ +cp build/libvni.a ../../third-party/build-libs/ios-simulator/arm64/ +cd .. diff --git a/platforms/ios/arm64/external.sh b/platforms/ios/arm64/external.sh index 5b85e375..f3464ed6 100755 --- a/platforms/ios/arm64/external.sh +++ b/platforms/ios/arm64/external.sh @@ -8,6 +8,7 @@ echo "Building libraries..." echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" echo " LIBPUPDMD_SHA: ${LIBPUPDMD_SHA}" +echo " LIBVNI_SHA: ${LIBVNI_SHA}" echo "" NUM_PROCS=$(sysctl -n hw.ncpu) @@ -86,3 +87,23 @@ cmake --build build -- -j${NUM_PROCS} cp src/pupdmd.h ../../third-party/include/ cp build/libpupdmd.a ../../third-party/build-libs/ios/arm64/ cd .. + +# +# build libvni and copy to external +# + +curl -sL https://github.com/PPUC/libvni/archive/${LIBVNI_SHA}.tar.gz -o libvni-${LIBVNI_SHA}.tar.gz +tar xzf libvni-${LIBVNI_SHA}.tar.gz +mv libvni-${LIBVNI_SHA} libvni +cd libvni +cmake \ + -DPLATFORM=ios \ + -DARCH=arm64 \ + -DBUILD_SHARED=OFF \ + -DBUILD_STATIC=ON \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -B build +cmake --build build -- -j${NUM_PROCS} +cp src/vni.h ../../third-party/include/ +cp build/libvni.a ../../third-party/build-libs/ios/arm64/ +cd .. diff --git a/platforms/linux/aarch64/external.sh b/platforms/linux/aarch64/external.sh index 8f0154a5..abe24601 100755 --- a/platforms/linux/aarch64/external.sh +++ b/platforms/linux/aarch64/external.sh @@ -5,9 +5,11 @@ set -e source ./platforms/config.sh echo "Building libraries..." +echo " LIBUSB_SHA: ${LIBUSB_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" echo " LIBPUPDMD_SHA: ${LIBPUPDMD_SHA}" +echo " LIBVNI_SHA: ${LIBVNI_SHA}" echo "" NUM_PROCS=$(nproc) @@ -16,6 +18,23 @@ rm -rf external mkdir external cd external +# +# build libusb and copy to third-party +# + +curl -sL https://github.com/libusb/libusb/archive/${LIBUSB_SHA}.tar.gz -o libusb-${LIBUSB_SHA}.tar.gz +tar xzf libusb-${LIBUSB_SHA}.tar.gz +mv libusb-${LIBUSB_SHA} libusb +cd libusb +./autogen.sh +./configure \ + --enable-shared +make -j${NUM_PROCS} +mkdir -p ../../third-party/include/libusb-1.0 +cp libusb/libusb.h ../../third-party/include/libusb-1.0 +cp -a libusb/.libs/libusb-1.0.{so,so.*} ../../third-party/runtime-libs/linux/aarch64/ +cd .. + # # build libzedmd and copy to external # @@ -91,3 +110,23 @@ cmake --build build -- -j${NUM_PROCS} cp src/pupdmd.h ../../third-party/include/ cp -a build/libpupdmd.{so,so.*} ../../third-party/runtime-libs/linux/aarch64/ cd .. + +# +# build libvni and copy to external +# + +curl -sL https://github.com/PPUC/libvni/archive/${LIBVNI_SHA}.tar.gz -o libvni-${LIBVNI_SHA}.tar.gz +tar xzf libvni-${LIBVNI_SHA}.tar.gz +mv libvni-${LIBVNI_SHA} libvni +cd libvni +cmake \ + -DPLATFORM=linux \ + -DARCH=aarch64 \ + -DBUILD_SHARED=ON \ + -DBUILD_STATIC=OFF \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -B build +cmake --build build -- -j${NUM_PROCS} +cp src/vni.h ../../third-party/include/ +cp -a build/libvni.{so,so.*} ../../third-party/runtime-libs/linux/aarch64/ +cd .. diff --git a/platforms/linux/x64/external.sh b/platforms/linux/x64/external.sh index 9eabe854..a67f31de 100755 --- a/platforms/linux/x64/external.sh +++ b/platforms/linux/x64/external.sh @@ -5,9 +5,11 @@ set -e source ./platforms/config.sh echo "Building libraries..." +echo " LIBUSB_SHA: ${LIBUSB_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" echo " LIBPUPDMD_SHA: ${LIBPUPDMD_SHA}" +echo " LIBVNI_SHA: ${LIBVNI_SHA}" echo "" NUM_PROCS=$(nproc) @@ -16,6 +18,23 @@ rm -rf external mkdir external cd external +# +# build libusb and copy to third-party +# + +curl -sL https://github.com/libusb/libusb/archive/${LIBUSB_SHA}.tar.gz -o libusb-${LIBUSB_SHA}.tar.gz +tar xzf libusb-${LIBUSB_SHA}.tar.gz +mv libusb-${LIBUSB_SHA} libusb +cd libusb +./autogen.sh +./configure \ + --enable-shared +make -j${NUM_PROCS} +mkdir -p ../../third-party/include/libusb-1.0 +cp libusb/libusb.h ../../third-party/include/libusb-1.0 +cp -a libusb/.libs/libusb-1.0.{so,so.*} ../../third-party/runtime-libs/linux/x64/ +cd .. + # # build libzedmd and copy to external # @@ -90,3 +109,23 @@ cmake --build build -- -j${NUM_PROCS} cp src/pupdmd.h ../../third-party/include/ cp -a build/libpupdmd.{so,so.*} ../../third-party/runtime-libs/linux/x64/ cd .. + +# +# build libvni and copy to external +# + +curl -sL https://github.com/PPUC/libvni/archive/${LIBVNI_SHA}.tar.gz -o libvni-${LIBVNI_SHA}.tar.gz +tar xzf libvni-${LIBVNI_SHA}.tar.gz +mv libvni-${LIBVNI_SHA} libvni +cd libvni +cmake \ + -DPLATFORM=linux \ + -DARCH=x64 \ + -DBUILD_SHARED=ON \ + -DBUILD_STATIC=OFF \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -B build +cmake --build build -- -j${NUM_PROCS} +cp src/vni.h ../../third-party/include/ +cp -a build/libvni.{so,so.*} ../../third-party/runtime-libs/linux/x64/ +cd .. diff --git a/platforms/macos/arm64/external.sh b/platforms/macos/arm64/external.sh index daca5017..ab42978c 100755 --- a/platforms/macos/arm64/external.sh +++ b/platforms/macos/arm64/external.sh @@ -5,9 +5,11 @@ set -e source ./platforms/config.sh echo "Building libraries..." +echo " LIBUSB_SHA: ${LIBUSB_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" echo " LIBPUPDMD_SHA: ${LIBPUPDMD_SHA}" +echo " LIBVNI_SHA: ${LIBVNI_SHA}" echo "" NUM_PROCS=$(sysctl -n hw.ncpu) @@ -16,6 +18,25 @@ rm -rf external mkdir external cd external +# +# build libusb and copy to third-party +# + +curl -sL https://github.com/libusb/libusb/archive/${LIBUSB_SHA}.tar.gz -o libusb-${LIBUSB_SHA}.tar.gz +tar xzf libusb-${LIBUSB_SHA}.tar.gz +mv libusb-${LIBUSB_SHA} libusb +cd libusb +./autogen.sh +./configure \ + --host=arm-apple-darwin \ + CFLAGS="-arch arm64" \ + LDFLAGS="-Wl,-install_name,@rpath/libusb-1.0.dylib" +make -j${NUM_PROCS} +mkdir -p ../../third-party/include/libusb-1.0 +cp libusb/libusb.h ../../third-party/include/libusb-1.0 +cp -a libusb/.libs/libusb*.dylib ../../third-party/runtime-libs/macos/arm64/ +cd .. + # # build libzedmd and copy to external # @@ -30,6 +51,8 @@ cmake \ -DARCH=arm64 \ -DBUILD_SHARED=ON \ -DBUILD_STATIC=OFF \ + -DCMAKE_C_VISIBILITY_PRESET=default \ + -DCMAKE_CXX_VISIBILITY_PRESET=default \ -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ -B build cmake --build build -- -j${NUM_PROCS} @@ -90,3 +113,23 @@ cmake --build build -- -j${NUM_PROCS} cp src/pupdmd.h ../../third-party/include/ cp -a build/libpupdmd.{dylib,*.dylib} ../../third-party/runtime-libs/macos/arm64/ cd .. + +# +# build libvni and copy to external +# + +curl -sL https://github.com/PPUC/libvni/archive/${LIBVNI_SHA}.tar.gz -o libvni-${LIBVNI_SHA}.tar.gz +tar xzf libvni-${LIBVNI_SHA}.tar.gz +mv libvni-${LIBVNI_SHA} libvni +cd libvni +cmake \ + -DPLATFORM=macos \ + -DARCH=arm64 \ + -DBUILD_SHARED=ON \ + -DBUILD_STATIC=OFF \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -B build +cmake --build build -- -j${NUM_PROCS} +cp src/vni.h ../../third-party/include/ +cp -a build/libvni.{dylib,*.dylib} ../../third-party/runtime-libs/macos/arm64/ +cd .. diff --git a/platforms/macos/x64/external.sh b/platforms/macos/x64/external.sh index 83eb9402..1de98af6 100755 --- a/platforms/macos/x64/external.sh +++ b/platforms/macos/x64/external.sh @@ -5,9 +5,11 @@ set -e source ./platforms/config.sh echo "Building libraries..." +echo " LIBUSB_SHA: ${LIBUSB_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" echo " LIBPUPDMD_SHA: ${LIBPUPDMD_SHA}" +echo " LIBVNI_SHA: ${LIBVNI_SHA}" echo "" NUM_PROCS=$(sysctl -n hw.ncpu) @@ -16,6 +18,25 @@ rm -rf external mkdir external cd external +# +# build libusb and copy to third-party +# + +curl -sL https://github.com/libusb/libusb/archive/${LIBUSB_SHA}.tar.gz -o libusb-${LIBUSB_SHA}.tar.gz +tar xzf libusb-${LIBUSB_SHA}.tar.gz +mv libusb-${LIBUSB_SHA} libusb +cd libusb +./autogen.sh +./configure \ + --host=x86_64-apple-darwin \ + CFLAGS="-arch x86_64" \ + LDFLAGS="-Wl,-install_name,@rpath/libusb-1.0.dylib" +make -j${NUM_PROCS} +mkdir -p ../../third-party/include/libusb-1.0 +cp libusb/libusb.h ../../third-party/include/libusb-1.0 +cp -a libusb/.libs/libusb*.dylib ../../third-party/runtime-libs/macos/x64/ +cd .. + # # build libzedmd and copy to external # @@ -90,3 +111,23 @@ cmake --build build -- -j${NUM_PROCS} cp src/pupdmd.h ../../third-party/include/ cp -a build/libpupdmd.{dylib,*.dylib} ../../third-party/runtime-libs/macos/x64/ cd .. + +# +# build libvni and copy to external +# + +curl -sL https://github.com/PPUC/libvni/archive/${LIBVNI_SHA}.tar.gz -o libvni-${LIBVNI_SHA}.tar.gz +tar xzf libvni-${LIBVNI_SHA}.tar.gz +mv libvni-${LIBVNI_SHA} libvni +cd libvni +cmake \ + -DPLATFORM=macos \ + -DARCH=x64 \ + -DBUILD_SHARED=ON \ + -DBUILD_STATIC=OFF \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -B build +cmake --build build -- -j${NUM_PROCS} +cp src/vni.h ../../third-party/include/ +cp -a build/libvni.{dylib,*.dylib} ../../third-party/runtime-libs/macos/x64/ +cd .. diff --git a/platforms/tvos/arm64/external.sh b/platforms/tvos/arm64/external.sh index e42adee9..4f127631 100755 --- a/platforms/tvos/arm64/external.sh +++ b/platforms/tvos/arm64/external.sh @@ -8,6 +8,7 @@ echo "Building libraries..." echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" echo " LIBPUPDMD_SHA: ${LIBPUPDMD_SHA}" +echo " LIBVNI_SHA: ${LIBVNI_SHA}" echo "" NUM_PROCS=$(sysctl -n hw.ncpu) @@ -86,3 +87,23 @@ cmake --build build -- -j${NUM_PROCS} cp src/pupdmd.h ../../third-party/include/ cp build/libpupdmd.a ../../third-party/build-libs/tvos/arm64/ cd .. + +# +# build libvni and copy to external +# + +curl -sL https://github.com/PPUC/libvni/archive/${LIBVNI_SHA}.tar.gz -o libvni-${LIBVNI_SHA}.tar.gz +tar xzf libvni-${LIBVNI_SHA}.tar.gz +mv libvni-${LIBVNI_SHA} libvni +cd libvni +cmake \ + -DPLATFORM=tvos \ + -DARCH=arm64 \ + -DBUILD_SHARED=OFF \ + -DBUILD_STATIC=ON \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -B build +cmake --build build -- -j${NUM_PROCS} +cp src/vni.h ../../third-party/include/ +cp build/libvni.a ../../third-party/build-libs/tvos/arm64/ +cd .. diff --git a/platforms/win/x64/external.sh b/platforms/win/x64/external.sh index bc8ac5ea..ca110b5b 100755 --- a/platforms/win/x64/external.sh +++ b/platforms/win/x64/external.sh @@ -5,15 +5,39 @@ set -e source ./platforms/config.sh echo "Building libraries..." +echo " LIBUSB_SHA: ${LIBUSB_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" echo " LIBPUPDMD_SHA: ${LIBPUPDMD_SHA}" +echo " LIBVNI_SHA: ${LIBVNI_SHA}" echo "" rm -rf external mkdir external cd external +# +# build libusb and copy to third-party +# + +curl -sL https://github.com/libusb/libusb/archive/${LIBUSB_SHA}.tar.gz -o libusb-${LIBUSB_SHA}.tar.gz +tar xzf libusb-${LIBUSB_SHA}.tar.gz +mv libusb-${LIBUSB_SHA} libusb +cd libusb +sed -i.bak 's/LIBRARY.*libusb-1.0/LIBRARY libusb64-1.0/' libusb/libusb-1.0.def +# remove patch after this is fixed: https://github.com/libusb/libusb/issues/1649#issuecomment-2940138443 +cp ../../platforms/win/x64/libusb/libusb_dll.vcxproj msvc +msbuild.exe msvc/libusb_dll.vcxproj \ + -p:TargetName=libusb64-1.0 \ + -p:Configuration=${BUILD_TYPE} \ + -p:Platform=x64 \ + -p:PlatformToolset=v143 +mkdir -p ../../third-party/include/libusb-1.0 +cp libusb/libusb.h ../../third-party/include/libusb-1.0 +cp build/v143/x64/${BUILD_TYPE}/libusb_dll/../dll/libusb64-1.0.lib ../../third-party/build-libs/win/x64 +cp build/v143/x64/${BUILD_TYPE}/libusb_dll/../dll/libusb64-1.0.dll ../../third-party/runtime-libs/win/x64 +cd .. + # # build libzedmd and copy to external # @@ -94,3 +118,24 @@ cp src/pupdmd.h ../../third-party/include/ cp build/${BUILD_TYPE}/pupdmd64.lib ../../third-party/build-libs/win/x64/ cp build/${BUILD_TYPE}/pupdmd64.dll ../../third-party/runtime-libs/win/x64/ cd .. + +# +# build libvni and copy to external +# + +curl -sL https://github.com/PPUC/libvni/archive/${LIBVNI_SHA}.tar.gz -o libvni-${LIBVNI_SHA}.tar.gz +tar xzf libvni-${LIBVNI_SHA}.tar.gz +mv libvni-${LIBVNI_SHA} libvni +cd libvni +cmake \ + -G "Visual Studio 17 2022" \ + -DPLATFORM=win \ + -DARCH=x64 \ + -DBUILD_SHARED=ON \ + -DBUILD_STATIC=OFF \ + -B build +cmake --build build --config ${BUILD_TYPE} +cp src/vni.h ../../third-party/include/ +cp build/${BUILD_TYPE}/vni64.lib ../../third-party/build-libs/win/x64/ +cp build/${BUILD_TYPE}/vni64.dll ../../third-party/runtime-libs/win/x64/ +cd .. diff --git a/platforms/win/x64/libusb/libusb_dll.vcxproj b/platforms/win/x64/libusb/libusb_dll.vcxproj new file mode 100644 index 00000000..54804c80 --- /dev/null +++ b/platforms/win/x64/libusb/libusb_dll.vcxproj @@ -0,0 +1,66 @@ + + + + + {349EE8FA-7D25-4909-AAF5-FF3FADE72187} + + + + + + + + + + + + + $(IntDir)..\dll\ + libusb-1.0 + + + + libusb-1.0.rc;%(EmbedManagedResourceFile) + ..\libusb\libusb-1.0.def + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + diff --git a/platforms/win/x86/external.sh b/platforms/win/x86/external.sh index ff8c91cb..ef7fccba 100755 --- a/platforms/win/x86/external.sh +++ b/platforms/win/x86/external.sh @@ -5,15 +5,37 @@ set -e source ./platforms/config.sh echo "Building libraries..." +echo " LIBUSB_SHA: ${LIBUSB_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" echo " LIBPUPDMD_SHA: ${LIBPUPDMD_SHA}" +echo " LIBVNI_SHA: ${LIBVNI_SHA}" echo "" rm -rf external mkdir external cd external +# +# build libusb and copy to third-party +# + +curl -sL https://github.com/libusb/libusb/archive/${LIBUSB_SHA}.tar.gz -o libusb-${LIBUSB_SHA}.tar.gz +tar xzf libusb-${LIBUSB_SHA}.tar.gz +mv libusb-${LIBUSB_SHA} libusb +cd libusb +# remove patch after this is fixed: https://github.com/libusb/libusb/issues/1649#issuecomment-2940138443 +cp ../../platforms/win/x86/libusb/libusb_dll.vcxproj msvc +msbuild.exe msvc/libusb_dll.vcxproj \ + -p:Configuration=${BUILD_TYPE} \ + -p:Platform=Win32 \ + -p:PlatformToolset=v143 +mkdir -p ../../third-party/include/libusb-1.0 +cp libusb/libusb.h ../../third-party/include/libusb-1.0 +cp build/v143/Win32/${BUILD_TYPE}/libusb_dll/../dll/libusb-1.0.lib ../../third-party/build-libs/win/x86 +cp build/v143/Win32/${BUILD_TYPE}/libusb_dll/../dll/libusb-1.0.dll ../../third-party/runtime-libs/win/x86 +cd .. + # # build libzedmd and copy to external # @@ -97,3 +119,25 @@ cp src/pupdmd.h ../../third-party/include/ cp build/${BUILD_TYPE}/pupdmd.lib ../../third-party/build-libs/win/x86/ cp build/${BUILD_TYPE}/pupdmd.dll ../../third-party/runtime-libs/win/x86/ cd .. + +# +# build libvni and copy to external +# + +curl -sL https://github.com/PPUC/libvni/archive/${LIBVNI_SHA}.tar.gz -o libvni-${LIBVNI_SHA}.tar.gz +tar xzf libvni-${LIBVNI_SHA}.tar.gz +mv libvni-${LIBVNI_SHA} libvni +cd libvni +cmake \ + -G "Visual Studio 17 2022" \ + -A Win32 \ + -DPLATFORM=win \ + -DARCH=x86 \ + -DBUILD_SHARED=ON \ + -DBUILD_STATIC=OFF \ + -B build +cmake --build build --config ${BUILD_TYPE} +cp src/vni.h ../../third-party/include/ +cp build/${BUILD_TYPE}/vni.lib ../../third-party/build-libs/win/x86/ +cp build/${BUILD_TYPE}/vni.dll ../../third-party/runtime-libs/win/x86/ +cd .. diff --git a/platforms/win/x86/libusb/libusb_dll.vcxproj b/platforms/win/x86/libusb/libusb_dll.vcxproj new file mode 100644 index 00000000..54804c80 --- /dev/null +++ b/platforms/win/x86/libusb/libusb_dll.vcxproj @@ -0,0 +1,66 @@ + + + + + {349EE8FA-7D25-4909-AAF5-FF3FADE72187} + + + + + + + + + + + + + $(IntDir)..\dll\ + libusb-1.0 + + + + libusb-1.0.rc;%(EmbedManagedResourceFile) + ..\libusb\libusb-1.0.def + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + diff --git a/src/Config.cpp b/src/Config.cpp index 19c68b8a..c8dd188e 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -20,6 +20,7 @@ Config::Config() m_altColorPath.clear(); m_pupCapture = false; m_serumPupTriggers = false; + m_vniKey.clear(); m_pupVideosPath.clear(); m_pupExactColorMatch = true; m_framesTimeout = 0; @@ -34,6 +35,7 @@ Config::Config() m_zedmdBrightness = -1; m_pixelcade = true; m_pixelcadeDevice.clear(); + m_PIN2DMD = true; m_dmdServer = false; m_dmdServerAddr = "localhost"; m_dmdServerPort = 6789; @@ -240,6 +242,16 @@ void Config::parseConfigFile(const char* path) SetPixelcadeDevice(""); } + // PIN2DMD + try + { + SetPIN2DMD(r.Get("PIN2DMD", "Enabled", true)); + } + catch (const std::exception&) + { + SetPIN2DMD(true); + } + // Serum try { @@ -268,6 +280,16 @@ void Config::parseConfigFile(const char* path) SetShowNotColorizedFrames(false); } + // VNI + try + { + SetVniKey(r.Get("VNI", "Key", "").c_str()); + } + catch (const std::exception&) + { + SetVniKey(""); + } + // Dump try { diff --git a/src/DMD.cpp b/src/DMD.cpp index 5e7311f2..4f3f43fe 100644 --- a/src/DMD.cpp +++ b/src/DMD.cpp @@ -17,9 +17,19 @@ #include "PixelcadeDMD.h" #endif +#if defined(DMDUTIL_ENABLE_PIN2DMD) && \ + !( \ + (defined(__APPLE__) && \ + ((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_TV) && TARGET_OS_TV))) || \ + defined(__ANDROID__)) +#include "PIN2DMD.h" +#endif + #include #include +#include #include +#include #include #include "AlphaNumeric.h" @@ -31,8 +41,79 @@ #include "pupdmd.h" #include "serum-decode.h" #include "serum.h" +#include "vni.h" #include "sockpp/tcp_connector.h" +namespace +{ +std::string ToLower(const std::string& value) +{ + std::string out; + out.reserve(value.size()); + for (unsigned char ch : value) + { + out.push_back(static_cast(std::tolower(ch))); + } + return out; +} + +bool FindCaseInsensitiveFile(const std::string& dir, const std::string& filename, std::string* outPath) +{ + namespace fs = std::filesystem; + std::error_code ec; + if (!fs::exists(dir, ec) || !fs::is_directory(dir, ec)) + { + return false; + } + + const std::string target = ToLower(filename); + for (const auto& entry : fs::directory_iterator(dir, ec)) + { + if (ec) + { + break; + } + if (!entry.is_regular_file(ec)) + { + continue; + } + + const std::string name = entry.path().filename().string(); + if (ToLower(name) == target) + { + *outPath = entry.path().string(); + return true; + } + } + + return false; +} + +std::string BuildAltColorDir(const char* altColorPath, const char* romName) +{ + std::string path = altColorPath ? altColorPath : ""; + if (!path.empty() && path.back() != '/' && path.back() != '\\') + { + path += '/'; + } + if (romName && romName[0] != '\0') + { + path += romName; + path += '/'; + } + return path; +} + +size_t PaletteBytesForDepth(uint8_t depth) +{ + if (depth > 8) + { + return 0; + } + return (static_cast(1u) << depth) * 3u; +} +} // namespace + namespace DMDUtil { @@ -147,6 +228,8 @@ DMD::DMD() for (uint8_t i = 0; i < DMDUTIL_FRAME_BUFFER_SIZE; i++) { m_pUpdateBufferQueue[i] = new Update(); + m_updateBufferQueueTimestamp[i] = 0; + m_updateBufferQueueHasTimestamp[i] = false; } m_updateBufferQueuePosition.store(0, std::memory_order_release); m_stopFlag.store(false, std::memory_order_release); @@ -154,6 +237,7 @@ DMD::DMD() m_pAlphaNumeric = new AlphaNumeric(); m_pSerum = nullptr; + m_pVni = nullptr; m_pZeDMD = nullptr; m_pPUPDMD = nullptr; @@ -163,6 +247,8 @@ DMD::DMD() m_pConsoleDMDThread = nullptr; m_pDumpDMDTxtThread = nullptr; m_pDumpDMDRawThread = nullptr; + m_pDumpDMDRgb565Thread = nullptr; + m_pDumpDMDRgb888Thread = nullptr; #if !( \ (defined(__APPLE__) && ((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_TV) && TARGET_OS_TV))) || \ defined(__ANDROID__)) @@ -170,9 +256,21 @@ DMD::DMD() m_pPixelcadeDMDThread = nullptr; #endif +#if defined(DMDUTIL_ENABLE_PIN2DMD) && \ + !( \ + (defined(__APPLE__) && \ + ((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_TV) && TARGET_OS_TV))) || \ + defined(__ANDROID__)) + m_pPIN2DMDThread = nullptr; + m_PIN2DMDConnected = false; + m_PIN2DMDWidth = 0; + m_PIN2DMDHeight = 0; +#endif + m_pDmdFrameThread = new std::thread(&DMD::DmdFrameThread, this); m_pPupDMDThread = new std::thread(&DMD::PupDMDThread, this); m_pSerumThread = new std::thread(&DMD::SerumThread, this); + m_pVniThread = new std::thread(&DMD::VniThread, this); m_pDMDServerConnector = nullptr; } @@ -229,6 +327,20 @@ DMD::~DMD() m_pDumpDMDRawThread = nullptr; } + if (m_pDumpDMDRgb565Thread) + { + m_pDumpDMDRgb565Thread->join(); + delete m_pDumpDMDRgb565Thread; + m_pDumpDMDRgb565Thread = nullptr; + } + + if (m_pDumpDMDRgb888Thread) + { + m_pDumpDMDRgb888Thread->join(); + delete m_pDumpDMDRgb888Thread; + m_pDumpDMDRgb888Thread = nullptr; + } + if (m_pPupDMDThread) { m_pPupDMDThread->join(); @@ -243,6 +355,13 @@ DMD::~DMD() m_pSerumThread = nullptr; } + if (m_pVniThread) + { + m_pVniThread->join(); + delete m_pVniThread; + m_pVniThread = nullptr; + } + #if !( \ (defined(__APPLE__) && ((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_TV) && TARGET_OS_TV))) || \ defined(__ANDROID__)) @@ -253,6 +372,19 @@ DMD::~DMD() m_pPixelcadeDMDThread = nullptr; } #endif + +#if defined(DMDUTIL_ENABLE_PIN2DMD) && \ + !( \ + (defined(__APPLE__) && \ + ((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_TV) && TARGET_OS_TV))) || \ + defined(__ANDROID__)) + if (m_pPIN2DMDThread) + { + m_pPIN2DMDThread->join(); + delete m_pPIN2DMDThread; + m_pPIN2DMDThread = nullptr; + } +#endif delete m_pAlphaNumeric; delete m_pZeDMD; delete m_pPUPDMD; @@ -309,7 +441,15 @@ bool DMD::HasDisplay() const #if !( \ (defined(__APPLE__) && ((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_TV) && TARGET_OS_TV))) || \ defined(__ANDROID__)) - return (m_pPixelcadeDMD != nullptr); + if (m_pPixelcadeDMD != nullptr) return true; +#endif + +#if defined(DMDUTIL_ENABLE_PIN2DMD) && \ + !( \ + (defined(__APPLE__) && \ + ((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_TV) && TARGET_OS_TV))) || \ + defined(__ANDROID__)) + if (m_PIN2DMDConnected) return true; #endif return false; @@ -327,6 +467,14 @@ bool DMD::HasHDDisplay() const } } +#if defined(DMDUTIL_ENABLE_PIN2DMD) && \ + !( \ + (defined(__APPLE__) && \ + ((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_TV) && TARGET_OS_TV))) || \ + defined(__ANDROID__)) + if (m_PIN2DMDConnected && m_PIN2DMDWidth == 256) return true; +#endif + return false; } @@ -352,6 +500,22 @@ void DMD::DumpDMDRaw() } } +void DMD::DumpDMDRgb565() +{ + if (!m_pDumpDMDRgb565Thread) + { + m_pDumpDMDRgb565Thread = new std::thread(&DMD::DumpDMDRgb565Thread, this); + } +} + +void DMD::DumpDMDRgb888() +{ + if (!m_pDumpDMDRgb888Thread) + { + m_pDumpDMDRgb888Thread = new std::thread(&DMD::DumpDMDRgb888Thread, this); + } +} + LevelDMD* DMD::CreateLevelDMD(uint16_t width, uint16_t height, bool sam) { LevelDMD* const pLevelDMD = new LevelDMD(width, height, sam); @@ -466,15 +630,43 @@ void DMD::UpdateData(const uint8_t* pData, int depth, uint16_t width, uint16_t h QueueUpdate(dmdUpdate, buffered); } -void DMD::QueueUpdate(const std::shared_ptr dmdUpdate, bool buffered) +void DMD::UpdateDataWithTimestampInternal(const uint8_t* pData, int depth, uint16_t width, uint16_t height, uint8_t r, + uint8_t g, uint8_t b, Mode mode, uint32_t timestampMs, bool buffered) +{ + auto dmdUpdate = std::make_shared(); + if (pData) + { + memcpy(dmdUpdate->data, pData, (size_t)width * height * (mode == Mode::RGB16 ? 2 : (mode == Mode::RGB24 ? 3 : 1))); + dmdUpdate->hasData = true; + } + else + { + dmdUpdate->hasData = false; + } + dmdUpdate->mode = mode; + dmdUpdate->depth = depth; + dmdUpdate->width = width; + dmdUpdate->height = height; + dmdUpdate->hasSegData = false; + dmdUpdate->hasSegData2 = false; + dmdUpdate->r = r; + dmdUpdate->g = g; + dmdUpdate->b = b; + + QueueUpdate(dmdUpdate, buffered, true, timestampMs); +} + +void DMD::QueueUpdate(const std::shared_ptr dmdUpdate, bool buffered, bool hasTimestamp, uint32_t timestampMs) { std::thread( - [this, dmdUpdate, buffered]() + [this, dmdUpdate, buffered, hasTimestamp, timestampMs]() { std::unique_lock ul(m_dmdSharedMutex); uint16_t updateBufferQueuePosition = m_updateBufferQueuePosition.load(std::memory_order_acquire); - memcpy(m_pUpdateBufferQueue[(++updateBufferQueuePosition) % DMDUTIL_FRAME_BUFFER_SIZE], dmdUpdate.get(), - sizeof(Update)); + uint8_t slot = (++updateBufferQueuePosition) % DMDUTIL_FRAME_BUFFER_SIZE; + memcpy(m_pUpdateBufferQueue[slot], dmdUpdate.get(), sizeof(Update)); + m_updateBufferQueueHasTimestamp[slot] = hasTimestamp; + m_updateBufferQueueTimestamp[slot] = timestampMs; m_updateBufferQueuePosition.store(updateBufferQueuePosition, std::memory_order_release); Log(DMDUtil_LogLevel_DEBUG, "Queued Frame: position=%d, mode=%d, depth=%d", updateBufferQueuePosition, @@ -527,6 +719,12 @@ void DMD::UpdateData(const uint8_t* pData, int depth, uint16_t width, uint16_t h UpdateData(pData, depth, width, height, r, g, b, Mode::Data, buffered); } +void DMD::UpdateDataWithTimestamp(const uint8_t* pData, int depth, uint16_t width, uint16_t height, uint8_t r, uint8_t g, + uint8_t b, uint32_t timestampMs, bool buffered) +{ + UpdateDataWithTimestampInternal(pData, depth, width, height, r, g, b, Mode::Data, timestampMs, buffered); +} + void DMD::UpdateRGB24Data(const uint8_t* pData, int depth, uint16_t width, uint16_t height, uint8_t r, uint8_t g, uint8_t b, bool buffered) { @@ -538,6 +736,18 @@ void DMD::UpdateRGB24Data(const uint8_t* pData, uint16_t width, uint16_t height, UpdateData(pData, 24, width, height, 0, 0, 0, Mode::RGB24, buffered); } +void DMD::UpdateRGB24DataWithTimestamp(const uint8_t* pData, int depth, uint16_t width, uint16_t height, uint8_t r, + uint8_t g, uint8_t b, uint32_t timestampMs, bool buffered) +{ + UpdateDataWithTimestampInternal(pData, depth, width, height, r, g, b, Mode::RGB24, timestampMs, buffered); +} + +void DMD::UpdateRGB24DataWithTimestamp(const uint8_t* pData, uint16_t width, uint16_t height, uint32_t timestampMs, + bool buffered) +{ + UpdateDataWithTimestampInternal(pData, 24, width, height, 0, 0, 0, Mode::RGB24, timestampMs, buffered); +} + void DMD::UpdateRGB16Data(const uint16_t* pData, uint16_t width, uint16_t height, bool buffered) { auto dmdUpdate = std::make_shared(); @@ -560,6 +770,29 @@ void DMD::UpdateRGB16Data(const uint16_t* pData, uint16_t width, uint16_t height QueueUpdate(dmdUpdate, buffered); } +void DMD::UpdateRGB16DataWithTimestamp(const uint16_t* pData, uint16_t width, uint16_t height, uint32_t timestampMs, + bool buffered) +{ + auto dmdUpdate = std::make_shared(); + dmdUpdate->mode = Mode::RGB16; + dmdUpdate->depth = 24; + dmdUpdate->width = width; + dmdUpdate->height = height; + if (pData) + { + memcpy(dmdUpdate->segData, pData, (size_t)width * height * sizeof(uint16_t)); + dmdUpdate->hasData = true; + } + else + { + dmdUpdate->hasData = false; + } + dmdUpdate->hasSegData = false; + dmdUpdate->hasSegData2 = false; + + QueueUpdate(dmdUpdate, buffered, true, timestampMs); +} + void DMD::UpdateAlphaNumericData(AlphaNumericLayout layout, const uint16_t* pData1, const uint16_t* pData2, uint8_t r, uint8_t g, uint8_t b) { @@ -705,6 +938,31 @@ void DMD::FindDisplays() m_pPixelcadeDMD = pPixelcadeDMD; #endif +#if defined(DMDUTIL_ENABLE_PIN2DMD) && \ + !( \ + (defined(__APPLE__) && \ + ((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_TV) && TARGET_OS_TV))) || \ + defined(__ANDROID__)) + m_PIN2DMDConnected = false; + m_PIN2DMDWidth = 0; + m_PIN2DMDHeight = 0; + + if (pConfig->IsPIN2DMD()) + { + int PIN2DMDInitResult = PIN2DMDInit(); + if (PIN2DMDInitResult > 0) + { + m_PIN2DMDWidth = PIN2DMDGetWidth(); + m_PIN2DMDHeight = PIN2DMDGetHeight(); + if (m_PIN2DMDWidth > 0 && m_PIN2DMDHeight > 0) + { + m_PIN2DMDConnected = true; + m_pPIN2DMDThread = new std::thread(&DMD::PIN2DMDThread, this); + } + } + } +#endif + m_finding = false; }) .detach(); @@ -785,7 +1043,7 @@ void DMD::ZeDMDThread() uint16_t frameSize = 0; uint16_t segData1[128] = {0}; uint16_t segData2[128] = {0}; - uint8_t palette[PALETTE_SIZE] = {0}; + uint8_t palette[256 * 3] = {0}; uint8_t indexBuffer[256 * 64] = {0}; uint8_t renderBuffer[256 * 64 * 3] = {0}; @@ -827,7 +1085,7 @@ void DMD::ZeDMDThread() bufferPosition = nextBufferPosition; uint8_t bufferPositionMod = bufferPosition % DMDUTIL_FRAME_BUFFER_SIZE; - if (m_pSerum && + if ((m_pSerum || m_pVni) && (!IsSerumMode(m_pUpdateBufferQueue[bufferPositionMod]->mode, showNotColorizedFrames) || (m_pZeDMD->GetWidth() == 256 && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::SerumV2_32_64) || (m_pZeDMD->GetWidth() < 256 && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::SerumV2_64_32))) @@ -876,13 +1134,18 @@ void DMD::ZeDMDThread() } else { - if (m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::SerumV1) + if (m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::SerumV1 || + m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::Vni) { - memcpy(palette, m_pUpdateBufferQueue[bufferPositionMod]->segData, PALETTE_SIZE); + size_t paletteBytes = PaletteBytesForDepth((uint8_t)m_pUpdateBufferQueue[bufferPositionMod]->depth); + if (paletteBytes > 0 && paletteBytes <= sizeof(palette)) + { + memcpy(palette, m_pUpdateBufferQueue[bufferPositionMod]->segData, paletteBytes); + } memcpy(indexBuffer, m_pUpdateBufferQueue[bufferPositionMod]->data, frameSize); update = true; } - else if ((!m_pSerum && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::Data) || + else if ((!(m_pSerum || m_pVni) && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::Data) || (showNotColorizedFrames && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::NotColorized)) { memcpy(indexBuffer, m_pUpdateBufferQueue[bufferPositionMod]->data, frameSize); @@ -961,6 +1224,8 @@ void DMD::SerumThread() if (m_pSerum) { Serum_Dispose(); + m_serumHasTimestamp = false; + m_serumLastTimestampMs = 0; } return; @@ -1005,6 +1270,8 @@ void DMD::SerumThread() // DMDServer accepted a different connection, turn off Serum Colorization. Serum_Dispose(); m_pSerum = nullptr; + m_serumHasTimestamp = false; + m_serumLastTimestampMs = 0; lastDmdUpdate = nullptr; strcpy(name, ""); QueueBuffer(); @@ -1021,6 +1288,8 @@ void DMD::SerumThread() { Serum_Dispose(); m_pSerum = nullptr; + m_serumHasTimestamp = false; + m_serumLastTimestampMs = 0; lastDmdUpdate = nullptr; } @@ -1065,6 +1334,20 @@ void DMD::SerumThread() if (m_pPixelcadeDMD) flags |= FLAG_REQUEST_32P_FRAMES; #endif +#if defined(DMDUTIL_ENABLE_PIN2DMD) && \ + !( \ + (defined(__APPLE__) && \ + ((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_TV) && TARGET_OS_TV))) || \ + defined(__ANDROID__)) + if (m_PIN2DMDConnected) + { + if (m_PIN2DMDHeight == 64) + flags |= FLAG_REQUEST_64P_FRAMES; + else + flags |= FLAG_REQUEST_32P_FRAMES; + } +#endif + if (!flags) flags |= FLAG_REQUEST_32P_FRAMES; m_pSerum = (name[0] != '\0') ? Serum_Load(m_altColorPath, m_romName, flags) : nullptr; @@ -1074,6 +1357,8 @@ void DMD::SerumThread() Serum_SetIgnoreUnknownFramesTimeout(Config::GetInstance()->GetIgnoreUnknownFramesTimeout()); Serum_SetMaximumUnknownFramesToSkip(Config::GetInstance()->GetMaximumUnknownFramesToSkip()); + m_serumHasTimestamp = false; + m_serumLastTimestampMs = 0; } } @@ -1088,7 +1373,11 @@ void DMD::SerumThread() lastDmdUpdate = m_pUpdateBufferQueue[bufferPositionMod]; - QueueSerumFrames(lastDmdUpdate, flags & FLAG_REQUEST_32P_FRAMES, flags & FLAG_REQUEST_64P_FRAMES); + uint32_t queuedTimestamp = 0; + bool hasTimestamp = GetQueueTimestamp(bufferPositionMod, queuedTimestamp); + + QueueSerumFrames(lastDmdUpdate, flags & FLAG_REQUEST_32P_FRAMES, flags & FLAG_REQUEST_64P_FRAMES, + hasTimestamp, queuedTimestamp); if (result > 0 && ((result & 0xffff) < 2048)) { @@ -1121,7 +1410,9 @@ void DMD::SerumThread() (size_t)m_pUpdateBufferQueue[bufferPositionMod]->width * m_pUpdateBufferQueue[bufferPositionMod]->height); - QueueUpdate(noSerumUpdate, false); + uint32_t queuedTimestamp = 0; + bool hasTimestamp = GetQueueTimestamp(bufferPositionMod, queuedTimestamp); + QueueUpdate(noSerumUpdate, false, hasTimestamp, queuedTimestamp); } } } @@ -1135,7 +1426,7 @@ void DMD::SerumThread() Log(DMDUtil_LogLevel_DEBUG, "Serum: rotation=%lu, flags=%lu", m_pSerum->rotationtimer, result >> 16); - QueueSerumFrames(lastDmdUpdate, result & 0x10000, result & 0x20000); + QueueSerumFrames(lastDmdUpdate, result & 0x10000, result & 0x20000, false, 0); if (result > 0 && ((result & 0xffff) < 2048)) { @@ -1149,10 +1440,189 @@ void DMD::SerumThread() } } -void DMD::QueueSerumFrames(Update* dmdUpdate, bool render32, bool render64) +void DMD::VniThread() +{ + Config* const pConfig = Config::GetInstance(); + + if (!pConfig->IsAltColor()) + { + return; + } + + uint16_t bufferPosition = 0; + char name[DMDUTIL_MAX_NAME_SIZE] = {0}; + + (void)m_stopFlag.load(std::memory_order_acquire); + + bool showNotColorizedFrames = pConfig->IsShowNotColorizedFrames(); + bool dumpNotColorizedFrames = pConfig->IsDumpNotColorizedFrames(); + + while (true) + { + std::shared_lock sl(m_dmdSharedMutex); + m_dmdCV.wait(sl, + [&]() + { + return m_stopFlag.load(std::memory_order_relaxed) || + (m_updateBufferQueuePosition.load(std::memory_order_relaxed) != bufferPosition); + }); + sl.unlock(); + + if (m_stopFlag.load(std::memory_order_acquire)) + { + if (m_pVni) + { + Vni_Dispose(m_pVni); + m_pVni = nullptr; + } + return; + } + + const uint16_t updateBufferQueuePosition = m_updateBufferQueuePosition.load(std::memory_order_acquire); + while (bufferPosition != updateBufferQueuePosition) + { + ++bufferPosition; // 65635 + 1 = 0 + uint8_t bufferPositionMod = bufferPosition % DMDUTIL_FRAME_BUFFER_SIZE; + + if (m_pSerum) + { + if (m_pVni) + { + Vni_Dispose(m_pVni); + m_pVni = nullptr; + } + strcpy(name, ""); + continue; + } + + if (m_pVni && (m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::RGB24 || + m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::RGB16)) + { + Vni_Dispose(m_pVni); + m_pVni = nullptr; + strcpy(name, ""); + QueueBuffer(); + continue; + } + + if (m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::Data) + { + if (strcmp(m_romName, name) != 0) + { + strcpy(name, m_romName); + + if (m_pVni) + { + Vni_Dispose(m_pVni); + m_pVni = nullptr; + } + + if (m_altColorPath[0] == '\0') strcpy(m_altColorPath, Config::GetInstance()->GetAltColorPath()); + + std::string baseDir = BuildAltColorDir(m_altColorPath, m_romName); + std::string palPath; + std::string vniPath; + std::string pacPath; + + const std::string baseName = std::string(m_romName); + FindCaseInsensitiveFile(baseDir, baseName + ".pal", &palPath); + FindCaseInsensitiveFile(baseDir, baseName + ".vni", &vniPath); + FindCaseInsensitiveFile(baseDir, baseName + ".pac", &pacPath); + + const char* vniKey = pConfig->GetVniKey(); + if (!pacPath.empty() && (!vniKey || vniKey[0] == '\0')) + { + Log(DMDUtil_LogLevel_ERROR, "VNI: pac file requires VNI key for %s", m_romName); + } + else if (!palPath.empty() || !vniPath.empty() || !pacPath.empty()) + { + m_pVni = Vni_LoadFromPaths(palPath.empty() ? nullptr : palPath.c_str(), + vniPath.empty() ? nullptr : vniPath.c_str(), + pacPath.empty() ? nullptr : pacPath.c_str(), + (vniKey && vniKey[0] != '\0') ? vniKey : nullptr); + if (m_pVni) + { + Log(DMDUtil_LogLevel_INFO, "VNI: Loaded colorization for %s", m_romName); + } + } + } + + if (m_pVni) + { + uint16_t width = m_pUpdateBufferQueue[bufferPositionMod]->width; + uint16_t height = m_pUpdateBufferQueue[bufferPositionMod]->height; + uint8_t depth = (uint8_t)m_pUpdateBufferQueue[bufferPositionMod]->depth; + + uint32_t result = Vni_Colorize(m_pVni, m_pUpdateBufferQueue[bufferPositionMod]->data, width, height, depth); + if (result) + { + const Vni_Frame_Struc* frame = Vni_GetFrame(m_pVni); + if (frame && frame->has_frame && frame->frame && frame->palette && frame->bitlen <= 8) + { + const size_t frameSize = (size_t)frame->width * frame->height; + const size_t paletteSize = (size_t)1u << frame->bitlen; + + if (frameSize <= (256u * 64u) && paletteSize <= 256u) + { + auto vniUpdate = std::make_shared(); + vniUpdate->mode = Mode::Vni; + vniUpdate->depth = frame->bitlen; + vniUpdate->width = (uint16_t)frame->width; + vniUpdate->height = (uint16_t)frame->height; + vniUpdate->hasData = true; + vniUpdate->hasSegData = false; + vniUpdate->hasSegData2 = false; + memcpy(vniUpdate->data, frame->frame, frameSize); + memcpy(vniUpdate->segData, frame->palette, paletteSize * 3); + + uint32_t queuedTimestamp = 0; + bool hasTimestamp = GetQueueTimestamp(bufferPositionMod, queuedTimestamp); + QueueUpdate(vniUpdate, false, hasTimestamp, queuedTimestamp); + } + } + } + else if (showNotColorizedFrames || dumpNotColorizedFrames) + { + Log(DMDUtil_LogLevel_DEBUG, "VNI: unidentified frame detected"); + + auto noVniUpdate = std::make_shared(); + noVniUpdate->mode = Mode::NotColorized; + noVniUpdate->depth = m_pUpdateBufferQueue[bufferPositionMod]->depth; + noVniUpdate->width = m_pUpdateBufferQueue[bufferPositionMod]->width; + noVniUpdate->height = m_pUpdateBufferQueue[bufferPositionMod]->height; + noVniUpdate->hasData = true; + noVniUpdate->hasSegData = false; + noVniUpdate->hasSegData2 = false; + memcpy(noVniUpdate->data, m_pUpdateBufferQueue[bufferPositionMod]->data, + (size_t)m_pUpdateBufferQueue[bufferPositionMod]->width * + m_pUpdateBufferQueue[bufferPositionMod]->height); + + uint32_t queuedTimestamp = 0; + bool hasTimestamp = GetQueueTimestamp(bufferPositionMod, queuedTimestamp); + QueueUpdate(noVniUpdate, false, hasTimestamp, queuedTimestamp); + } + } + } + } + } +} + +void DMD::QueueSerumFrames(Update* dmdUpdate, bool render32, bool render64, bool hasTimestamp, uint32_t timestampMs) { if (!render32 && !render64) return; + if (!hasTimestamp && m_serumHasTimestamp && m_pSerum && m_pSerum->rotationtimer > 0) + { + timestampMs = m_serumLastTimestampMs + (uint32_t)m_pSerum->rotationtimer; + hasTimestamp = true; + } + + if (hasTimestamp) + { + m_serumHasTimestamp = true; + m_serumLastTimestampMs = timestampMs; + } + auto serumUpdate = std::make_shared(); serumUpdate->hasData = true; serumUpdate->hasSegData = false; @@ -1167,7 +1637,7 @@ void DMD::QueueSerumFrames(Update* dmdUpdate, bool render32, bool render64) memcpy(serumUpdate->data, m_pSerum->frame, (size_t)dmdUpdate->width * dmdUpdate->height); memcpy(serumUpdate->segData, m_pSerum->palette, PALETTE_SIZE); - QueueUpdate(serumUpdate, false); + QueueUpdate(serumUpdate, false, hasTimestamp, timestampMs); } else if (m_pSerum->SerumVersion == SERUM_V2) { @@ -1181,7 +1651,7 @@ void DMD::QueueSerumFrames(Update* dmdUpdate, bool render32, bool render64) serumUpdate->height = 32; memcpy(serumUpdate->segData, m_pSerum->frame32, m_pSerum->width32 * 32 * sizeof(uint16_t)); - QueueUpdate(serumUpdate, false); + QueueUpdate(serumUpdate, false, hasTimestamp, timestampMs); } } else if (m_pSerum->width32 == 0 && m_pSerum->width64 > 0) @@ -1194,7 +1664,7 @@ void DMD::QueueSerumFrames(Update* dmdUpdate, bool render32, bool render64) serumUpdate->height = 64; memcpy(serumUpdate->segData, m_pSerum->frame64, m_pSerum->width64 * 64 * sizeof(uint16_t)); - QueueUpdate(serumUpdate, false); + QueueUpdate(serumUpdate, false, hasTimestamp, timestampMs); } } else if (m_pSerum->width32 > 0 && m_pSerum->width64 > 0) @@ -1207,7 +1677,7 @@ void DMD::QueueSerumFrames(Update* dmdUpdate, bool render32, bool render64) serumUpdate->height = 32; memcpy(serumUpdate->segData, m_pSerum->frame32, m_pSerum->width32 * 32 * sizeof(uint16_t)); - QueueUpdate(serumUpdate, false); + QueueUpdate(serumUpdate, false, hasTimestamp, timestampMs); } if (render64) @@ -1223,7 +1693,7 @@ void DMD::QueueSerumFrames(Update* dmdUpdate, bool render32, bool render64) serumUpdateHD->height = 64; memcpy(serumUpdateHD->segData, m_pSerum->frame64, m_pSerum->width64 * 64 * sizeof(uint16_t)); - QueueUpdate(serumUpdateHD, false); + QueueUpdate(serumUpdateHD, false, hasTimestamp, timestampMs); } } } @@ -1233,51 +1703,265 @@ void DMD::QueueSerumFrames(Update* dmdUpdate, bool render32, bool render64) (defined(__APPLE__) && ((defined(TARGET_OS_IOS) && TARGET_OS_IOS) || (defined(TARGET_OS_TV) && TARGET_OS_TV))) || \ defined(__ANDROID__)) -void DMD::PixelcadeDMDThread() +#if defined(DMDUTIL_ENABLE_PIN2DMD) +void DMD::PIN2DMDThread() { uint16_t bufferPosition = 0; uint16_t segData1[128] = {0}; uint16_t segData2[128] = {0}; - uint8_t palette[PALETTE_SIZE] = {0}; + uint8_t palette[256 * 3] = {0}; + uint8_t renderBuffer[256 * 64] = {0}; - const int targetWidth = m_pPixelcadeDMD->GetWidth(); - const int targetHeight = m_pPixelcadeDMD->GetHeight(); + const int targetWidth = m_PIN2DMDWidth; + const int targetHeight = m_PIN2DMDHeight; const int targetLength = targetWidth * targetHeight; - uint16_t* rgb565Data = new uint16_t[targetLength]; - memset(rgb565Data, 0, targetLength * sizeof(uint16_t)); + const int maxSourceLength = 256 * 64; + uint8_t* rgb24Data = new uint8_t[maxSourceLength * 3]; + uint8_t* scaledBuffer = new uint8_t[targetLength * 3]; + constexpr int kMaxTempWidth = 384; + constexpr int kMaxTempHeight = 128; + uint8_t* tempBuffer = new uint8_t[kMaxTempWidth * kMaxTempHeight * 3]; + + memset(rgb24Data, 0, maxSourceLength * 3); + memset(scaledBuffer, 0, targetLength * 3); (void)m_stopFlag.load(std::memory_order_acquire); Config* const pConfig = Config::GetInstance(); bool showNotColorizedFrames = pConfig->IsShowNotColorizedFrames(); - while (true) + auto scaleToTarget = [&](const uint8_t* src, uint16_t width, uint16_t height, uint8_t* dst) -> bool { - std::shared_lock sl(m_dmdSharedMutex); - m_dmdCV.wait(sl, - [&]() - { - return m_stopFlag.load(std::memory_order_relaxed) || - (m_updateBufferQueuePosition.load(std::memory_order_relaxed) != bufferPosition); - }); - sl.unlock(); - if (m_stopFlag.load(std::memory_order_acquire)) + if (width == targetWidth && height == targetHeight) { - delete[] rgb565Data; - return; + memcpy(dst, src, targetLength * 3); + return true; } - const uint16_t updateBufferQueuePosition = m_updateBufferQueuePosition.load(std::memory_order_acquire); - while (!m_stopFlag.load(std::memory_order_relaxed) && bufferPosition != updateBufferQueuePosition) + if (width == targetWidth && height == 16) { - bufferPosition = GetNextBufferQueuePosition(bufferPosition, updateBufferQueuePosition); - uint8_t bufferPositionMod = bufferPosition % DMDUTIL_FRAME_BUFFER_SIZE; + FrameUtil::Helper::Center(dst, targetWidth, targetHeight, src, targetWidth, 16, 24); + return true; + } - if (m_pSerum && !IsSerumMode(m_pUpdateBufferQueue[bufferPositionMod]->mode, showNotColorizedFrames)) continue; + if (height == 64 && targetHeight == 32) + { + FrameUtil::Helper::ScaleDown(dst, targetWidth, targetHeight, src, width, 64, 24); + return true; + } - if (m_pUpdateBufferQueue[bufferPositionMod]->hasData || m_pUpdateBufferQueue[bufferPositionMod]->hasSegData) + if (height == 32 && targetHeight == 64) + { + const int upWidth = width * 2; + const int upHeight = height * 2; + if (upWidth == targetWidth && upHeight == targetHeight) { - uint16_t width = m_pUpdateBufferQueue[bufferPositionMod]->width; + FrameUtil::Helper::ScaleUp(dst, src, width, height, 24); + return true; + } + if (upWidth <= kMaxTempWidth && upHeight <= kMaxTempHeight) + { + FrameUtil::Helper::ScaleUp(tempBuffer, src, width, height, 24); + FrameUtil::Helper::ScaleDown(dst, targetWidth, targetHeight, tempBuffer, upWidth, upHeight, 24); + return true; + } + return false; + } + + if (height == 64 && targetHeight == 64) + { + if (width > targetWidth) + { + FrameUtil::Helper::ScaleDown(dst, targetWidth, targetHeight, src, width, height, 24); + return true; + } + if (width < targetWidth) + { + const int upWidth = width * 2; + const int upHeight = height * 2; + if (upWidth <= kMaxTempWidth && upHeight <= kMaxTempHeight) + { + FrameUtil::Helper::ScaleUp(tempBuffer, src, width, height, 24); + FrameUtil::Helper::ScaleDown(dst, targetWidth, targetHeight, tempBuffer, upWidth, upHeight, 24); + return true; + } + } + } + + return false; + }; + + while (true) + { + std::shared_lock sl(m_dmdSharedMutex); + m_dmdCV.wait(sl, + [&]() + { + return m_stopFlag.load(std::memory_order_relaxed) || + (m_updateBufferQueuePosition.load(std::memory_order_relaxed) != bufferPosition); + }); + sl.unlock(); + if (m_stopFlag.load(std::memory_order_acquire)) + { + delete[] rgb24Data; + delete[] scaledBuffer; + delete[] tempBuffer; + return; + } + + const uint16_t updateBufferQueuePosition = m_updateBufferQueuePosition.load(std::memory_order_acquire); + while (!m_stopFlag.load(std::memory_order_relaxed) && bufferPosition != updateBufferQueuePosition) + { + bufferPosition = GetNextBufferQueuePosition(bufferPosition, updateBufferQueuePosition); + uint8_t bufferPositionMod = bufferPosition % DMDUTIL_FRAME_BUFFER_SIZE; + + if ((m_pSerum || m_pVni) && + !IsSerumMode(m_pUpdateBufferQueue[bufferPositionMod]->mode, showNotColorizedFrames)) + continue; + + if (!(m_pUpdateBufferQueue[bufferPositionMod]->hasData || m_pUpdateBufferQueue[bufferPositionMod]->hasSegData)) + continue; + + uint16_t width = m_pUpdateBufferQueue[bufferPositionMod]->width; + uint16_t height = m_pUpdateBufferQueue[bufferPositionMod]->height; + int length = (int)width * height; + + bool update = false; + if (m_pUpdateBufferQueue[bufferPositionMod]->depth != 24) + { + update = UpdatePalette(palette, m_pUpdateBufferQueue[bufferPositionMod]->depth, + m_pUpdateBufferQueue[bufferPositionMod]->r, m_pUpdateBufferQueue[bufferPositionMod]->g, + m_pUpdateBufferQueue[bufferPositionMod]->b); + } + + if (m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::RGB24) + { + AdjustRGB24Depth(m_pUpdateBufferQueue[bufferPositionMod]->data, rgb24Data, length, palette, + m_pUpdateBufferQueue[bufferPositionMod]->depth); + update = true; + } + else if (m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::RGB16 || + IsSerumV2Mode(m_pUpdateBufferQueue[bufferPositionMod]->mode)) + { + const uint16_t* src = m_pUpdateBufferQueue[bufferPositionMod]->segData; + for (int i = 0; i < length; i++) + { + uint16_t value = src[i]; + uint8_t r = (uint8_t)((value >> 11) & 0x1F); + uint8_t g = (uint8_t)((value >> 5) & 0x3F); + uint8_t b = (uint8_t)(value & 0x1F); + rgb24Data[i * 3] = (uint8_t)((r << 3) | (r >> 2)); + rgb24Data[i * 3 + 1] = (uint8_t)((g << 2) | (g >> 4)); + rgb24Data[i * 3 + 2] = (uint8_t)((b << 3) | (b >> 2)); + } + update = true; + } + else + { + if (m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::SerumV1 || + m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::Vni) + { + size_t paletteBytes = PaletteBytesForDepth((uint8_t)m_pUpdateBufferQueue[bufferPositionMod]->depth); + if (paletteBytes > 0 && paletteBytes <= sizeof(palette)) + { + memcpy(palette, m_pUpdateBufferQueue[bufferPositionMod]->segData, paletteBytes); + } + memcpy(renderBuffer, m_pUpdateBufferQueue[bufferPositionMod]->data, length); + update = true; + } + else if ((!(m_pSerum || m_pVni) && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::Data) || + (showNotColorizedFrames && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::NotColorized)) + { + memcpy(renderBuffer, m_pUpdateBufferQueue[bufferPositionMod]->data, length); + update = true; + } + else if (m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::AlphaNumeric) + { + if (memcmp(segData1, m_pUpdateBufferQueue[bufferPositionMod]->segData, sizeof(segData1)) != 0) + { + memcpy(segData1, m_pUpdateBufferQueue[bufferPositionMod]->segData, sizeof(segData1)); + update = true; + } + + if (m_pUpdateBufferQueue[bufferPositionMod]->hasSegData2 && + memcmp(segData2, m_pUpdateBufferQueue[bufferPositionMod]->segData2, sizeof(segData2)) != 0) + { + memcpy(segData2, m_pUpdateBufferQueue[bufferPositionMod]->segData2, sizeof(segData2)); + update = true; + } + + if (update) + { + if (m_pUpdateBufferQueue[bufferPositionMod]->hasSegData2) + m_pAlphaNumeric->Render(renderBuffer, m_pUpdateBufferQueue[bufferPositionMod]->layout, segData1, + segData2); + else + m_pAlphaNumeric->Render(renderBuffer, m_pUpdateBufferQueue[bufferPositionMod]->layout, segData1); + } + } + + if (update) + { + FrameUtil::Helper::ConvertToRgb24(rgb24Data, renderBuffer, length, palette); + } + } + + if (update && scaleToTarget(rgb24Data, width, height, scaledBuffer)) + { + PIN2DMDRenderRaw(targetWidth, targetHeight, scaledBuffer, 1); + } + } + } +} +#endif + +void DMD::PixelcadeDMDThread() +{ + uint16_t bufferPosition = 0; + uint16_t segData1[128] = {0}; + uint16_t segData2[128] = {0}; + uint8_t palette[256 * 3] = {0}; + + const int targetWidth = m_pPixelcadeDMD->GetWidth(); + const int targetHeight = m_pPixelcadeDMD->GetHeight(); + const int targetLength = targetWidth * targetHeight; + uint16_t* rgb565Data = new uint16_t[targetLength]; + memset(rgb565Data, 0, targetLength * sizeof(uint16_t)); + + (void)m_stopFlag.load(std::memory_order_acquire); + + Config* const pConfig = Config::GetInstance(); + bool showNotColorizedFrames = pConfig->IsShowNotColorizedFrames(); + + while (true) + { + std::shared_lock sl(m_dmdSharedMutex); + m_dmdCV.wait(sl, + [&]() + { + return m_stopFlag.load(std::memory_order_relaxed) || + (m_updateBufferQueuePosition.load(std::memory_order_relaxed) != bufferPosition); + }); + sl.unlock(); + if (m_stopFlag.load(std::memory_order_acquire)) + { + delete[] rgb565Data; + return; + } + + const uint16_t updateBufferQueuePosition = m_updateBufferQueuePosition.load(std::memory_order_acquire); + while (!m_stopFlag.load(std::memory_order_relaxed) && bufferPosition != updateBufferQueuePosition) + { + bufferPosition = GetNextBufferQueuePosition(bufferPosition, updateBufferQueuePosition); + uint8_t bufferPositionMod = bufferPosition % DMDUTIL_FRAME_BUFFER_SIZE; + + if ((m_pSerum || m_pVni) && + !IsSerumMode(m_pUpdateBufferQueue[bufferPositionMod]->mode, showNotColorizedFrames)) + continue; + + if (m_pUpdateBufferQueue[bufferPositionMod]->hasData || m_pUpdateBufferQueue[bufferPositionMod]->hasSegData) + { + uint16_t width = m_pUpdateBufferQueue[bufferPositionMod]->width; uint16_t height = m_pUpdateBufferQueue[bufferPositionMod]->height; int length = (int)width * height; @@ -1357,13 +2041,18 @@ void DMD::PixelcadeDMDThread() { uint8_t renderBuffer[256 * 64]; - if (m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::SerumV1) + if (m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::SerumV1 || + m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::Vni) { - memcpy(palette, m_pUpdateBufferQueue[bufferPositionMod]->segData, PALETTE_SIZE); + size_t paletteBytes = PaletteBytesForDepth((uint8_t)m_pUpdateBufferQueue[bufferPositionMod]->depth); + if (paletteBytes > 0 && paletteBytes <= sizeof(palette)) + { + memcpy(palette, m_pUpdateBufferQueue[bufferPositionMod]->segData, paletteBytes); + } memcpy(renderBuffer, m_pUpdateBufferQueue[bufferPositionMod]->data, length); update = true; } - else if ((!m_pSerum && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::Data) || + else if ((!(m_pSerum || m_pVni) && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::Data) || (showNotColorizedFrames && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::NotColorized)) { memcpy(renderBuffer, m_pUpdateBufferQueue[bufferPositionMod]->data, length); @@ -1479,7 +2168,7 @@ void DMD::RGB24DMDThread() uint16_t bufferPosition = 0; uint16_t segData1[128] = {0}; uint16_t segData2[128] = {0}; - uint8_t palette[PALETTE_SIZE] = {0}; + uint8_t palette[256 * 3] = {0}; uint8_t renderBuffer[256 * 64] = {0}; uint8_t rgb24Data[256 * 64 * 3] = {0}; uint8_t rgb24DataScaled[256 * 64 * 3] = {0}; @@ -1510,7 +2199,9 @@ void DMD::RGB24DMDThread() bufferPosition = GetNextBufferQueuePosition(bufferPosition, updateBufferQueuePosition); uint8_t bufferPositionMod = bufferPosition % DMDUTIL_FRAME_BUFFER_SIZE; - if (m_pSerum && !IsSerumMode(m_pUpdateBufferQueue[bufferPositionMod]->mode, showNotColorizedFrames)) continue; + if ((m_pSerum || m_pVni) && + !IsSerumMode(m_pUpdateBufferQueue[bufferPositionMod]->mode, showNotColorizedFrames)) + continue; if (!m_rgb24DMDs.empty() && (m_pUpdateBufferQueue[bufferPositionMod]->hasData || m_pUpdateBufferQueue[bufferPositionMod]->hasSegData)) @@ -1545,9 +2236,14 @@ void DMD::RGB24DMDThread() else if (m_pUpdateBufferQueue[bufferPositionMod]->mode != Mode::RGB16 && !IsSerumV2Mode(m_pUpdateBufferQueue[bufferPositionMod]->mode)) { - if (m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::SerumV1) + if (m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::SerumV1 || + m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::Vni) { - memcpy(palette, m_pUpdateBufferQueue[bufferPositionMod]->segData, PALETTE_SIZE); + size_t paletteBytes = PaletteBytesForDepth((uint8_t)m_pUpdateBufferQueue[bufferPositionMod]->depth); + if (paletteBytes > 0 && paletteBytes <= sizeof(palette)) + { + memcpy(palette, m_pUpdateBufferQueue[bufferPositionMod]->segData, paletteBytes); + } memcpy(renderBuffer, m_pUpdateBufferQueue[bufferPositionMod]->data, length); update = true; } @@ -1557,7 +2253,7 @@ void DMD::RGB24DMDThread() palette, m_pUpdateBufferQueue[bufferPositionMod]->depth, m_pUpdateBufferQueue[bufferPositionMod]->r, m_pUpdateBufferQueue[bufferPositionMod]->g, m_pUpdateBufferQueue[bufferPositionMod]->b); - if ((!m_pSerum && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::Data) || + if ((!(m_pSerum || m_pVni) && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::Data) || (showNotColorizedFrames && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::NotColorized)) { if (memcmp(renderBuffer, m_pUpdateBufferQueue[bufferPositionMod]->data, length) != 0) @@ -1626,7 +2322,7 @@ void DMD::RGB24DMDThread() for (RGB24DMD* pRGB24DMD : m_rgb24DMDs) { - if (m_pSerum && + if ((m_pSerum || m_pVni) && (!IsSerumMode(m_pUpdateBufferQueue[bufferPositionMod]->mode, showNotColorizedFrames) || (pRGB24DMD->GetWidth() == 256 && m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::SerumV2_32_64) || @@ -1761,6 +2457,66 @@ void DMD::GenerateRandomSuffix(char* buffer, size_t length) buffer[length] = '\0'; } +bool DMD::GetDumpSuffix(const char* romName, char* outSuffix, size_t outSize) +{ + if (!romName || romName[0] == '\0' || !outSuffix || outSize == 0) + { + return false; + } + + std::lock_guard lock(m_dumpSuffixMutex); + if (!m_dumpSuffixValid || strcmp(romName, m_dumpSuffixRom) != 0) + { + strncpy(m_dumpSuffixRom, romName, sizeof(m_dumpSuffixRom) - 1); + m_dumpSuffixRom[sizeof(m_dumpSuffixRom) - 1] = '\0'; + GenerateRandomSuffix(m_dumpSuffix, 8); + m_dumpSuffixValid = true; + } + + strncpy(outSuffix, m_dumpSuffix, outSize - 1); + outSuffix[outSize - 1] = '\0'; + return true; +} + +bool DMD::GetQueueTimestamp(uint8_t bufferPositionMod, uint32_t& timestampMs) const +{ + if (!m_updateBufferQueueHasTimestamp[bufferPositionMod]) return false; + timestampMs = m_updateBufferQueueTimestamp[bufferPositionMod]; + return true; +} + +uint16_t DMD::GetUpdateQueuePosition() const +{ + return m_updateBufferQueuePosition.load(std::memory_order_acquire); +} + +bool DMD::DumpersReached(uint16_t targetPosition) const +{ + if (m_dumpTxtActive.load(std::memory_order_acquire) && + m_dumpTxtPosition.load(std::memory_order_acquire) != targetPosition) + return false; + if (m_dumpRawActive.load(std::memory_order_acquire) && + m_dumpRawPosition.load(std::memory_order_acquire) != targetPosition) + return false; + if (m_dump565Active.load(std::memory_order_acquire) && + m_dump565Position.load(std::memory_order_acquire) != targetPosition) + return false; + if (m_dump888Active.load(std::memory_order_acquire) && + m_dump888Position.load(std::memory_order_acquire) != targetPosition) + return false; + return true; +} + +bool DMD::WaitForDumpers(uint16_t targetPosition, uint32_t timeoutMs) +{ + std::unique_lock lock(m_dumpPositionMutex); + if (DumpersReached(targetPosition)) return true; + if (timeoutMs == 0) return false; + auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + return m_dumpPositionCv.wait_until(lock, deadline, [&]() { return DumpersReached(targetPosition); }); +} + + void DMD::DumpDMDTxtThread() { char name[DMDUTIL_MAX_NAME_SIZE] = {0}; @@ -1776,6 +2532,9 @@ void DMD::DumpDMDTxtThread() Config* const pConfig = Config::GetInstance(); bool dumpNotColorizedFrames = pConfig->IsDumpNotColorizedFrames(); bool filterTransitionalFrames = pConfig->IsFilterTransitionalFrames(); + m_dumpTxtActive.store(true, std::memory_order_release); + m_dumpTxtPosition.store(bufferPosition, std::memory_order_release); + m_dumpPositionCv.notify_all(); while (true) { @@ -1795,6 +2554,8 @@ void DMD::DumpDMDTxtThread() fclose(f); f = nullptr; } + m_dumpTxtActive.store(false, std::memory_order_release); + m_dumpPositionCv.notify_all(); return; } @@ -1804,6 +2565,8 @@ void DMD::DumpDMDTxtThread() // Don't use GetNextBufferPosition() here, we need all frames! ++bufferPosition; // 65635 + 1 = 0 uint8_t bufferPositionMod = bufferPosition % DMDUTIL_FRAME_BUFFER_SIZE; + m_dumpTxtPosition.store(bufferPosition, std::memory_order_release); + m_dumpPositionCv.notify_all(); if (m_pUpdateBufferQueue[bufferPositionMod]->depth <= 4 && m_pUpdateBufferQueue[bufferPositionMod]->hasData && ((m_pUpdateBufferQueue[bufferPositionMod]->mode == Mode::Data && !dumpNotColorizedFrames) || @@ -1825,7 +2588,10 @@ void DMD::DumpDMDTxtThread() { char filename[DMDUTIL_MAX_NAME_SIZE + 128 + 8 + 5]; char suffix[9]; // 8 chars + null terminator - GenerateRandomSuffix(suffix, 8); + if (!GetDumpSuffix(name, suffix, sizeof(suffix))) + { + GenerateRandomSuffix(suffix, 8); + } if (m_dumpPath[0] == '\0') strcpy(m_dumpPath, Config::GetInstance()->GetDumpPath()); size_t pathLen = strlen(m_dumpPath); if (pathLen == 0) @@ -1853,9 +2619,17 @@ void DMD::DumpDMDTxtThread() (int)m_pUpdateBufferQueue[bufferPositionMod]->width * m_pUpdateBufferQueue[bufferPositionMod]->height; if (update || (memcmp(renderBuffer[1], m_pUpdateBufferQueue[bufferPositionMod]->data, length) != 0)) { - passed[2] = (uint32_t)(std::chrono::duration_cast( - std::chrono::steady_clock::now() - start) - .count()); + uint32_t queuedTimestamp = 0; + if (GetQueueTimestamp(bufferPositionMod, queuedTimestamp)) + { + passed[2] = queuedTimestamp; + } + else + { + passed[2] = (uint32_t)(std::chrono::duration_cast( + std::chrono::steady_clock::now() - start) + .count()); + } memcpy(renderBuffer[2], m_pUpdateBufferQueue[bufferPositionMod]->data, length); if (filterTransitionalFrames && m_pUpdateBufferQueue[bufferPositionMod]->depth == 2 && @@ -1925,6 +2699,410 @@ void DMD::DumpDMDTxtThread() } } +void DMD::DumpDMDRgb565Thread() +{ + char name[DMDUTIL_MAX_NAME_SIZE] = {0}; + uint16_t bufferPosition = 0; + uint16_t renderBuffer[3][256 * 64] = {0}; + uint16_t frameWidths[3] = {0}; + uint16_t frameHeights[3] = {0}; + uint32_t passed[3] = {0}; + std::chrono::steady_clock::time_point start; + FILE* f = nullptr; + uint8_t palette[256 * 3] = {0}; + uint8_t rgb24Temp[256 * 64 * 3] = {0}; + + (void)m_stopFlag.load(std::memory_order_acquire); + m_dump565Active.store(true, std::memory_order_release); + m_dump565Position.store(bufferPosition, std::memory_order_release); + m_dumpPositionCv.notify_all(); + + while (true) + { + std::shared_lock sl(m_dmdSharedMutex); + m_dmdCV.wait(sl, + [&]() + { + return m_stopFlag.load(std::memory_order_relaxed) || + (m_updateBufferQueuePosition.load(std::memory_order_relaxed) != bufferPosition); + }); + sl.unlock(); + if (m_stopFlag.load(std::memory_order_acquire)) + { + if (f) + { + fflush(f); + fclose(f); + f = nullptr; + } + m_dump565Active.store(false, std::memory_order_release); + m_dumpPositionCv.notify_all(); + return; + } + + const uint16_t updateBufferQueuePosition = m_updateBufferQueuePosition.load(std::memory_order_acquire); + while (!m_stopFlag.load(std::memory_order_relaxed) && bufferPosition != updateBufferQueuePosition) + { + // Don't use GetNextBufferPosition() here, we need all frames! + ++bufferPosition; // 65635 + 1 = 0 + uint8_t bufferPositionMod = bufferPosition % DMDUTIL_FRAME_BUFFER_SIZE; + m_dump565Position.store(bufferPosition, std::memory_order_release); + m_dumpPositionCv.notify_all(); + + Update* update = m_pUpdateBufferQueue[bufferPositionMod]; + if (!(update->hasData || update->hasSegData)) continue; + + if (!(update->mode == Mode::RGB24 || update->mode == Mode::RGB16 || update->mode == Mode::SerumV1 || + update->mode == Mode::Vni || IsSerumV2Mode(update->mode))) + continue; + + bool updateFrame = false; + if (strcmp(m_romName, name) != 0) + { + // New game ROM. + start = std::chrono::steady_clock::now(); + if (f) + { + fclose(f); + f = nullptr; + } + strcpy(name, m_romName); + + if (name[0] != '\0') + { + char filename[DMDUTIL_MAX_NAME_SIZE + 128 + 8 + 9]; + char suffix[9]; // 8 chars + null terminator + if (!GetDumpSuffix(name, suffix, sizeof(suffix))) + { + GenerateRandomSuffix(suffix, 8); + } + if (m_dumpPath[0] == '\0') strcpy(m_dumpPath, Config::GetInstance()->GetDumpPath()); + size_t pathLen = strlen(m_dumpPath); + if (pathLen == 0) + { + snprintf(filename, sizeof(filename), "./%s-%s.565.txt", name, suffix); + } + else if (m_dumpPath[pathLen - 1] == '/' || m_dumpPath[pathLen - 1] == '\\') + { + snprintf(filename, sizeof(filename), "%s%s-%s.565.txt", m_dumpPath, name, suffix); + } + else + { + snprintf(filename, sizeof(filename), "%s/%s-%s.565.txt", m_dumpPath, name, suffix); + } + f = fopen(filename, "w"); + updateFrame = true; + memset(renderBuffer, 0, sizeof(renderBuffer)); + memset(frameWidths, 0, sizeof(frameWidths)); + memset(frameHeights, 0, sizeof(frameHeights)); + passed[0] = passed[1] = 0; + } + } + + if (name[0] == '\0') + { + continue; + } + + uint16_t width = update->width; + uint16_t height = update->height; + int length = (int)width * height; + size_t frameBytes = (size_t)length * sizeof(uint16_t); + if (frameBytes > sizeof(renderBuffer[0])) + { + continue; + } + + if (width != frameWidths[1] || height != frameHeights[1]) + { + updateFrame = true; + } + + uint16_t* nextFrame = renderBuffer[2]; + if (update->mode == Mode::RGB16 || IsSerumV2Mode(update->mode)) + { + memcpy(nextFrame, update->segData, frameBytes); + } + else + { + if (update->mode == Mode::RGB24) + { + if (update->depth != 24) + { + UpdatePalette(palette, update->depth, update->r, update->g, update->b); + } + AdjustRGB24Depth(update->data, rgb24Temp, length, palette, update->depth); + } + else + { + size_t paletteBytes = PaletteBytesForDepth((uint8_t)update->depth); + if (paletteBytes > 0 && paletteBytes <= sizeof(palette)) + { + memcpy(palette, update->segData, paletteBytes); + } + FrameUtil::Helper::ConvertToRgb24(rgb24Temp, update->data, length, palette); + } + + for (int i = 0; i < length; i++) + { + int pos = i * 3; + uint32_t r = rgb24Temp[pos]; + uint32_t g = rgb24Temp[pos + 1]; + uint32_t b = rgb24Temp[pos + 2]; + nextFrame[i] = (uint16_t)(((r & 0xF8u) << 8) | ((g & 0xFCu) << 3) | (b >> 3)); + } + } + + if (updateFrame || memcmp(renderBuffer[1], nextFrame, frameBytes) != 0) + { + uint32_t queuedTimestamp = 0; + if (GetQueueTimestamp(bufferPositionMod, queuedTimestamp)) + { + passed[2] = queuedTimestamp; + } + else + { + passed[2] = (uint32_t)(std::chrono::duration_cast( + std::chrono::steady_clock::now() - start) + .count()); + } + frameWidths[2] = width; + frameHeights[2] = height; + + if (f && passed[0] > 0 && frameWidths[0] > 0 && frameHeights[0] > 0) + { + fprintf(f, "0x%08x\r\n", passed[0]); + uint32_t rowWidth = frameWidths[0]; + uint32_t rowHeight = frameHeights[0]; + const uint16_t* frame = renderBuffer[0]; + for (uint32_t y = 0; y < rowHeight; y++) + { + for (uint32_t x = 0; x < rowWidth; x++) + { + fprintf(f, "%04x", frame[y * rowWidth + x]); + } + fprintf(f, "\r\n"); + } + fprintf(f, "\r\n"); + } + + size_t prevBytes = (size_t)frameWidths[1] * frameHeights[1] * sizeof(uint16_t); + if (prevBytes > sizeof(renderBuffer[0])) prevBytes = sizeof(renderBuffer[0]); + memcpy(renderBuffer[0], renderBuffer[1], prevBytes); + passed[0] = passed[1]; + frameWidths[0] = frameWidths[1]; + frameHeights[0] = frameHeights[1]; + + memcpy(renderBuffer[1], nextFrame, frameBytes); + passed[1] = passed[2]; + frameWidths[1] = frameWidths[2]; + frameHeights[1] = frameHeights[2]; + } + } + } +} + +void DMD::DumpDMDRgb888Thread() +{ + char name[DMDUTIL_MAX_NAME_SIZE] = {0}; + uint16_t bufferPosition = 0; + uint8_t renderBuffer[3][256 * 64 * 3] = {0}; + uint16_t frameWidths[3] = {0}; + uint16_t frameHeights[3] = {0}; + uint32_t passed[3] = {0}; + std::chrono::steady_clock::time_point start; + FILE* f = nullptr; + uint8_t palette[256 * 3] = {0}; + + (void)m_stopFlag.load(std::memory_order_acquire); + m_dump888Active.store(true, std::memory_order_release); + m_dump888Position.store(bufferPosition, std::memory_order_release); + m_dumpPositionCv.notify_all(); + + while (true) + { + std::shared_lock sl(m_dmdSharedMutex); + m_dmdCV.wait(sl, + [&]() + { + return m_stopFlag.load(std::memory_order_relaxed) || + (m_updateBufferQueuePosition.load(std::memory_order_relaxed) != bufferPosition); + }); + sl.unlock(); + if (m_stopFlag.load(std::memory_order_acquire)) + { + if (f) + { + fflush(f); + fclose(f); + f = nullptr; + } + m_dump888Active.store(false, std::memory_order_release); + m_dumpPositionCv.notify_all(); + return; + } + + const uint16_t updateBufferQueuePosition = m_updateBufferQueuePosition.load(std::memory_order_acquire); + while (!m_stopFlag.load(std::memory_order_relaxed) && bufferPosition != updateBufferQueuePosition) + { + // Don't use GetNextBufferPosition() here, we need all frames! + ++bufferPosition; // 65635 + 1 = 0 + uint8_t bufferPositionMod = bufferPosition % DMDUTIL_FRAME_BUFFER_SIZE; + m_dump888Position.store(bufferPosition, std::memory_order_release); + m_dumpPositionCv.notify_all(); + + Update* update = m_pUpdateBufferQueue[bufferPositionMod]; + if (!(update->hasData || update->hasSegData)) continue; + + if (!(update->mode == Mode::RGB24 || update->mode == Mode::RGB16 || update->mode == Mode::SerumV1 || + update->mode == Mode::Vni || IsSerumV2Mode(update->mode))) + continue; + + bool updateFrame = false; + if (strcmp(m_romName, name) != 0) + { + // New game ROM. + start = std::chrono::steady_clock::now(); + if (f) + { + fclose(f); + f = nullptr; + } + strcpy(name, m_romName); + + if (name[0] != '\0') + { + char filename[DMDUTIL_MAX_NAME_SIZE + 128 + 8 + 9]; + char suffix[9]; // 8 chars + null terminator + if (!GetDumpSuffix(name, suffix, sizeof(suffix))) + { + GenerateRandomSuffix(suffix, 8); + } + if (m_dumpPath[0] == '\0') strcpy(m_dumpPath, Config::GetInstance()->GetDumpPath()); + size_t pathLen = strlen(m_dumpPath); + if (pathLen == 0) + { + snprintf(filename, sizeof(filename), "./%s-%s.888.txt", name, suffix); + } + else if (m_dumpPath[pathLen - 1] == '/' || m_dumpPath[pathLen - 1] == '\\') + { + snprintf(filename, sizeof(filename), "%s%s-%s.888.txt", m_dumpPath, name, suffix); + } + else + { + snprintf(filename, sizeof(filename), "%s/%s-%s.888.txt", m_dumpPath, name, suffix); + } + f = fopen(filename, "w"); + updateFrame = true; + memset(renderBuffer, 0, sizeof(renderBuffer)); + memset(frameWidths, 0, sizeof(frameWidths)); + memset(frameHeights, 0, sizeof(frameHeights)); + passed[0] = passed[1] = 0; + } + } + + if (name[0] == '\0') + { + continue; + } + + uint16_t width = update->width; + uint16_t height = update->height; + int length = (int)width * height; + size_t frameBytes = (size_t)length * 3; + if (frameBytes > sizeof(renderBuffer[0])) + { + continue; + } + + if (width != frameWidths[1] || height != frameHeights[1]) + { + updateFrame = true; + } + + uint8_t* nextFrame = renderBuffer[2]; + if (update->mode == Mode::RGB24) + { + if (update->depth != 24) + { + UpdatePalette(palette, update->depth, update->r, update->g, update->b); + } + AdjustRGB24Depth(update->data, nextFrame, length, palette, update->depth); + } + else if (update->mode == Mode::RGB16 || IsSerumV2Mode(update->mode)) + { + const uint16_t* src = update->segData; + for (int i = 0; i < length; i++) + { + uint16_t value = src[i]; + uint8_t r = (uint8_t)((value >> 11) & 0x1F); + uint8_t g = (uint8_t)((value >> 5) & 0x3F); + uint8_t b = (uint8_t)(value & 0x1F); + nextFrame[i * 3] = (uint8_t)((r << 3) | (r >> 2)); + nextFrame[i * 3 + 1] = (uint8_t)((g << 2) | (g >> 4)); + nextFrame[i * 3 + 2] = (uint8_t)((b << 3) | (b >> 2)); + } + } + else + { + size_t paletteBytes = PaletteBytesForDepth((uint8_t)update->depth); + if (paletteBytes > 0 && paletteBytes <= sizeof(palette)) + { + memcpy(palette, update->segData, paletteBytes); + } + FrameUtil::Helper::ConvertToRgb24(nextFrame, update->data, length, palette); + } + + if (updateFrame || memcmp(renderBuffer[1], nextFrame, frameBytes) != 0) + { + uint32_t queuedTimestamp = 0; + if (GetQueueTimestamp(bufferPositionMod, queuedTimestamp)) + { + passed[2] = queuedTimestamp; + } + else + { + passed[2] = (uint32_t)(std::chrono::duration_cast( + std::chrono::steady_clock::now() - start) + .count()); + } + frameWidths[2] = width; + frameHeights[2] = height; + + if (f && passed[0] > 0 && frameWidths[0] > 0 && frameHeights[0] > 0) + { + fprintf(f, "0x%08x\r\n", passed[0]); + uint32_t rowWidth = frameWidths[0]; + uint32_t rowHeight = frameHeights[0]; + const uint8_t* frame = renderBuffer[0]; + for (uint32_t y = 0; y < rowHeight; y++) + { + for (uint32_t x = 0; x < rowWidth; x++) + { + int pos = (int)(y * rowWidth + x) * 3; + fprintf(f, "%02x%02x%02x", frame[pos], frame[pos + 1], frame[pos + 2]); + } + fprintf(f, "\r\n"); + } + fprintf(f, "\r\n"); + } + + size_t prevBytes = (size_t)frameWidths[1] * frameHeights[1] * 3; + if (prevBytes > sizeof(renderBuffer[0])) prevBytes = sizeof(renderBuffer[0]); + memcpy(renderBuffer[0], renderBuffer[1], prevBytes); + passed[0] = passed[1]; + frameWidths[0] = frameWidths[1]; + frameHeights[0] = frameHeights[1]; + + memcpy(renderBuffer[1], nextFrame, frameBytes); + passed[1] = passed[2]; + frameWidths[1] = frameWidths[2]; + frameHeights[1] = frameHeights[2]; + } + } + } +} + void DMD::DumpDMDRawThread() { char name[DMDUTIL_MAX_NAME_SIZE] = {0}; @@ -1933,6 +3111,9 @@ void DMD::DumpDMDRawThread() FILE* f = nullptr; (void)m_stopFlag.load(std::memory_order_acquire); + m_dumpRawActive.store(true, std::memory_order_release); + m_dumpRawPosition.store(bufferPosition, std::memory_order_release); + m_dumpPositionCv.notify_all(); while (true) { @@ -1952,6 +3133,8 @@ void DMD::DumpDMDRawThread() fclose(f); f = nullptr; } + m_dumpRawActive.store(false, std::memory_order_release); + m_dumpPositionCv.notify_all(); return; } @@ -1961,6 +3144,8 @@ void DMD::DumpDMDRawThread() // Don't use GetNextBufferPosition() here, we need all frames! ++bufferPosition; // 65635 + 1 = 0 uint8_t bufferPositionMod = bufferPosition % DMDUTIL_FRAME_BUFFER_SIZE; + m_dumpRawPosition.store(bufferPosition, std::memory_order_release); + m_dumpPositionCv.notify_all(); if (m_pUpdateBufferQueue[bufferPositionMod]->hasData || m_pUpdateBufferQueue[bufferPositionMod]->hasSegData) { @@ -1987,8 +3172,13 @@ void DMD::DumpDMDRawThread() { if (f) { - auto current = - std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); + uint32_t current = 0; + if (!GetQueueTimestamp(bufferPositionMod, current)) + { + current = (uint32_t)std::chrono::duration_cast( + std::chrono::steady_clock::now() - start) + .count(); + } fwrite(¤t, 4, 1, f); uint32_t size = sizeof(m_pUpdateBufferQueue[bufferPositionMod]); diff --git a/src/PIN2DMD.cpp b/src/PIN2DMD.cpp new file mode 100644 index 00000000..c2300b86 --- /dev/null +++ b/src/PIN2DMD.cpp @@ -0,0 +1,170 @@ +#include +#include +#include + +#include "PIN2DMD.h" + +//define PIN2DMD vendor id and product id +constexpr uint16_t kVid = 0x0314; +constexpr uint16_t kPid = 0xe457; + +//endpoints for PIN2DMD communication +constexpr uint8_t kEpIn = 0x81; +constexpr uint8_t kEpOut = 0x01; + +static bool g_PIN2DMD = false; +static bool g_PIN2DMDXL = false; +static bool g_PIN2DMDHD = false; + +static libusb_device** g_devices = nullptr; +static libusb_device_handle* g_deviceHandle = nullptr; +static libusb_device_descriptor g_descriptor; +static libusb_context* g_usbContext = nullptr; + +static uint8_t g_outputBuffer[65536] = {}; + +int PIN2DMDInit() { + static int ret = 0; + static uint8_t product[256] = {}; + static const char* string = nullptr; + + libusb_init(&g_usbContext); /* initialize the library */ + + int device_count = libusb_get_device_list(g_usbContext, &g_devices); + if (device_count < 0) + { + return 0; + } + + //Now look through the list that we just populated. We are trying to see if any of them match our device. + int i; + for (i = 0; i < device_count; i++) { + libusb_get_device_descriptor(g_devices[i], &g_descriptor); + if (kVid == g_descriptor.idVendor && kPid == g_descriptor.idProduct) { + break; + } + } + + if (kVid == g_descriptor.idVendor && kPid == g_descriptor.idProduct) { + ret = libusb_open(g_devices[i], &g_deviceHandle); + if (ret < 0) { + libusb_free_device_list(g_devices, 1); + return ret; + } + } + else { + libusb_free_device_list(g_devices, 1); + return 0; + } + + libusb_free_device_list(g_devices, 1); + + if (g_deviceHandle == nullptr) { + libusb_exit(g_usbContext); + return 0; + } + + ret = libusb_get_string_descriptor_ascii(g_deviceHandle, g_descriptor.iProduct, product, 256); + + if (libusb_claim_interface(g_deviceHandle, 0) < 0) //claims the interface with the Operating System + { + //Closes a device opened since the claim interface is failed. + libusb_close(g_deviceHandle); + g_deviceHandle = nullptr; + libusb_exit(g_usbContext); + return 0; + } + + string = (const char*)product; + if (ret > 0) { + if (strcmp(string, "PIN2DMD") == 0) { + g_PIN2DMD = true; + ret = 1; + } + else if (strcmp(string, "PIN2DMD XL") == 0) { + g_PIN2DMDXL = true; + ret = 2; + } + else if (strcmp(string, "PIN2DMD HD") == 0) { + g_PIN2DMDHD = true; + ret = 3; + } + else { + ret = 0; + } + } + + return ret; +} + +bool PIN2DMDIsConnected() +{ + return (g_PIN2DMD || g_PIN2DMDXL || g_PIN2DMDHD); +} + +uint16_t PIN2DMDGetWidth() +{ + if (g_PIN2DMDHD) return 256; + if (g_PIN2DMDXL) return 192; + if (g_PIN2DMD) return 128; + return 0; +} + +uint16_t PIN2DMDGetHeight() +{ + if (g_PIN2DMDHD) return 64; + if (g_PIN2DMDXL) return 64; + if (g_PIN2DMD) return 32; + return 0; +} + +void PIN2DMDRender(uint16_t width, uint16_t height, uint8_t* buffer, int bitDepth) { + if (!g_deviceHandle) return; + if ( + (width == 256 && height == 64 && g_PIN2DMDHD) || + (width == 192 && height == 64 && (g_PIN2DMDXL || g_PIN2DMDHD)) || + (width == 128 && height <= 32 && (g_PIN2DMD || g_PIN2DMDXL || g_PIN2DMDHD)) + ) { + int frameSizeInByte = width * height / 8; + int chunksOf512Bytes = (frameSizeInByte / 512) * bitDepth; + + g_outputBuffer[0] = 0x81; + g_outputBuffer[1] = 0xc3; + if (bitDepth == 4 && width == 128 && height == 32) { + g_outputBuffer[2] = 0xe7; // 4 bit header + g_outputBuffer[3] = 0x00; + } else { + g_outputBuffer[2] = 0xe8; // non 4 bit header + g_outputBuffer[3] = chunksOf512Bytes; // number of 512 byte chunks + } + + const int payloadSize = chunksOf512Bytes * 512; + if ((payloadSize + 4) > (int)sizeof(g_outputBuffer)) return; + memcpy(&g_outputBuffer[4], buffer, payloadSize); + + // The OutputBuffer to be sent consists of a 4 byte header and a number of chunks of 512 bytes. + libusb_bulk_transfer(g_deviceHandle, kEpOut, g_outputBuffer, payloadSize + 4, nullptr, 1000); + } +} + +void PIN2DMDRenderRaw(uint16_t width, uint16_t height, uint8_t* buffer, uint32_t frames) { + if (!g_deviceHandle) return; + if ( + (width == 256 && height == 64 && g_PIN2DMDHD) || + (width == 192 && height == 64 && (g_PIN2DMDXL || g_PIN2DMDHD)) || + (width == 128 && height <= 32 && (g_PIN2DMD || g_PIN2DMDXL || g_PIN2DMDHD)) + ) { + int frameSizeInByte = width * height * 3; + int chunksOf512Bytes = (frameSizeInByte / 512) * frames; + int bufferSizeInBytes = frameSizeInByte * frames; + if ((bufferSizeInBytes + 4) > (int)sizeof(g_outputBuffer)) return; + + g_outputBuffer[0] = 0x52; // RAW mode + g_outputBuffer[1] = 0x80; + g_outputBuffer[2] = 0x20; + g_outputBuffer[3] = chunksOf512Bytes; // number of 512 byte chunks + memcpy(&g_outputBuffer[4], buffer, bufferSizeInBytes); + + libusb_bulk_transfer(g_deviceHandle, kEpOut, g_outputBuffer, bufferSizeInBytes + 4, nullptr, 1000); + } +} diff --git a/src/PIN2DMD.h b/src/PIN2DMD.h new file mode 100644 index 00000000..e5706556 --- /dev/null +++ b/src/PIN2DMD.h @@ -0,0 +1,13 @@ +#ifndef PIN2DMD_H +#define PIN2DMD_H + +#include + +int PIN2DMDInit(); +bool PIN2DMDIsConnected(); +uint16_t PIN2DMDGetWidth(); +uint16_t PIN2DMDGetHeight(); +void PIN2DMDRender(uint16_t width, uint16_t height, uint8_t* buffer, int bitDepth); +void PIN2DMDRenderRaw(uint16_t width, uint16_t height, uint8_t* buffer, uint32_t frames); + +#endif /* PIN2DMD_H */ diff --git a/src/playDump.cpp b/src/playDump.cpp new file mode 100644 index 00000000..aeb0b27f --- /dev/null +++ b/src/playDump.cpp @@ -0,0 +1,983 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DMDUtil/DMDUtil.h" +#include "cargs.h" + +namespace +{ +std::atomic g_stopRequested{false}; + +void HandleSigInt(int) +{ + g_stopRequested.store(true, std::memory_order_release); +} + +enum class FrameFormat +{ + Indexed, + RGB565, + RGB888 +}; + +struct Frame +{ + uint32_t timestampMs = 0; + uint32_t originalTimestampMs = 0; + uint32_t durationMs = 0; + uint16_t width = 0; + uint16_t height = 0; + FrameFormat format = FrameFormat::Indexed; + std::vector data; + std::vector data16; +}; + +static bool EndsWithCaseInsensitive(const std::string& value, const std::string& suffix) +{ + if (suffix.size() > value.size()) return false; + const size_t offset = value.size() - suffix.size(); + for (size_t i = 0; i < suffix.size(); ++i) + { + char a = value[offset + i]; + char b = suffix[i]; + if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a'); + if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a'); + if (a != b) return false; + } + return true; +} + +static std::string GetBaseName(const std::string& path) +{ + size_t pos = path.find_last_of("/\\"); + if (pos == std::string::npos) return path; + return path.substr(pos + 1); +} + +static std::string StripExtension(const std::string& name) +{ + size_t pos = name.find_last_of('.'); + if (pos == std::string::npos) return name; + return name.substr(0, pos); +} + +static int HexToInt(char ch) +{ + if (ch >= '0' && ch <= '9') return ch - '0'; + if (ch >= 'a' && ch <= 'f') return 10 + (ch - 'a'); + if (ch >= 'A' && ch <= 'F') return 10 + (ch - 'A'); + return -1; +} + +static uint8_t ScaleIndex(uint8_t value, uint8_t inDepth, uint8_t outDepth) +{ + if (inDepth == outDepth) return value; + if (inDepth == 0 || outDepth == 0) return 0; + + const int maxIn = (1 << inDepth) - 1; + const int maxOut = (1 << outDepth) - 1; + if (maxIn <= 0) return 0; + + int scaled = (value * maxOut + (maxIn / 2)) / maxIn; + if (scaled < 0) scaled = 0; + if (scaled > maxOut) scaled = maxOut; + return (uint8_t)scaled; +} + +static uint8_t QuantizeToDepth(uint8_t r, uint8_t g, uint8_t b, uint8_t depth) +{ + int v = (int)(0.2126f * (float)r + 0.7152f * (float)g + 0.0722f * (float)b); + if (v > 255) v = 255; + return (depth == 2) ? (uint8_t)(v >> 6) : (uint8_t)(v >> 4); +} + +static bool LoadTxtDump(const std::string& path, uint8_t outDepth, std::vector& frames) +{ + std::ifstream file(path); + if (!file) + { + std::cerr << "Error: Unable to open input file: " << path << "\n"; + return false; + } + + std::string line; + Frame current; + bool inFrame = false; + uint16_t width = 0; + uint16_t height = 0; + int maxValue = 0; + + auto finalizeFrame = [&]() + { + if (!inFrame) return; + if (width == 0 || height == 0) + { + inFrame = false; + return; + } + + uint8_t inDepth = (maxValue <= 3) ? 2 : 4; + if (maxValue > 15) + { + std::cerr << "Error: Invalid pixel value in txt dump\n"; + inFrame = false; + return; + } + + if (inDepth != outDepth) + { + for (size_t i = 0; i < current.data.size(); ++i) + { + current.data[i] = ScaleIndex(current.data[i], inDepth, outDepth); + } + } + + current.width = width; + current.height = height; + frames.push_back(current); + current = Frame(); + inFrame = false; + }; + + while (std::getline(file, line)) + { + if (!line.empty() && line.back() == '\r') line.pop_back(); + + if (line.empty()) + { + finalizeFrame(); + width = 0; + height = 0; + maxValue = 0; + continue; + } + + if (!inFrame) + { + if (line.rfind("0x", 0) != 0 && line.rfind("0X", 0) != 0) + { + continue; + } + current = Frame(); + current.timestampMs = (uint32_t)strtoul(line.c_str(), nullptr, 16); + current.originalTimestampMs = current.timestampMs; + inFrame = true; + width = 0; + height = 0; + maxValue = 0; + continue; + } + + if (width == 0) + { + width = (uint16_t)line.size(); + } + else if (line.size() != width) + { + std::cerr << "Error: Inconsistent line width in txt dump\n"; + return false; + } + + for (char ch : line) + { + int value = HexToInt(ch); + if (value < 0) + { + std::cerr << "Error: Invalid hex digit in txt dump\n"; + return false; + } + current.data.push_back((uint8_t)value); + if (value > maxValue) maxValue = value; + } + height++; + } + + finalizeFrame(); + + if (frames.empty()) + { + std::cerr << "Error: No frames found in txt dump\n"; + return false; + } + + return true; +} + +static bool ParseHexUint16(const std::string& line, size_t pos, uint16_t& value) +{ + if (pos + 4 > line.size()) return false; + int h0 = HexToInt(line[pos]); + int h1 = HexToInt(line[pos + 1]); + int h2 = HexToInt(line[pos + 2]); + int h3 = HexToInt(line[pos + 3]); + if (h0 < 0 || h1 < 0 || h2 < 0 || h3 < 0) return false; + value = (uint16_t)((h0 << 12) | (h1 << 8) | (h2 << 4) | h3); + return true; +} + +static bool ParseHexRgb24(const std::string& line, size_t pos, uint8_t& r, uint8_t& g, uint8_t& b) +{ + if (pos + 6 > line.size()) return false; + int h0 = HexToInt(line[pos]); + int h1 = HexToInt(line[pos + 1]); + int h2 = HexToInt(line[pos + 2]); + int h3 = HexToInt(line[pos + 3]); + int h4 = HexToInt(line[pos + 4]); + int h5 = HexToInt(line[pos + 5]); + if (h0 < 0 || h1 < 0 || h2 < 0 || h3 < 0 || h4 < 0 || h5 < 0) return false; + r = (uint8_t)((h0 << 4) | h1); + g = (uint8_t)((h2 << 4) | h3); + b = (uint8_t)((h4 << 4) | h5); + return true; +} + +static bool LoadRgb565Dump(const std::string& path, uint8_t outDepth, std::vector& frames) +{ + (void)outDepth; + std::ifstream file(path); + if (!file) + { + std::cerr << "Error: Unable to open input file: " << path << "\n"; + return false; + } + + std::string line; + Frame current; + bool inFrame = false; + uint16_t width = 0; + uint16_t height = 0; + + auto finalizeFrame = [&]() + { + if (!inFrame) return; + if (width == 0 || height == 0) + { + inFrame = false; + return; + } + + current.width = width; + current.height = height; + current.format = FrameFormat::RGB565; + frames.push_back(current); + current = Frame(); + inFrame = false; + }; + + while (std::getline(file, line)) + { + if (!line.empty() && line.back() == '\r') line.pop_back(); + + if (line.empty()) + { + finalizeFrame(); + width = 0; + height = 0; + continue; + } + + if (!inFrame) + { + if (line.rfind("0x", 0) != 0 && line.rfind("0X", 0) != 0) + { + continue; + } + current = Frame(); + current.timestampMs = (uint32_t)strtoul(line.c_str(), nullptr, 16); + current.originalTimestampMs = current.timestampMs; + inFrame = true; + width = 0; + height = 0; + continue; + } + + if ((line.size() % 4) != 0) + { + std::cerr << "Error: Invalid line width in rgb565 dump\n"; + return false; + } + + uint16_t lineWidth = (uint16_t)(line.size() / 4); + if (width == 0) + { + width = lineWidth; + } + else if (lineWidth != width) + { + std::cerr << "Error: Inconsistent line width in rgb565 dump\n"; + return false; + } + + for (size_t pos = 0; pos < line.size(); pos += 4) + { + uint16_t value = 0; + if (!ParseHexUint16(line, pos, value)) + { + std::cerr << "Error: Invalid hex digit in rgb565 dump\n"; + return false; + } + current.data16.push_back(value); + } + height++; + } + + finalizeFrame(); + + if (frames.empty()) + { + std::cerr << "Error: No frames found in rgb565 dump\n"; + return false; + } + + return true; +} + +static bool LoadRgb888Dump(const std::string& path, uint8_t outDepth, std::vector& frames) +{ + (void)outDepth; + std::ifstream file(path); + if (!file) + { + std::cerr << "Error: Unable to open input file: " << path << "\n"; + return false; + } + + std::string line; + Frame current; + bool inFrame = false; + uint16_t width = 0; + uint16_t height = 0; + + auto finalizeFrame = [&]() + { + if (!inFrame) return; + if (width == 0 || height == 0) + { + inFrame = false; + return; + } + + current.width = width; + current.height = height; + current.format = FrameFormat::RGB888; + frames.push_back(current); + current = Frame(); + inFrame = false; + }; + + while (std::getline(file, line)) + { + if (!line.empty() && line.back() == '\r') line.pop_back(); + + if (line.empty()) + { + finalizeFrame(); + width = 0; + height = 0; + continue; + } + + if (!inFrame) + { + if (line.rfind("0x", 0) != 0 && line.rfind("0X", 0) != 0) + { + continue; + } + current = Frame(); + current.timestampMs = (uint32_t)strtoul(line.c_str(), nullptr, 16); + current.originalTimestampMs = current.timestampMs; + inFrame = true; + width = 0; + height = 0; + continue; + } + + if ((line.size() % 6) != 0) + { + std::cerr << "Error: Invalid line width in rgb888 dump\n"; + return false; + } + + uint16_t lineWidth = (uint16_t)(line.size() / 6); + if (width == 0) + { + width = lineWidth; + } + else if (lineWidth != width) + { + std::cerr << "Error: Inconsistent line width in rgb888 dump\n"; + return false; + } + + for (size_t pos = 0; pos < line.size(); pos += 6) + { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + if (!ParseHexRgb24(line, pos, r, g, b)) + { + std::cerr << "Error: Invalid hex digit in rgb888 dump\n"; + return false; + } + current.data.push_back(r); + current.data.push_back(g); + current.data.push_back(b); + } + height++; + } + + finalizeFrame(); + + if (frames.empty()) + { + std::cerr << "Error: No frames found in rgb888 dump\n"; + return false; + } + + return true; +} + +static void FinalizeFrameDurations(std::vector& frames) +{ + if (frames.empty()) return; + + bool monotonic = true; + for (size_t i = 1; i < frames.size(); ++i) + { + if (frames[i].timestampMs < frames[i - 1].timestampMs) + { + monotonic = false; + break; + } + } + + if (!monotonic) + { + uint32_t accumulated = 0; + for (Frame& frame : frames) + { + frame.durationMs = frame.timestampMs; + frame.originalTimestampMs = accumulated; + accumulated += frame.durationMs; + } + return; + } + + for (size_t i = 0; i + 1 < frames.size(); ++i) + { + uint32_t curr = frames[i].timestampMs; + uint32_t next = frames[i + 1].timestampMs; + frames[i].durationMs = (next > curr) ? (next - curr) : 0; + frames[i].originalTimestampMs = curr; + } + + if (frames.size() > 1) + { + frames.back().durationMs = frames[frames.size() - 2].durationMs; + } + frames.back().originalTimestampMs = frames.back().timestampMs; +} + +static bool ConvertUpdateToIndexed(const DMDUtil::DMD::Update& update, uint8_t outDepth, Frame& frame) +{ + using Mode = DMDUtil::DMD::Mode; + frame.width = update.width; + frame.height = update.height; + frame.format = FrameFormat::Indexed; + frame.data16.clear(); + if (frame.width == 0 || frame.height == 0) return false; + + const int length = (int)frame.width * frame.height; + frame.data.assign(length, 0); + + switch (update.mode) + { + case Mode::Data: + case Mode::NotColorized: + { + if (!update.hasData) return false; + uint8_t inDepth = (uint8_t)update.depth; + if (inDepth == 0 || inDepth > 8) return false; + for (int i = 0; i < length; ++i) + { + frame.data[i] = ScaleIndex(update.data[i], inDepth, outDepth); + } + return true; + } + case Mode::RGB24: + { + if (!update.hasData) return false; + for (int i = 0; i < length; ++i) + { + int pos = i * 3; + uint8_t r = update.data[pos]; + uint8_t g = update.data[pos + 1]; + uint8_t b = update.data[pos + 2]; + frame.data[i] = QuantizeToDepth(r, g, b, outDepth); + } + return true; + } + case Mode::RGB16: + case Mode::SerumV2_32: + case Mode::SerumV2_32_64: + case Mode::SerumV2_64: + case Mode::SerumV2_64_32: + { + const uint16_t* src = update.segData; + for (int i = 0; i < length; ++i) + { + uint16_t value = src[i]; + uint8_t r = (uint8_t)((value >> 11) & 0x1F); + uint8_t g = (uint8_t)((value >> 5) & 0x3F); + uint8_t b = (uint8_t)(value & 0x1F); + r = (uint8_t)((r << 3) | (r >> 2)); + g = (uint8_t)((g << 2) | (g >> 4)); + b = (uint8_t)((b << 3) | (b >> 2)); + frame.data[i] = QuantizeToDepth(r, g, b, outDepth); + } + return true; + } + case Mode::SerumV1: + case Mode::Vni: + { + if (!update.hasData) return false; + uint8_t inDepth = (uint8_t)update.depth; + if (inDepth == 0 || inDepth > 8) return false; + int colors = 1 << inDepth; + const uint8_t* palette = reinterpret_cast(update.segData); + for (int i = 0; i < length; ++i) + { + uint8_t idx = update.data[i]; + if (idx >= colors) idx = (uint8_t)(colors - 1); + int pos = idx * 3; + uint8_t r = palette[pos]; + uint8_t g = palette[pos + 1]; + uint8_t b = palette[pos + 2]; + frame.data[i] = QuantizeToDepth(r, g, b, outDepth); + } + return true; + } + default: + return false; + } +} + +static bool LoadRawDump(const std::string& path, uint8_t outDepth, std::vector& frames) +{ + std::ifstream file(path, std::ios::binary); + if (!file) + { + std::cerr << "Error: Unable to open input file: " << path << "\n"; + return false; + } + + while (true) + { + uint32_t timestamp = 0; + uint32_t size = 0; + if (!file.read(reinterpret_cast(×tamp), sizeof(timestamp))) break; + if (!file.read(reinterpret_cast(&size), sizeof(size))) break; + + if (size < sizeof(DMDUtil::DMD::Update)) + { + std::cerr << "Error: Raw dump frame size is too small\n"; + return false; + } + + std::vector buffer(size); + if (!file.read(reinterpret_cast(buffer.data()), size)) break; + + DMDUtil::DMD::Update update; + memcpy(&update, buffer.data(), sizeof(update)); + + Frame frame; + frame.timestampMs = timestamp; + frame.originalTimestampMs = timestamp; + if (ConvertUpdateToIndexed(update, outDepth, frame)) + { + frames.push_back(std::move(frame)); + } + } + + if (frames.empty()) + { + std::cerr << "Error: No frames found in raw dump\n"; + return false; + } + + return true; +} + +static bool ParseServer(const std::string& value, std::string& host, int& port) +{ + if (value.empty()) return false; + size_t colon = value.find_last_of(':'); + if (colon == std::string::npos) + { + host = value; + port = 6789; + return true; + } + + host = value.substr(0, colon); + std::string portStr = value.substr(colon + 1); + if (portStr.empty()) + { + port = 6789; + return true; + } + + port = atoi(portStr.c_str()); + if (port <= 0) port = 6789; + return true; +} +} // namespace + +static struct cag_option options[] = { + {.identifier = 'i', + .access_letters = "i", + .access_name = "input", + .value_name = "FILE", + .description = "Input dump file (.txt or .raw)"}, + {.identifier = 'a', + .access_letters = "a", + .access_name = "alt-color-path", + .value_name = "PATH", + .description = "Alt color base path (optional, enables Serum colorization)"}, + {.identifier = 'd', + .access_letters = "d", + .access_name = "depth", + .value_name = "VALUE", + .description = "Bit depth to send (2 or 4) (optional, default is 2)"}, + {.identifier = 's', + .access_letters = "s", + .access_name = "server", + .value_name = "HOST[:PORT]", + .description = "Connect to a DMD server (optional)"}, + {.identifier = 'L', .access_letters = "L", .access_name = "no-local", .description = "Disable local displays"}, + {.identifier = 't', .access_letters = "t", .access_name = "dump-txt", .description = "Dump txt while playing"}, + {.identifier = '5', .access_letters = "5", .access_name = "dump-565", .description = "Dump rgb565 while playing"}, + {.identifier = '8', .access_letters = "8", .access_name = "dump-888", .description = "Dump rgb888 while playing"}, + {.identifier = 'w', + .access_letters = "w", + .access_name = "delay-ms", + .value_name = "MS", + .description = "Fixed delay between frames in milliseconds (optional, default is 100)"}, + {.identifier = 'o', + .access_letters = "o", + .access_name = "dump-path", + .value_name = "PATH", + .description = "Output path for dumps (optional)"}, + {.identifier = 'r', + .access_letters = "r", + .access_name = "rom", + .value_name = "NAME", + .description = "ROM name for dumps (optional)"}, + {.identifier = 'R', .access_letters = "R", .access_name = "raw", .description = "Force raw dump parsing"}, + {.identifier = 'h', .access_letters = "h", .access_name = "help", .description = "Show help"}}; + +int main(int argc, char* argv[]) +{ + char identifier; + cag_option_context cag_context; + + const char* opt_input = nullptr; + const char* opt_alt_color_path = nullptr; + const char* opt_server = nullptr; + const char* opt_dump_path = nullptr; + const char* opt_rom = nullptr; + uint8_t opt_depth = 2; + bool opt_no_local = false; + bool opt_dump_txt = false; + bool opt_dump_565 = false; + bool opt_dump_888 = false; + bool opt_force_raw = false; + uint32_t opt_delay_ms = 100; + bool opt_delay_set = true; + + cag_option_init(&cag_context, options, CAG_ARRAY_SIZE(options), argc, argv); + while (cag_option_fetch(&cag_context)) + { + identifier = cag_option_get_identifier(&cag_context); + switch (identifier) + { + case 'i': + opt_input = cag_option_get_value(&cag_context); + break; + case 'a': + opt_alt_color_path = cag_option_get_value(&cag_context); + break; + case 'd': + opt_depth = (uint8_t)atoi(cag_option_get_value(&cag_context)); + break; + case 's': + opt_server = cag_option_get_value(&cag_context); + break; + case 'L': + opt_no_local = true; + break; + case 't': + opt_dump_txt = true; + break; + case '5': + opt_dump_565 = true; + break; + case '8': + opt_dump_888 = true; + break; + case 'w': + { + const char* valueStr = cag_option_get_value(&cag_context); + if (valueStr) + { + int value = atoi(valueStr); + if (value >= 0) opt_delay_ms = (uint32_t)value; + } + break; + } + case 'o': + opt_dump_path = cag_option_get_value(&cag_context); + break; + case 'r': + opt_rom = cag_option_get_value(&cag_context); + break; + case 'R': + opt_force_raw = true; + break; + case 'h': + std::cerr << "Usage: " << argv[0] << " [OPTION]...\n"; + cag_option_print(options, CAG_ARRAY_SIZE(options), stdout); + return 0; + } + } + + std::signal(SIGINT, HandleSigInt); + + if (!opt_input) + { + std::cerr << "Error: Missing input file\n"; + std::cerr << "Usage: " << argv[0] << " -i MY_DUMP_FILE.txt\n"; + cag_option_print(options, CAG_ARRAY_SIZE(options), stdout); + return 1; + } + + if (opt_depth != 2 && opt_depth != 4) + { + std::cerr << "Error: Depth must be 2 or 4\n"; + return 1; + } + + std::string inputPath = opt_input; + enum class InputFormat + { + Txt, + Raw, + Rgb565, + Rgb888 + }; + InputFormat format = InputFormat::Txt; + if (opt_force_raw || EndsWithCaseInsensitive(inputPath, ".raw")) + { + format = InputFormat::Raw; + } + else if (EndsWithCaseInsensitive(inputPath, ".565.txt")) + { + format = InputFormat::Rgb565; + } + else if (EndsWithCaseInsensitive(inputPath, ".888.txt")) + { + format = InputFormat::Rgb888; + } + + std::vector frames; + switch (format) + { + case InputFormat::Raw: + if (!LoadRawDump(inputPath, opt_depth, frames)) return 1; + break; + case InputFormat::Rgb565: + if (!LoadRgb565Dump(inputPath, opt_depth, frames)) return 1; + break; + case InputFormat::Rgb888: + if (!LoadRgb888Dump(inputPath, opt_depth, frames)) return 1; + break; + case InputFormat::Txt: + default: + if (!LoadTxtDump(inputPath, opt_depth, frames)) return 1; + break; + } + FinalizeFrameDurations(frames); + + std::string romName; + if (opt_rom && opt_rom[0] != '\0') + { + romName = opt_rom; + } + else + { + romName = StripExtension(GetBaseName(inputPath)); + } + + if (romName.empty()) romName = "dump"; + if (romName.size() > DMDUTIL_MAX_NAME_SIZE - 1) + { + romName.resize(DMDUTIL_MAX_NAME_SIZE - 1); + } + + DMDUtil::Config* config = DMDUtil::Config::GetInstance(); + if (opt_alt_color_path && opt_alt_color_path[0] != '\0') + { + config->SetAltColor(true); + config->SetAltColorPath(opt_alt_color_path); + } + if (opt_server && opt_server[0] != '\0') + { + std::string host; + int port = 6789; + ParseServer(opt_server, host, port); + config->SetDMDServer(true); + config->SetDMDServerAddr(host.c_str()); + config->SetDMDServerPort(port); + config->SetLocalDisplaysActive(!opt_no_local); + } + else if (opt_no_local) + { + config->SetLocalDisplaysActive(false); + } + + DMDUtil::DMD dmd; + dmd.SetRomName(romName.c_str()); + if (opt_alt_color_path && opt_alt_color_path[0] != '\0') + { + dmd.SetAltColorPath(opt_alt_color_path); + } + dmd.FindDisplays(); + + for (int i = 0; i < 100 && DMDUtil::DMD::IsFinding(); ++i) + { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + if (opt_dump_txt || opt_dump_565 || opt_dump_888) + { + if (opt_dump_path && opt_dump_path[0] != '\0') + { + config->SetDumpPath(opt_dump_path); + } + if (opt_dump_txt) dmd.DumpDMDTxt(); + if (opt_dump_565) dmd.DumpDMDRgb565(); + if (opt_dump_888) dmd.DumpDMDRgb888(); + } + + const uint8_t dumpR = 255; + const uint8_t dumpG = 69; + const uint8_t dumpB = 0; + const bool dumpEnabled = opt_dump_txt || opt_dump_565 || opt_dump_888; + + if (opt_delay_ms < 100) + { + opt_delay_ms = 100; + } + + for (const Frame& frame : frames) + { + if (g_stopRequested.load(std::memory_order_acquire)) + { + break; + } + const uint32_t queueTimestamp = frame.originalTimestampMs; + + if (frame.format == FrameFormat::RGB565) + { + if (!frame.data16.empty()) + { + dmd.UpdateRGB16DataWithTimestamp(frame.data16.data(), frame.width, frame.height, queueTimestamp, false); + } + } + else if (frame.format == FrameFormat::RGB888) + { + if (!frame.data.empty()) + { + dmd.UpdateRGB24DataWithTimestamp(frame.data.data(), frame.width, frame.height, queueTimestamp, false); + } + } + else if (!frame.data.empty()) + { + dmd.UpdateDataWithTimestamp(frame.data.data(), opt_depth, frame.width, frame.height, dumpR, dumpG, dumpB, + queueTimestamp, false); + } + + if (dumpEnabled) + { + bool settled = false; + while (!g_stopRequested.load(std::memory_order_acquire) && !settled) + { + uint16_t target = dmd.GetUpdateQueuePosition(); + while (!g_stopRequested.load(std::memory_order_acquire) && !dmd.WaitForDumpers(target, 0)) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + settled = true; + auto quietStart = std::chrono::steady_clock::now(); + while (!g_stopRequested.load(std::memory_order_acquire)) + { + if (dmd.GetUpdateQueuePosition() != target) + { + settled = false; + break; + } + if (std::chrono::steady_clock::now() - quietStart >= std::chrono::milliseconds(5)) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + } + + uint32_t sleepMs = 0; + if (opt_delay_set) + { + sleepMs = opt_delay_ms; + if (frame.durationMs > 0 && frame.durationMs < sleepMs) + { + sleepMs = frame.durationMs; + } + } + else + { + sleepMs = frame.durationMs; + } + + if (sleepMs > 0) + { + std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs)); + } + } + + if (g_stopRequested.load(std::memory_order_acquire)) + { + uint16_t target = dmd.GetUpdateQueuePosition(); + for (int i = 0; i < 10; ++i) + { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + uint16_t current = dmd.GetUpdateQueuePosition(); + if (current == target) break; + target = current; + } + dmd.WaitForDumpers(target, 2000); + } + + return 0; +} diff --git a/third-party/include/FrameUtil.h b/third-party/include/FrameUtil.h index cffcf5d5..c4616afb 100644 --- a/third-party/include/FrameUtil.h +++ b/third-party/include/FrameUtil.h @@ -30,6 +30,88 @@ namespace FrameUtil { +inline constexpr uint32_t kChecksumTable[256] = { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, + 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, + 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, + 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, + 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, + 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, + 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, + 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, + 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, + 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, + 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, + 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, + 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, + 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, + 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, + 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, + 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, + 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, + 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, + 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, + 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, + 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, + 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, + 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, + 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, + 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, + 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, + 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D +}; + +inline constexpr uint8_t kReverseByte[256] = { + 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, + 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0, + 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, + 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8, + 0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, + 0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4, + 0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, + 0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc, + 0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, + 0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2, + 0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, + 0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa, + 0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, + 0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6, + 0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee, + 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe, + 0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1, + 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1, + 0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9, + 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9, + 0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5, + 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5, + 0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed, + 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd, + 0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3, + 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3, + 0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, + 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb, + 0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7, + 0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7, + 0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, + 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff +}; + + enum class ColorMatrix { Rgb, @@ -57,6 +139,18 @@ class Helper uint8_t bits); static void CenterIndexed(uint8_t* pDestFrame, const uint16_t destWidth, const uint8_t destHeight, const uint8_t* pSrcFrame, const uint16_t srcWidth, const uint8_t srcHeight); + static void Join(uint8_t* pFrame, uint16_t width, uint16_t height, uint8_t bitlen, const uint8_t* pPlanes); + static std::vector NewPlane(uint16_t width, uint16_t height); + static void ClearPlane(uint8_t* plane, size_t len); + static void OrPlane(const uint8_t* plane, uint8_t* target, size_t len); + static void CombinePlaneWithMask(const uint8_t* planeA, const uint8_t* planeB, const uint8_t* mask, uint8_t* out, + size_t len); + static uint8_t ReverseByte(uint8_t value); + static uint32_t Checksum(const uint8_t* input, size_t len, bool reverse = false); + static uint32_t ChecksumWithMask(const uint8_t* input, const uint8_t* mask, size_t len, bool reverse = false); + static void Scale2XIndexed(uint8_t* pDestFrame, const uint8_t* pSrcFrame, uint16_t srcWidth, uint16_t srcHeight); + static void ScaleDoubleIndexed(uint8_t* pDestFrame, const uint8_t* pSrcFrame, uint16_t srcWidth, uint16_t srcHeight); + static void Center(uint8_t* pDestFrame, const uint16_t destWidth, const uint8_t destHeight, const uint8_t* pSrcFrame, const uint16_t srcWidth, const uint8_t srcHeight, uint8_t bits); }; @@ -544,4 +638,175 @@ inline void Helper::CenterIndexed(uint8_t* pDestFrame, const uint16_t destWidth, Center(pDestFrame, destWidth, destHeight, pSrcFrame, srcWidth, srcHeight, 8); } +inline uint8_t Helper::ReverseByte(uint8_t value) +{ + return kReverseByte[value]; +} + +inline uint32_t Helper::Checksum(const uint8_t* input, size_t len, bool reverse) +{ + uint32_t cs = 0xFFFFFFFFu; + if (!reverse) + { + for (size_t i = 0; i < len; i++) + { + cs = (cs >> 8) ^ kChecksumTable[(cs ^ input[i]) & 0xFFu]; + } + } + else + { + for (size_t i = 0; i < len; i++) + { + cs = (cs >> 8) ^ kChecksumTable[(cs ^ kReverseByte[input[i]]) & 0xFFu]; + } + } + return cs ^ 0xFFFFFFFFu; +} + +inline uint32_t Helper::ChecksumWithMask(const uint8_t* input, const uint8_t* mask, size_t len, bool reverse) +{ + uint32_t cs = 0xFFFFFFFFu; + if (!reverse) + { + for (size_t i = 0; i < len; i++) + { + cs = (cs >> 8) ^ kChecksumTable[(cs ^ (input[i] & mask[i])) & 0xFFu]; + } + } + else + { + for (size_t i = 0; i < len; i++) + { + cs = (cs >> 8) ^ kChecksumTable[(cs ^ (kReverseByte[input[i]] & mask[i])) & 0xFFu]; + } + } + return cs ^ 0xFFFFFFFFu; +} + +inline void Helper::Join(uint8_t* pFrame, uint16_t width, uint16_t height, uint8_t bitlen, const uint8_t* pPlanes) +{ + int planeSize = width * height / 8; + std::vector planes(bitlen, nullptr); + for (uint8_t i = 0; i < bitlen; i++) + { + planes[i] = pPlanes + i * planeSize; + } + + int byteIdx = 0; + uint8_t andValue = 1; + int total = width * height; + for (int i = 0; i < total; i++) + { + uint8_t value = 0; + for (uint8_t p = 0; p < bitlen; p++) + { + if (planes[p][byteIdx] & andValue) + { + value |= static_cast(1u << p); + } + } + pFrame[i] = value; + + if (andValue == 0x80) + { + andValue = 0x01; + byteIdx++; + } + else + { + andValue <<= 1; + } + } +} + +inline std::vector Helper::NewPlane(uint16_t width, uint16_t height) +{ + size_t count = static_cast(width / 8) * height; + return std::vector(count); +} + +inline void Helper::ClearPlane(uint8_t* plane, size_t len) +{ + memset(plane, 0, len); +} + +inline void Helper::OrPlane(const uint8_t* plane, uint8_t* target, size_t len) +{ + for (size_t i = 0; i < len; i++) + { + target[i] = static_cast(target[i] | plane[i]); + } +} + +inline void Helper::CombinePlaneWithMask(const uint8_t* planeA, const uint8_t* planeB, const uint8_t* mask, + uint8_t* out, size_t len) +{ + for (size_t i = 0; i < len; i++) + { + out[i] = static_cast((planeA[i] & mask[i]) | (planeB[i] & ~mask[i])); + } +} + +inline void Helper::ScaleDoubleIndexed(uint8_t* pDestFrame, const uint8_t* pSrcFrame, uint16_t srcWidth, uint16_t srcHeight) +{ + uint16_t destWidth = srcWidth * 2; + uint16_t destHeight = srcHeight * 2; + for (uint16_t y = 0; y < srcHeight; y++) + { + for (uint16_t x = 0; x < srcWidth; x++) + { + uint8_t value = pSrcFrame[y * srcWidth + x]; + uint16_t outX = x * 2; + uint16_t outY = y * 2; + pDestFrame[outY * destWidth + outX] = value; + pDestFrame[outY * destWidth + outX + 1] = value; + pDestFrame[(outY + 1) * destWidth + outX] = value; + pDestFrame[(outY + 1) * destWidth + outX + 1] = value; + } + } +} + +inline void Helper::Scale2XIndexed(uint8_t* pDestFrame, const uint8_t* pSrcFrame, uint16_t srcWidth, uint16_t srcHeight) +{ + auto getPixel = [&](int x, int y) -> uint8_t { + if (x < 0) x = 0; + if (y < 0) y = 0; + if (x >= srcWidth) x = srcWidth - 1; + if (y >= srcHeight) y = srcHeight - 1; + return pSrcFrame[y * srcWidth + x]; + }; + + uint16_t destWidth = srcWidth * 2; + for (uint16_t y = 0; y < srcHeight; y++) + { + for (uint16_t x = 0; x < srcWidth; x++) + { + uint8_t b = getPixel(x, static_cast(y) - 1); + uint8_t h = getPixel(x, static_cast(y) + 1); + uint8_t d = getPixel(static_cast(x) - 1, y); + uint8_t f = getPixel(static_cast(x) + 1, y); + uint8_t e = getPixel(x, y); + + uint8_t e0 = e; + uint8_t e1 = e; + uint8_t e2 = e; + uint8_t e3 = e; + if (b != h && d != f) + { + e0 = (d == b) ? d : e; + e1 = (b == f) ? f : e; + e2 = (d == h) ? d : e; + e3 = (h == f) ? f : e; + } + + uint16_t outX = x * 2; + uint16_t outY = y * 2; + pDestFrame[outY * destWidth + outX] = e0; + pDestFrame[outY * destWidth + outX + 1] = e1; + pDestFrame[(outY + 1) * destWidth + outX] = e2; + pDestFrame[(outY + 1) * destWidth + outX + 1] = e3; + } + } +} + } // namespace FrameUtil