From e3d4471b5083d814172987ae8ff8ada14a8424e0 Mon Sep 17 00:00:00 2001 From: reymondmeking-dot Date: Wed, 9 Sep 2026 21:51:29 +0800 Subject: [PATCH 1/2] Add native Windows builds, WGL viewer, and ABI-checked Python bindings --- .github/workflows/windows.yml | 32 +++ CMakeLists.txt | 235 +++++++++++---- README.md | 49 ++-- cmake/StageRuntime.cmake | 7 + knowledge/DEPENDENCIES.md | 8 +- knowledge/WINDOWS.md | 270 ++++++++++++++++++ knowledge/WINDOWS_VALIDATION.md | 124 ++++++++ knowledge/WSL.md | 2 +- python/fast_sam_3dbody_dump_csv.py | 62 +--- .../fast_sam_3dbody_dump_dpose_compat_csv.py | 69 +---- python/fast_sam_3dbody_frontend-3D.py | 62 +--- python/fast_sam_3dbody_frontend.py | 82 +----- python/fsb_ctypes.py | 163 +++++++++++ python/ros_demo_webcam.py | 64 +---- scripts/build_windows.ps1 | 42 +++ src/AmMatrix/matrix4x4Tools.c | 4 +- src/AmMatrix/matrix4x4Tools.h | 9 +- .../ModelLoader/model_loader_tri.c | 2 +- .../calculate/bvh_to_tri_pose.c | 5 +- .../MotionCaptureLoader/edit/bvh_cut_paste.c | 4 +- .../edit/cTextFileToMemory.h | 2 +- .../MotionCaptureLoader/import/fromBVH.c | 15 +- src/GraphicsEngine/System/portable_getline.h | 47 +++ src/GraphicsEngine/System/wgl3.c | 232 +++++++++++++++ src/SAM3DBODY-cpp/cli_common.h | 146 ++++++++-- src/SAM3DBODY-cpp/fast_sam_3dbody.cpp | 20 +- src/SAM3DBODY-cpp/fast_sam_3dbody_capi.cpp | 4 + src/SAM3DBODY-cpp/fast_sam_3dbody_capi.h | 31 +- src/SAM3DBODY-cpp/main.cpp | 4 +- src/SAM3DBODY-cpp/offline_passes.cpp | 14 +- src/SAM3DBODY-cpp/pthreadWorkerPool.h | 5 + src/SAM3DBODY-cpp/windowsWorkerPool.h | 143 ++++++++++ .../windows_worker_pool_test.cpp | 67 +++++ src/multiview/CMakeLists.txt | 7 + src/multiview/extrinsics_test.cpp | 2 +- src/multiview/sam_3dbody_extrinsics.cpp | 7 +- src/render/fast_sam_3dbody_render.cpp | 85 ++++-- src/render/offline_sam_3dbody_render.cpp | 10 +- tests/test_fetch_model.ps1 | 80 ++++++ tests/test_portable_getline.c | 29 ++ tests/test_python_abi.py | 116 ++++++++ tests/test_python_abi_layout.c | 49 ++++ tests/test_wgl_context.c | 120 ++++++++ tools/fetch_model.ps1 | 217 ++++++++++++++ 44 files changed, 2256 insertions(+), 491 deletions(-) create mode 100644 .github/workflows/windows.yml create mode 100644 cmake/StageRuntime.cmake create mode 100644 knowledge/WINDOWS.md create mode 100644 knowledge/WINDOWS_VALIDATION.md create mode 100644 python/fsb_ctypes.py create mode 100644 scripts/build_windows.ps1 create mode 100644 src/GraphicsEngine/System/portable_getline.h create mode 100644 src/GraphicsEngine/System/wgl3.c create mode 100644 src/SAM3DBODY-cpp/windowsWorkerPool.h create mode 100644 src/SAM3DBODY-cpp/windows_worker_pool_test.cpp create mode 100644 tests/test_fetch_model.ps1 create mode 100644 tests/test_portable_getline.c create mode 100644 tests/test_python_abi.py create mode 100644 tests/test_python_abi_layout.c create mode 100644 tests/test_wgl_context.c create mode 100644 tools/fetch_model.ps1 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..13db6a8 --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,32 @@ +name: Windows native build + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + msvc: + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + - name: Fetch official OpenCV and GLEW development packages + shell: pwsh + run: | + New-Item -ItemType Directory -Path build/deps -Force | Out-Null + Invoke-WebRequest https://github.com/opencv/opencv/releases/download/4.10.0/opencv-4.10.0-windows.exe -OutFile build/deps/opencv.exe + 7z x build/deps/opencv.exe -obuild/deps -y -bso0 -bsp0 + if ($LASTEXITCODE -ne 0) { throw 'OpenCV extraction failed' } + Invoke-WebRequest https://github.com/nigels-com/glew/releases/download/glew-2.2.0/glew-2.2.0-win32.zip -OutFile build/deps/glew.zip + Expand-Archive build/deps/glew.zip build/deps + - name: Build all Windows targets and run model-free regression tests + shell: pwsh + run: | + ./scripts/build_windows.ps1 -OpenCVDir build/deps/opencv/build -GLEWRoot build/deps/glew-2.2.0 + powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/test_fetch_model.ps1 + if ($LASTEXITCODE -ne 0) { throw 'PowerShell download tests failed' } + # A hosted runner has no guaranteed OpenGL 3.3 desktop. test_wgl_context + # is compiled here; opt in to its execution locally with -TestOpenGL. diff --git a/CMakeLists.txt b/CMakeLists.txt index 7488326..a049cd9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,13 +8,17 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # --------------------------------------------------------------------------- # Platform helpers # --------------------------------------------------------------------------- -# Windows is a HEADLESS-ONLY target: the live OpenGL/X11 overlay renderer -# (fast_sam_3dbody_render) is not built there. The CLI (fast_sam_3dbody_run) -# and the offline BVH extractor (offline_sam_3dbody_render) are. See the -# configure-time notice further down where the renderer would be defined. if(WIN32) + # TARGET_RUNTIME_DLLS is used to stage dependencies next to the binaries. + if(CMAKE_VERSION VERSION_LESS 3.21) + message(FATAL_ERROR "Windows builds require CMake 3.21 or newer") + endif() set(MATH_LIB "") # libm is folded into the CRT on Windows set(RT_LIB "") # librt is POSIX-only + add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN _USE_MATH_DEFINES) + if(MSVC) + add_compile_options(/utf-8 /wd4996) + endif() else() set(MATH_LIB m) set(RT_LIB rt) @@ -52,14 +56,26 @@ option(SAM3D_FETCH_MODELS "Download the model files at configure time" OFF) if(NOT EXISTS "${_SENTINEL}") if(SAM3D_FETCH_MODELS) message(STATUS "SAM3D_FETCH_MODELS=ON — fetching models into ${_ONNX_DIR} …") - execute_process( - COMMAND bash "${CMAKE_CURRENT_SOURCE_DIR}/tools/fetch_model.sh" - shared cuda --onnx-dir "${_ONNX_DIR}" --yes - RESULT_VARIABLE _FETCH_RC) + if(WIN32) + execute_process( + COMMAND powershell.exe -NoProfile -ExecutionPolicy Bypass -File + "${CMAKE_CURRENT_SOURCE_DIR}/tools/fetch_model.ps1" + -Profile cuda -OnnxDir "${_ONNX_DIR}" -Yes + RESULT_VARIABLE _FETCH_RC) + else() + execute_process( + COMMAND bash "${CMAKE_CURRENT_SOURCE_DIR}/tools/fetch_model.sh" + shared cuda --onnx-dir "${_ONNX_DIR}" --yes + RESULT_VARIABLE _FETCH_RC) + endif() if(NOT _FETCH_RC EQUAL 0) message(WARNING "Model fetch failed (exit ${_FETCH_RC}) — " "run 'bash tools/fetch_model.sh' by hand.") endif() + elseif(WIN32) + message(STATUS "Models absent: build and model-free tests are available. Download with " + "powershell -File tools/fetch_model.ps1 -Profile cuda -Yes " + "(use -Profile cpu for CPU inference). See knowledge/WINDOWS.md.") else() message(WARNING "\n" @@ -130,7 +146,9 @@ if(CUDAToolkit_FOUND AND NOT CMAKE_CUDA_COMPILER AND CUDAToolkit_NVCC_EXECUTABLE endif() check_language(CUDA) if(CUDAToolkit_FOUND AND CMAKE_CUDA_COMPILER) - set(CMAKE_CUDA_ARCHITECTURES "86" CACHE STRING "CUDA architectures" FORCE) + if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES) + set(CMAKE_CUDA_ARCHITECTURES "86" CACHE STRING "CUDA architectures") + endif() enable_language(CUDA) set(GGML_CUDA ON CACHE BOOL "" FORCE) set(GGML_CUDA_F16 ON CACHE BOOL "" FORCE) @@ -147,6 +165,30 @@ else() endif() endif() +# Invoke in the directory that creates the target (including multiview). +function(sam3d_stage_runtime target) + if(WIN32) + file(GLOB _extra_dlls + "${ONNX_RUNTIME_DIR}/lib/onnxruntime_providers*.dll" + "${ONNX_RUNTIME_DIR}/bin/onnxruntime_providers*.dll" + "${OpenCV_CONFIG_PATH}/../bin/opencv_videoio_ffmpeg*.dll" + "${OpenCV_INSTALL_PATH}/bin/opencv_videoio_ffmpeg*.dll") + if(SAM3D_GLEW_DLL AND target MATCHES "^(fast_sam_3dbody_render|test_wgl_context)$") + list(APPEND _extra_dlls "${SAM3D_GLEW_DLL}") + endif() + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} + "-DSAM3D_RUNTIME_DLLS=$;${_extra_dlls}" + "-DSAM3D_DESTINATION=$" + -P "${PROJECT_SOURCE_DIR}/cmake/StageRuntime.cmake" + VERBATIM) + endif() +endfunction() + +# ORT's prebuilt CUDA provider does not need nvcc at build time. This lets +# Windows users run GPU inference while compiling ggml and LBS on the CPU. +option(SAM3D_ONNX_CUDA "Use the GPU ONNX Runtime package (CUDA runtime required when running)" ${WITH_CUDA}) + # --------------------------------------------------------------------------- # ggml (provides GGUF loading + small FFN inference for MHR/camera heads) # --------------------------------------------------------------------------- @@ -192,7 +234,7 @@ if(NOT DEFINED ONNX_RUNTIME_DIR OR ONNX_RUNTIME_DIR STREQUAL "") if(ONNX_RUNTIME_DIR STREQUAL "" OR NOT DEFINED ONNX_RUNTIME_DIR) # Download the pre-built package matching this platform/accelerator. set(_ORT_VERSION "1.20.1") - if(WITH_CUDA) + if(SAM3D_ONNX_CUDA) set(_ORT_SUFFIX "-gpu") else() set(_ORT_SUFFIX "") @@ -207,21 +249,29 @@ if(NOT DEFINED ONNX_RUNTIME_DIR OR ONNX_RUNTIME_DIR STREQUAL "") set(_ORT_DIRNAME "onnxruntime-${_ORT_OS}${_ORT_SUFFIX}-${_ORT_VERSION}") set(_ORT_ARCHIVE "${_ORT_DIRNAME}.${_ORT_EXT}") set(_ORT_URL "https://github.com/microsoft/onnxruntime/releases/download/v${_ORT_VERSION}/${_ORT_ARCHIVE}") - set(_ORT_DOWNLOAD_DIR "${CMAKE_BINARY_DIR}/onnxruntime_dl") - # After extraction + rename, contents live directly in _ORT_DOWNLOAD_DIR + # Keep CPU/GPU (and version/platform) packages separate so changing + # SAM3D_ONNX_CUDA in an existing build never reuses the wrong runtime. + set(_ORT_DOWNLOAD_ROOT "${CMAKE_BINARY_DIR}/onnxruntime_dl") + set(_ORT_DOWNLOAD_DIR "${_ORT_DOWNLOAD_ROOT}/${_ORT_DIRNAME}") set(ONNX_RUNTIME_DIR "${_ORT_DOWNLOAD_DIR}") - if(NOT EXISTS "${_ORT_DOWNLOAD_DIR}") + if(NOT EXISTS "${_ORT_DOWNLOAD_DIR}/include/onnxruntime_cxx_api.h") message(STATUS "Downloading ONNX Runtime ${_ORT_VERSION} (${_ORT_OS}${_ORT_SUFFIX}) …") - file(DOWNLOAD "${_ORT_URL}" "${CMAKE_BINARY_DIR}/${_ORT_ARCHIVE}" SHOW_PROGRESS) + file(DOWNLOAD "${_ORT_URL}" "${CMAKE_BINARY_DIR}/${_ORT_ARCHIVE}" + SHOW_PROGRESS STATUS _ORT_DOWNLOAD_STATUS TLS_VERIFY ON) + list(GET _ORT_DOWNLOAD_STATUS 0 _ORT_DOWNLOAD_RC) + if(NOT _ORT_DOWNLOAD_RC EQUAL 0) + message(FATAL_ERROR "ONNX Runtime download failed: ${_ORT_DOWNLOAD_STATUS}. Set ONNX_RUNTIME_DIR to an extracted package.") + endif() + file(MAKE_DIRECTORY "${_ORT_DOWNLOAD_ROOT}") execute_process( COMMAND ${CMAKE_COMMAND} -E tar xf "${CMAKE_BINARY_DIR}/${_ORT_ARCHIVE}" - WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" - ) - file(RENAME - "${CMAKE_BINARY_DIR}/${_ORT_DIRNAME}" - "${_ORT_DOWNLOAD_DIR}" + WORKING_DIRECTORY "${_ORT_DOWNLOAD_ROOT}" + RESULT_VARIABLE _ORT_EXTRACT_RC ) + if(NOT _ORT_EXTRACT_RC EQUAL 0) + message(FATAL_ERROR "Failed to extract ${_ORT_ARCHIVE}") + endif() endif() endif() endif() @@ -230,12 +280,24 @@ endif() if(NOT TARGET onnxruntime::onnxruntime) set(ORT_INCLUDE_DIRS "${ONNX_RUNTIME_DIR}/include") set(ORT_LIB_DIR "${ONNX_RUNTIME_DIR}/lib") + # These are derived from ONNX_RUNTIME_DIR, not independent user settings. + # CMake caches find_* results across configures; discard them before looking + # in a newly selected CPU/GPU package or an explicit replacement directory. + unset(ORT_LIB CACHE) + unset(ORT_LIB) + unset(ORT_DLL CACHE) + unset(ORT_DLL) find_library(ORT_LIB onnxruntime PATHS "${ORT_LIB_DIR}" NO_DEFAULT_PATH REQUIRED) add_library(onnxruntime::onnxruntime SHARED IMPORTED) set_target_properties(onnxruntime::onnxruntime PROPERTIES - IMPORTED_LOCATION "${ORT_LIB}" - INTERFACE_INCLUDE_DIRECTORIES "${ORT_INCLUDE_DIRS}" - ) + INTERFACE_INCLUDE_DIRECTORIES "${ORT_INCLUDE_DIRS}") + if(WIN32) + find_file(ORT_DLL onnxruntime.dll PATHS "${ORT_LIB_DIR}" "${ONNX_RUNTIME_DIR}/bin" NO_DEFAULT_PATH REQUIRED) + set_target_properties(onnxruntime::onnxruntime PROPERTIES + IMPORTED_IMPLIB "${ORT_LIB}" IMPORTED_LOCATION "${ORT_DLL}") + else() + set_target_properties(onnxruntime::onnxruntime PROPERTIES IMPORTED_LOCATION "${ORT_LIB}") + endif() set(ORT_LIBS onnxruntime::onnxruntime) message(STATUS "ONNX Runtime at ${ONNX_RUNTIME_DIR}") endif() @@ -328,6 +390,8 @@ target_link_libraries(fast_sam_3dbody PUBLIC if(WIN32) # M_PI and friends are not defined by default in the MSVC . target_compile_definitions(fast_sam_3dbody PUBLIC _USE_MATH_DEFINES) + target_compile_definitions(fast_sam_3dbody PRIVATE FSB_BUILD_DLL) + set_target_properties(fast_sam_3dbody PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) endif() if(WITH_CUDA) @@ -342,7 +406,7 @@ endif() # TensorRT EP: only meaningful for CUDA builds (it dlopens the NVIDIA TRT runtime # at session-create time and degrades to the CUDA EP if it's not present). -if(SAM3D_TENSORRT AND WITH_CUDA) +if(SAM3D_TENSORRT AND SAM3D_ONNX_CUDA) target_compile_definitions(fast_sam_3dbody PRIVATE USE_TENSORRT_EP) message(STATUS "SAM3D_TENSORRT=ON — TensorRT EP compiled in (enable at runtime with --trt)") endif() @@ -370,7 +434,7 @@ endif() target_compile_options(fast_sam_3dbody PRIVATE $<$:-Wall -Wextra -Wno-unused-parameter -Wno-unused-variable -Wno-unused-function -O3 -march=native> - $<$:/O2> + $<$,$>>:/O2> ) # --------------------------------------------------------------------------- @@ -382,24 +446,46 @@ target_link_libraries(fast_sam_3dbody_run PRIVATE fast_sam_3dbody) target_compile_options(fast_sam_3dbody_run PRIVATE $<$:-Wall -Wextra -Wno-unused-parameter -Wno-unused-variable -Wno-unused-function -O3 -march=native> - $<$:/O2> + $<$,$>>:/O2> ) # --------------------------------------------------------------------------- # fast_sam_3dbody_render executable (OpenGL overlay renderer) # -# Linux-only: it is built on a GLX/X11 OpenGL context (glx3.c) with no Windows -# (WGL) equivalent in-tree. Windows builds are headless — the renderer is -# skipped and a notice is printed instead. +# Linux uses GLX; Windows uses the matching WGL context implementation. # --------------------------------------------------------------------------- -if(NOT WIN32) +option(SAM3D_BUILD_RENDERER "Build the live OpenGL viewer (GLX on Linux, WGL on Windows)" ON) +if(SAM3D_BUILD_RENDERER) find_package(OpenGL REQUIRED) find_package(GLEW REQUIRED) + # FindGLEW's module-mode target is UNKNOWN IMPORTED on Windows and its + # IMPORTED_LOCATION is the .lib, so TARGET_RUNTIME_DLLS cannot see the DLL. + if(WIN32) + if(GLEW_USE_STATIC_LIBS) + set_property(TARGET GLEW::GLEW APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS GLEW_STATIC) + else() + get_target_property(_glew_type GLEW::GLEW TYPE) + if(_glew_type STREQUAL "UNKNOWN_LIBRARY") + find_file(SAM3D_GLEW_DLL NAMES glew32.dll GLEW.dll + HINTS "${GLEW_ROOT}" "$ENV{GLEW_ROOT}" "${GLEW_INCLUDE_DIR}/.." + PATH_SUFFIXES bin/Release/x64 bin REQUIRED) + endif() + endif() + endif() + + if(WIN32) + set(SAM3D_GL_CONTEXT ${GRAPHICS_ENGINE_DIR}/System/wgl3.c) + set(SAM3D_WINDOW_LIBS user32 gdi32) + else() + set(SAM3D_GL_CONTEXT ${GRAPHICS_ENGINE_DIR}/System/glx3.c) + set(SAM3D_WINDOW_LIBS X11) + endif() + add_executable(fast_sam_3dbody_render src/render/fast_sam_3dbody_render.cpp src/SAM3DBODY-cpp/v4l2_capture.cpp - ${GRAPHICS_ENGINE_DIR}/System/glx3.c + ${SAM3D_GL_CONTEXT} ${GRAPHICS_ENGINE_DIR}/ModelLoader/model_loader_tri.c ${GRAPHICS_ENGINE_DIR}/ModelLoader/model_loader_transform_joints.c ) @@ -422,13 +508,15 @@ if(NOT WIN32) OpenGL::GL GLEW::GLEW ${OpenCV_LIBS} - X11 + ${SAM3D_WINDOW_LIBS} ${RT_LIB} ${MATH_LIB} - AmMatrix + ${AMMATRIX_LIB} ) - if(CMAKE_BUILD_TYPE STREQUAL "Debug") + if(MSVC) + target_compile_options(fast_sam_3dbody_render PRIVATE $<$>:/O2>) + elseif(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_options(fast_sam_3dbody_render PRIVATE $<$:-Wall -Wextra -Wno-unused-parameter -Wno-unused-variable -Wno-unused-function -g -O0> $<$:-Wall -Wno-unused-parameter -Wno-unused-variable -Wno-unused-function -g -O0> @@ -440,19 +528,7 @@ if(NOT WIN32) ) endif() else() - message(STATUS "") - message(STATUS " ┌─────────────────────────────────────────────────────────────────┐") - message(STATUS " │ Windows build: HEADLESS ONLY │") - message(STATUS " │ │") - message(STATUS " │ Live visualization is NOT supported on Windows. The OpenGL │") - message(STATUS " │ overlay renderer (fast_sam_3dbody_render) needs GLX/X11 and │") - message(STATUS " │ will not be built. │") - message(STATUS " │ │") - message(STATUS " │ Use these instead: │") - message(STATUS " │ fast_sam_3dbody_run – CLI pose/keypoint output │") - message(STATUS " │ offline_sam_3dbody_render – offline BVH extraction │") - message(STATUS " └─────────────────────────────────────────────────────────────────┘") - message(STATUS "") + message(STATUS "Live OpenGL viewer disabled (SAM3D_BUILD_RENDERER=OFF)") endif() # --------------------------------------------------------------------------- @@ -481,7 +557,7 @@ target_link_libraries(offline_sam_3dbody_render PRIVATE target_compile_options(offline_sam_3dbody_render PRIVATE $<$:-Wall -Wextra -Wno-unused-parameter -Wno-unused-variable -Wno-unused-function -O3 -march=native> - $<$:/O2> + $<$,$>>:/O2> ) # --------------------------------------------------------------------------- @@ -534,7 +610,9 @@ endif() # --------------------------------------------------------------------------- # QR timecode synchronization tool (optional; needs X11/Xrandr/qrencode) # --------------------------------------------------------------------------- -add_subdirectory(src/synchronization) +if(NOT WIN32) + add_subdirectory(src/synchronization) +endif() # --------------------------------------------------------------------------- # multi-view capture & fusion (MULTIVIEW_PLAN.md) — .calib loader so far @@ -542,21 +620,74 @@ add_subdirectory(src/synchronization) enable_testing() add_subdirectory(src/multiview) +foreach(_target fast_sam_3dbody_run offline_sam_3dbody_render fast_sam_3dbody_render) + if(TARGET ${_target}) + add_test(NAME ${_target}_help COMMAND ${_target} + --onnx-dir definitely-missing-models --from 999 --help) + set_tests_properties(${_target}_help PROPERTIES TIMEOUT 15 + ENVIRONMENT "SAM3D_AUTO_FETCH=0" + PASS_REGULAR_EXPRESSION "Usage:" + FAIL_REGULAR_EXPRESSION "Loading backbone|models missing|Failed to load pipeline") + endif() +endforeach() + +# These regression tests need no model files or network access. +add_executable(test_python_abi_layout tests/test_python_abi_layout.c) +target_include_directories(test_python_abi_layout PRIVATE src/SAM3DBODY-cpp) +find_package(Python3 QUIET COMPONENTS Interpreter) +if(Python3_Interpreter_FOUND) + add_test(NAME python_abi COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_python_abi.py" + --probe $ + --lib-dir $) +endif() +add_executable(test_portable_getline tests/test_portable_getline.c) +add_test(NAME portable_getline COMMAND test_portable_getline) +if(WIN32) + # CTest launched by PowerShell 7 otherwise passes its incompatible module + # search path to Windows PowerShell 5.1. Let that child derive its own path. + add_test(NAME powershell_model_manifest COMMAND ${CMAKE_COMMAND} -E env + --unset=PSModulePath powershell.exe -NoProfile + -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_fetch_model.ps1") + set_tests_properties(powershell_model_manifest PROPERTIES TIMEOUT 30) + add_executable(windows_worker_pool_test src/SAM3DBODY-cpp/windows_worker_pool_test.cpp) + add_test(NAME windows_worker_pool COMMAND windows_worker_pool_test) + if(SAM3D_BUILD_RENDERER) + add_executable(test_wgl_context tests/test_wgl_context.c ${SAM3D_GL_CONTEXT}) + target_link_libraries(test_wgl_context PRIVATE OpenGL::GL GLEW::GLEW user32 gdi32) + option(SAM3D_TEST_OPENGL "Run the WGL test (requires an interactive desktop and OpenGL 3.3 driver)" OFF) + if(SAM3D_TEST_OPENGL) + add_test(NAME wgl_context COMMAND test_wgl_context) + set_tests_properties(wgl_context PROPERTIES TIMEOUT 30) + endif() + endif() + + # ORT providers and OpenCV's FFmpeg backend are loaded dynamically, so + # TARGET_RUNTIME_DLLS alone cannot discover them. + foreach(_target fast_sam_3dbody fast_sam_3dbody_run offline_sam_3dbody_render + fast_sam_3dbody_render test_wgl_context) + if(TARGET ${_target}) + sam3d_stage_runtime(${_target}) + endif() + endforeach() +endif() + # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- -if(WIN32) - set(_VIS_STATUS "DISABLED (headless build — Windows not supported)") - set(_TARGETS_STATUS "fast_sam_3dbody (lib) fast_sam_3dbody_run (exe) offline_sam_3dbody_render (exe)") -else() +if(SAM3D_BUILD_RENDERER) set(_VIS_STATUS "enabled (fast_sam_3dbody_render)") set(_TARGETS_STATUS "fast_sam_3dbody (lib) fast_sam_3dbody_run (exe) fast_sam_3dbody_render (exe) offline_sam_3dbody_render (exe)") +else() + set(_VIS_STATUS "disabled") + set(_TARGETS_STATUS "fast_sam_3dbody (lib) fast_sam_3dbody_run (exe) offline_sam_3dbody_render (exe)") endif() message(STATUS "") message(STATUS "=== fast_sam_3dbody build ===") message(STATUS " Platform : ${CMAKE_SYSTEM_NAME}") message(STATUS " CUDA support : ${WITH_CUDA}") +message(STATUS " ORT GPU pkg : ${SAM3D_ONNX_CUDA}") message(STATUS " ggml CUDA : ${GGML_CUDA}") message(STATUS " OpenCV : ${OpenCV_VERSION}") message(STATUS " ORT include : ${ORT_INCLUDE_DIRS}") diff --git a/README.md b/README.md index 7e64702..fe6af51 100644 --- a/README.md +++ b/README.md @@ -292,24 +292,39 @@ CMake handles dependencies automatically: > libonnxruntime_providers_cuda.so` or `Could not find an implementation for > Expand(13)`, see **[DEPENDENCIES.md](knowledge/DEPENDENCIES.md)** for the cause and fix. -#### Windows (headless build) - -Windows is supported as a **headless build only** (MSVC + CMake; OpenCV via -vcpkg). CMake automatically fetches the `win-x64` ONNX Runtime and configures the -CLI (`fast_sam_3dbody_run`) and offline BVH extractor (`offline_sam_3dbody_render`). - -The live OpenGL overlay viewer (`fast_sam_3dbody_render`) is **not built on -Windows** — it depends on GLX/X11, which has no in-tree Windows equivalent. CMake -prints a notice to this effect at configure time. For visualization on Windows, -use the offline BVH output or the Python frontends. Linux remains the platform -for live rendering. - -Outputs in `build/`: +#### Windows (native build and WGL viewer) + +Native Windows supports the CLI, offline BVH extractor, Python/C shared DLL, +and an OpenGL 3.3 viewer using Win32/WGL. Use **Visual Studio 2022 x64**, **CMake +3.21+**, the official **OpenCV 4.10.0** Windows package (`vc16` x64 libraries), +and **GLEW 2.2.0** for the viewer. From the repository root in PowerShell: + +```powershell +.\scripts\build_windows.ps1 ` + -OpenCVDir D:\deps\opencv\build ` + -GLEWRoot D:\deps\glew-2.2.0 -Gpu +.\tools\fetch_model.ps1 -Profile cuda -Yes +``` -| File | Description | -|------|-------------| -| `fast_sam_3dbody_run` | Standalone CLI executable | -| `libfast_sam_3dbody.so` | Shared library for C++ linking or ctypes | +`-Gpu` uses GPU ONNX Runtime 1.20.1 without requiring nvcc; without a CUDA +compiler, native ggml/LBS remains on the CPU. Running the CUDA provider still +requires CUDA 12.x/cuDNN 9.x runtime DLLs on `PATH`. Use `-Headless` to omit the +viewer, `-TestOpenGL` to opt into its desktop graphics test, or +`-OnnxRuntimeDir` to supply an existing ORT package. + +| Default Release output | Description | +|---|---| +| `build/windows/Release/fast_sam_3dbody_run.exe` | Inference CLI | +| `build/windows/Release/fast_sam_3dbody_render.exe` | WGL viewer, unless built with `-Headless` | +| `build/windows/Release/offline_sam_3dbody_render.exe` | Offline BVH extractor | +| `build/windows/Release/fast_sam_3dbody.dll` | C++/C/Python library with ABI-checked ctypes bindings | + +Keep running from the **repository root**: shaders, mesh assets and BVH +templates retain repository-relative paths. The output folder alone is not a +standalone distribution. See **[WINDOWS.md](knowledge/WINDOWS.md)** for CPU and +FP16 model profiles, CUDA-provider commands that do not require TensorRT, +Python `--lib-dir build/windows`, DLL setup and platform limitations. +The WGL implementation credits [beemsoft's PR #13](https://github.com/AmmarkoV/SAM3DBody-cpp/pull/13). --- diff --git a/cmake/StageRuntime.cmake b/cmake/StageRuntime.cmake new file mode 100644 index 0000000..4bdac41 --- /dev/null +++ b/cmake/StageRuntime.cmake @@ -0,0 +1,7 @@ +# A static dependency configuration can legitimately produce an empty list. +foreach(_dll IN LISTS SAM3D_RUNTIME_DLLS) + if(NOT _dll STREQUAL "") + get_filename_component(_name "${_dll}" NAME) + file(COPY_FILE "${_dll}" "${SAM3D_DESTINATION}/${_name}" ONLY_IF_DIFFERENT) + endif() +endforeach() diff --git a/knowledge/DEPENDENCIES.md b/knowledge/DEPENDENCIES.md index 1778e26..ea8814a 100644 --- a/knowledge/DEPENDENCIES.md +++ b/knowledge/DEPENDENCIES.md @@ -10,7 +10,7 @@ at runtime**, you are almost certainly in the right place. | Component | Version | Notes | |-----------|---------|-------| -| ONNX Runtime | **1.20.1 (GPU build)** | Downloaded automatically by CMake into `build/onnxruntime_dl/` if not found. | +| ONNX Runtime | **1.20.1 (GPU build)** | Downloaded automatically by CMake into `build/onnxruntime_dl//` if not found; for Linux GPU this is `onnxruntime-linux-x64-gpu-1.20.1`. CPU/GPU packages use separate directories. | | CUDA | **12.x** | Required by the ORT 1.20.1 CUDA execution provider. | | cuDNN | **9.x** | Required by ORT ≥ 1.19. **cuDNN 8 will not work** and is the #1 cause of the CUDA EP failing to load. | | NVIDIA driver | Recent enough for CUDA 12 (≥ 525) | Check with `nvidia-smi`. | @@ -41,7 +41,7 @@ version — virtually always **cuDNN 9** or a CUDA 12 runtime lib. nvidia-smi # 2. THE decisive command — what is the provider .so actually missing? -ldd build/onnxruntime_dl/lib/libonnxruntime_providers_cuda.so | grep -i "not found" +ldd build/onnxruntime_dl/onnxruntime-linux-x64-gpu-1.20.1/lib/libonnxruntime_providers_cuda.so | grep -i "not found" # 3. Is cuDNN 9 / cuBLAS installed and visible to the loader? ldconfig -p | grep -i "cudnn\|cublas\|cufft" @@ -72,7 +72,7 @@ for m in (nvidia.cudnn, nvidia.cublas)))"):$LD_LIBRARY_PATH ```bash # Should now print nothing (no missing libraries): -ldd build/onnxruntime_dl/lib/libonnxruntime_providers_cuda.so | grep -i "not found" +ldd build/onnxruntime_dl/onnxruntime-linux-x64-gpu-1.20.1/lib/libonnxruntime_providers_cuda.so | grep -i "not found" ``` Re-run `scripts/webcam.sh` — the CUDA EP should load and the pipeline should run @@ -172,7 +172,7 @@ A system `.deb`/`.tar` TensorRT 10.4 install from developer.nvidia.com/tensorrt (then `sudo ldconfig`) works too. Verify the runtime is visible: ```bash -ldd build/onnxruntime_dl/lib/libonnxruntime_providers_tensorrt.so | grep -i nvinfer +ldd build/onnxruntime_dl/onnxruntime-linux-x64-gpu-1.20.1/lib/libonnxruntime_providers_tensorrt.so | grep -i nvinfer # every libnvinfer*.so.10 / libnvonnxparser.so.10 line should resolve (no "not found") ``` diff --git a/knowledge/WINDOWS.md b/knowledge/WINDOWS.md new file mode 100644 index 0000000..05b4418 --- /dev/null +++ b/knowledge/WINDOWS.md @@ -0,0 +1,270 @@ +# Native Windows build + +The Windows targets include the C++ inference CLI, the offline BVH extractor, +the shared DLL for Python/C callers, and a Win32/WGL OpenGL viewer. Bash and WSL +are not required for the native build. The Linux setup remains available in +[WSL.md](WSL.md). + +Run the commands below in PowerShell **from the repository root**: + +```powershell +Set-Location D:\AI\SAM3DBody-cpp +``` + +The build stages dependent runtime DLLs next to the executables, but it does not +create a standalone redistributable folder. The renderer still uses repository +resources such as `src/render/default.vert`, `src/render/default.frag`, +`onnx/body_mesh.tri`, and BVH templates under `bvh/`. Keep the checkout and model +files available; copying only `build/windows/Release` elsewhere is insufficient. + +## Prerequisites + +Use x64 components throughout: + +- Visual Studio 2022 or Build Tools 2022 with **Desktop development with C++**, + the MSVC x64 toolset and a Windows SDK. +- CMake **3.21 or newer**, plus Git, on `PATH`. +- Windows PowerShell **5.1 or newer** and `curl.exe` for the model downloader. +- The official [OpenCV 4.10.0 Windows package](https://github.com/opencv/opencv/releases/tag/4.10.0). + Extract `opencv-4.10.0-windows.exe`; the examples assume the resulting + `opencv/build` directory is `D:\deps\opencv\build`. Its x64 `vc16` libraries + are used with the Visual Studio 2022 generator. Point `-OpenCVDir` at the + extracted `build` directory containing `OpenCVConfig.cmake`. +- For the viewer, the [GLEW 2.2.0 Windows binaries](https://github.com/nigels-com/glew/releases/tag/glew-2.2.0). + The example root `D:\deps\glew-2.2.0` should contain `include/GL/glew.h`, + `lib/Release/x64/glew32.lib`, and `bin/Release/x64/glew32.dll`. + A GPU display driver supporting OpenGL **3.3** is required to run the viewer. + +CMake fetches ggml and, unless an existing ONNX Runtime is supplied, downloads +the appropriate ONNX Runtime **1.20.1** Windows x64 package. These dependency +downloads are separate from the model downloads. The helper reuses the fetched +ggml checkout on subsequent builds; pass +`-CMakeArgs '-DFETCHCONTENT_UPDATES_DISCONNECTED=OFF'` to request dependency updates. + +## Build + +For an NVIDIA GPU and the WGL viewer: + +```powershell +.\scripts\build_windows.ps1 ` + -OpenCVDir D:\deps\opencv\build ` + -GLEWRoot D:\deps\glew-2.2.0 ` + -Gpu +``` + +The default Visual Studio generator targets x64 and builds Release into +`build/windows/Release`. The script runs CTest after building. A separate CPU +build without the OpenGL viewer needs no GLEW: + +```powershell +.\scripts\build_windows.ps1 ` + -OpenCVDir D:\deps\opencv\build ` + -BuildDir .\build\windows-cpu ` + -Headless +``` + +`-Gpu` selects the prebuilt GPU ONNX Runtime package and enables its CUDA +execution provider. **It does not require nvcc to compile this project.** When +nvcc is unavailable, native ggml/LBS code runs on the CPU while ONNX inference +can use the GPU. If CMake finds a working CUDA compiler and toolkit, it can +also build the native CUDA paths. + +| Script option | Effect | +|---|---| +| `-Gpu` | Set `SAM3D_ONNX_CUDA=ON`; select GPU ORT when CMake downloads ORT. | +| `-Headless` | Disable the viewer target with `SAM3D_BUILD_RENDERER=OFF`; retain the CLI, DLL and offline extractor. | +| `-TestOpenGL` | Include the model-free WGL test in CTest; requires the viewer build, an interactive desktop session and an OpenGL 3.3 driver. | +| `-OnnxRuntimeDir D:\deps\onnxruntime-win-x64-gpu-1.20.1` | Use an extracted ORT package containing `include` and `lib`; when using `-Gpu`, supply the GPU package. | +| `-CudaArchitectures 89` | Forward `CMAKE_CUDA_ARCHITECTURES` for native CUDA compilation when nvcc is available; it does not change the prebuilt ORT package. | +| `-BuildDir .\build\windows` | Choose the CMake build directory. | +| `-Configuration Release` | Select Release, RelWithDebInfo or Debug. Use Release for these examples. | +| `-Jobs 4` | Limit parallel build jobs. | +| `-SkipTests` | Build without running CTest. | + +Automatically downloaded CPU and GPU ORT packages use separate, versioned +directories under `build/windows/onnxruntime_dl`, so switching `-Gpu` selects +the corresponding package. An explicit `-OnnxRuntimeDir` takes precedence: +when switching modes, update that argument to match the intended package. + +If script execution is blocked, invoke the script with a process-local policy: + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass ` + -File scripts\build_windows.ps1 ` + -OpenCVDir D:\deps\opencv\build ` + -GLEWRoot D:\deps\glew-2.2.0 -Gpu +``` + +## GPU runtime DLLs + +The GPU ORT 1.20.1 package still needs compatible **CUDA 12.x and cuDNN 9.x +runtime DLLs**, plus an NVIDIA driver, when the program runs. The project does +not bundle those NVIDIA runtime dependencies. See the +[ONNX Runtime CUDA provider requirements](https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html#requirements). + +Add the directories actually containing your CUDA/cuDNN DLLs to the current +PowerShell session before running a native executable or Python. For example, +if those are the directories used by your installation: + +```powershell +$env:PATH = "D:\deps\cuda12\bin;D:\deps\cudnn9\bin;$env:PATH" +``` + +`-OnnxRuntimeDir` selects ORT's own package; it does not install CUDA, cuDNN or +TensorRT. Inspect provider initialization messages to confirm the desired +provider loaded. A missing DLL error can refer to a dependency of +`onnxruntime_providers_cuda.dll`, even when that provider DLL itself is present. + +## Download models + +The PowerShell downloader reads the same manifest as `tools/fetch_model.sh`. +Use `-List` to inspect the selection without downloading or creating directories: + +```powershell +.\tools\fetch_model.ps1 -Profile cuda -List +.\tools\fetch_model.ps1 -Profile cuda -OnnxDir .\onnx -Yes +``` + +| Profile | Files selected, in addition to shared files | +|---|---| +| `cpu` | `backbone_fp32.onnx` and its sidecar, plus `decoder_fp16.onnx` and its sidecar. | +| `cuda` | The stock `backbone.onnx`/sidecar and `decoder.onnx`, intended for CUDA inference. | +| `trt` | `backbone_fp16_trt.onnx`/sidecar and `decoder_fp16.onnx`/sidecar. These are model files; selecting the profile does not install or activate TensorRT. | +| `refined` | Additional iterative decoder graphs and `pipeline_refined.gguf`; explicitly opt in alongside a base profile. | +| `libreyolo` | The optional LibreYOLO detector. | +| `all` | The CPU, CUDA and TRT base profiles, with duplicate files removed. Refined and LibreYOLO remain opt-in. | + +`shared` is implied by every profile. Multiple profiles can be combined, for +example `-Profile cuda,refined`. `-Revision` overrides the Hugging Face revision; +the default is `SAM3D_HF_REVISION` if set, otherwise `main`. The manifest size +and SHA256 are still enforced when choosing a different revision. + +Existing files are skipped only after size and SHA256 verification. Downloads +go to `.partial` files and replace the final name only after verification. +Incomplete files can resume; `-Force` starts a fresh download. A corrupt file +is reported as invalid and is never silently accepted. `SAM3D_AUTO_FETCH=0` +blocks downloads even with `-Yes`; `SAM3D_AUTO_FETCH=1` skips the prompt. + +## Run the native executables + +The examples use your own input file at `D:\data\person.jpg` or +`D:\data\clip.mp4`; replace those paths with existing files. Continue to run +from the repository root so shader and BVH template paths resolve. + +For the stock CUDA profile and a GPU ORT build: + +```powershell +.\build\windows\Release\fast_sam_3dbody_run.exe ` + --onnx-dir .\onnx --gguf .\onnx\pipeline.gguf ` + --yolo .\onnx\yolo.onnx --cuda 0 --from D:\data\person.jpg +``` + +For a CPU-only run, first fetch `-Profile cpu`, then use the CPU build and +`--cuda -1`: + +```powershell +.\build\windows-cpu\Release\fast_sam_3dbody_run.exe ` + --onnx-dir .\onnx --gguf .\onnx\pipeline.gguf ` + --yolo .\onnx\yolo.onnx --cuda -1 --from D:\data\person.jpg +``` + +To use the smaller FP16 model files with the **CUDA execution provider**, +download `-Profile trt` and explicitly select both model filenames: + +```powershell +.\tools\fetch_model.ps1 -Profile trt -Yes +.\build\windows\Release\fast_sam_3dbody_run.exe ` + --onnx-dir .\onnx --gguf .\onnx\pipeline.gguf ` + --yolo .\onnx\yolo.onnx --cuda 0 ` + --backbone backbone_fp16_trt.onnx --decoder decoder_fp16.onnx ` + --from D:\data\person.jpg +``` + +This command does not need TensorRT runtime libraries. Only add `--trt` after +installing TensorRT runtime DLLs compatible with your ORT build and making them +discoverable. Model precision and execution provider are separate choices. + +The viewer and offline extractor accept the same model-selection flags. With +the FP16 files above, a viewer invocation is: + +```powershell +.\build\windows\Release\fast_sam_3dbody_render.exe ` + --onnx-dir .\onnx --gguf .\onnx\pipeline.gguf ` + --yolo .\onnx\yolo.onnx --cuda 0 ` + --backbone backbone_fp16_trt.onnx --decoder decoder_fp16.onnx ` + --from D:\data\clip.mp4 +``` + +For offline BVH output, substitute `offline_sam_3dbody_render.exe` and add +`--bvh D:\data\capture.bvh`. The offline extractor enables refined pose by +default, so also fetch `-Profile refined`, or add `--no-refined-pose` to run +with only the base models downloaded above. Windows reports missing models +with manual download instructions instead of launching the Bash downloader. +The build-time `-Headless` switch omits the viewer; +the viewer's separate runtime `--headless` option uses a hidden WGL window and +still requires OpenGL. The offline extractor does not need a GL context. + +## Python frontend and ABI + +Use a 64-bit Python interpreter with NumPy and `opencv-python` for the +lightweight frontend. The 3D/PyTorch and ROS frontends retain their additional +dependencies. For the stock **cuda** profile: + +```powershell +.\tools\fetch_model.ps1 -Profile cuda -Yes +python .\python\fast_sam_3dbody_frontend.py ` + --lib-dir .\build\windows --cuda 0 --from D:\data\person.jpg +``` + +`--lib-dir build/windows` also searches its `Release`, `RelWithDebInfo`, `Debug` +and `MinSizeRel` subdirectories. The shared `python/fsb_ctypes.py` binding loads +the cdecl DLL, registers dependency directories, and checks the native ABI +version and both structure sizes before binding inference functions. A stale +DLL or mismatched Python checkout fails with a clear ABI error; rebuild and use +matching files instead of bypassing the check. + +The C/Python API uses the default model filenames. The explicit +`--backbone`/`--decoder` overrides in this guide belong to the native CLI; +the current Python frontend does not expose those model-selection options. + +## Tests and current scope + +See [WINDOWS_VALIDATION.md](WINDOWS_VALIDATION.md) for the tested environment, +actual GPU inference, WGL rendering and BVH export results. + +Run the compiled regression suite separately with: + +```powershell +ctest --test-dir .\build\windows -C Release --output-on-failure +powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests\test_fetch_model.ps1 +``` + +CTest includes the C/Python layout and DLL lifecycle checks when a Python +interpreter is found. `-TestOpenGL` additionally enables the WGL context, +shader/pixel-readback, resize and teardown test. These tests do not require +model downloads; they do not establish camera compatibility, pose accuracy or +inference performance. + +Windows support currently has these boundaries: + +- The POSIX network server/client target, shared-memory transport, raw V4L2 + capture and X11 QR timecode display remain outside the native Windows build. + Windows capture uses OpenCV's capture path. +- ArUco-dependent multiview tools are optional. When the `aruco` module is + absent from the selected OpenCV package, CMake skips those tools; supplying + an appropriate OpenCV contrib build enables their build paths. This is + separate from the Linux-only X11 QR display utility. +- The documented build paths do not imply validation of every camera backend, + native nvcc configuration, TensorRT runtime combination, or all non-ASCII + model/media paths. Keep those checks separate from basic native build tests. +- Shell wrappers for FFmpeg, GMR or ROS workflows may need their own Windows + adaptations and additional dependencies. Use the native executables for the + commands in this guide. + +## Attribution + +The WGL implementation was adapted from **beemsoft's Windows port** in +[PR #13](https://github.com/AmmarkoV/SAM3DBody-cpp/pull/13), with attribution +retained in `src/GraphicsEngine/System/wgl3.c`. The current implementation adds +the OpenGL 3.3 context setup and the current window callback/title/cleanup +integration. The repository's existing license remains applicable. diff --git a/knowledge/WINDOWS_VALIDATION.md b/knowledge/WINDOWS_VALIDATION.md new file mode 100644 index 0000000..57e6877 --- /dev/null +++ b/knowledge/WINDOWS_VALIDATION.md @@ -0,0 +1,124 @@ +# Windows validation record + +The Windows branch was tested locally on 2026-09-09, based on upstream commit +`a4190de3b31e396e4dc273085282da4fbceb3adf`. This records build and runtime smoke +tests, not a pose-accuracy evaluation or a performance benchmark. + +## Environment + +| Component | Tested version | +|---|---| +| OS | Windows 11 x64, build 26200 | +| Compiler | Visual Studio 2022 Build Tools, MSVC 14.44.35207 | +| SDK / CMake | Windows SDK 10.0.26100 / CMake 4.3.4 | +| Shell / Python | Windows PowerShell 5.1 / Python 3.11 x64 | +| OpenCV / GLEW | Official OpenCV 4.10.0 vc16 x64 / GLEW 2.2.0 | +| ONNX Runtime | Official 1.20.1 Windows x64 GPU package | +| GPU | NVIDIA GeForce RTX 4060 Laptop GPU, 8 GB VRAM | +| NVIDIA driver | 610.62 | +| CUDA runtime packages | cuBLAS 12.4.5.8, cuDNN 9.1.0.70, CUDA runtime 12.4.127, cuFFT 11.2.1.3 | +| ggml checkout | `7840aaba1989c6deeefede1d77d5aaf8f52b947e` | + +There was no nvcc toolchain installed for the inference tests. Native ggml/LBS +ran on CPU; ONNX Runtime used CUDA. The NVIDIA runtime DLL directories were +added to the process PATH. This distinguishes the runtime dependency from the +optional native CUDA build toolchain. + +## Build and regression checks + +The PowerShell build helper completed a native Release build, including the +DLL, CLI, WGL viewer, offline extractor and available multiview targets. +CTest passed all 12 enabled tests with `-TestOpenGL`: CLI/viewer/offline help, +C/Python ABI, portable getline, PowerShell model manifest, Windows worker pool, +WGL context/readback, calibration, extrinsics, and the two synchronization tests. +The model downloader test includes 17 offline checks. + +Targeted reconfiguration checks verified CPU/GPU/explicit ORT package-path +selection in one build directory without retaining stale library cache values. +Those selection checks used directory junctions to an existing package; they +do not constitute a separate CPU runtime test. Generated MSVC Debug projects +use `/Od /RTC1`, and the CLI's Debug compilation step passed; a complete Debug +link and inference run was not performed. + +The ABI check compares every field offset, field size and structure size +against a compiled C probe. It also loads the actual DLL and checks handle +creation/destruction and invalid-input guards. The WGL test compiles shaders, +draws and reads back pixels, exercises hidden and visible windows, resize, +close/Escape handling and context recreation. + +The Windows GitHub Actions workflow passed actionlint 1.7.12 locally. The local +checks alone do not establish that a hosted workflow has passed; consult the +checks attached to the actual GitHub commit for that result. + +## Actual inference and rendering + +The PowerShell downloader fetched and verified all nine files in the `trt` +profile (about 1.80 GiB). These FP16 model files were executed on the **CUDA +execution provider**, without TensorRT, using explicit model filenames: + +```powershell +$env:SAM3D_AUTO_FETCH = '0' +.\build\windows\Release\fast_sam_3dbody_run.exe ` + --onnx-dir .\onnx --cuda 0 ` + --backbone backbone_fp16_trt.onnx --decoder decoder_fp16.onnx ` + --from .\build\dancing.jpg --headless --max-persons 1 ` + --out .\build\dance.csv --bvh .\build\dance.bvh + +.\build\windows\Release\fast_sam_3dbody_render.exe ` + --onnx-dir .\onnx --cuda 0 ` + --backbone backbone_fp16_trt.onnx --decoder decoder_fp16.onnx ` + --from .\build\dancing.jpg --headless --max-persons 1 --frames 1 ` + --save-frames .\build\dance-render +``` + +The image run returned one person with finite 3D keypoints. The CSV contained +212 columns, including 70 three-dimensional points. The native WGL viewer +saved a 2250 x 1500 JPEG with the reconstructed body mesh over the source image; +the overlay was inspected visually. Refined pose was not used in this test. + +A 16-frame excerpt (zero-based source frames 200 through 215) of OpenCV's +`vtest.avi` was written at 10 FPS and processed by the offline extractor: + +```powershell +.\build\windows\Release\offline_sam_3dbody_render.exe ` + --onnx-dir .\onnx --cuda 0 ` + --backbone backbone_fp16_trt.onnx --decoder decoder_fp16.onnx ` + --from .\build\vtest-busy.avi --max-persons 3 --thresh 0.25 ` + --bvh .\build\offline.bvh --static-scene --min-track-frames 1 ` + --no-refined-pose +``` + +It completed tracking and zero-phase smoothing, exporting two BVH tracks with +16 and 10 frames. Each motion row had all 498 declared channels; every value +was finite and the declared frame counts matched the data. The clip contains +more visible pedestrians than the detector returned; this test establishes +pipeline operation, not detection recall or reconstruction accuracy. + +A separate actual-DLL ctypes smoke test used source frame 200: capacity three +returned two people with distinct bounding boxes and finite, nonzero keypoints +and 127-joint skeletons. Capacity one returned one person. Sentinel values +following both result arrays remained intact. For this test only, the FP16 +graphs were hard-linked into a temporary model directory under the C API's +default filenames, with the original external-data sidecar filenames retained. +The public Python CLI still uses the default model names described in +[WINDOWS.md](WINDOWS.md); this does not add a Python FP16-selection option. + +ONNX Runtime JSON profiles from a separate 16-frame video run contained CUDA +execution events for all three sessions: backbone, decoder and detector. +Some nodes also ran on CPU. CUDA use was checked from provider assignments, +not inferred only from a command-line flag. + +## Inputs and limits + +The input assets were used locally and are not added to this repository: + +- [SAM 3D Body dancing sample](https://github.com/facebookresearch/sam-3d-body/blob/main/notebook/images/dancing.jpg), + SHA256 `0112b0a32ea5860db6a6fc700804751528341a02bf07fed0329a0c2610f905d3`. +- [OpenCV vtest.avi sample](https://github.com/opencv/opencv/blob/5.x/samples/data/vtest.avi), + SHA256 `45cddc9490be69345cbdab64ca583be65987e864ca408038e648db99e10516cf`. + +No inference FPS guarantee is derived from these short, varying-person-count +clips. Camera backends, TensorRT, native nvcc compilation, refined models, +Blender import, exhaustive non-ASCII path support, Linux runtime regression +and standalone installer packaging were not validated by these tests. Model +weights and CUDA/cuDNN runtime binaries are not included in the source change. diff --git a/knowledge/WSL.md b/knowledge/WSL.md index c33fab6..c46777d 100644 --- a/knowledge/WSL.md +++ b/knowledge/WSL.md @@ -171,7 +171,7 @@ Before running the pipeline, confirm the ONNX Runtime CUDA provider has all its dependencies (no output = success): ```bash -ldd build/onnxruntime_dl/lib/libonnxruntime_providers_cuda.so | grep -i "not found" +ldd build/onnxruntime_dl/onnxruntime-linux-x64-gpu-1.20.1/lib/libonnxruntime_providers_cuda.so | grep -i "not found" ``` If anything shows up here, revisit Step 4 (cuDNN) and Step 6 (paths). See diff --git a/python/fast_sam_3dbody_dump_csv.py b/python/fast_sam_3dbody_dump_csv.py index 82b4812..e8e370b 100644 --- a/python/fast_sam_3dbody_dump_csv.py +++ b/python/fast_sam_3dbody_dump_csv.py @@ -110,67 +110,7 @@ # ctypes structs (must match fast_sam_3dbody_capi.h exactly) # ────────────────────────────────────────────────────────────────────────────── -class FsbConfig(ctypes.Structure): - _fields_ = [ - ("onnx_dir", ctypes.c_char_p), - ("gguf_path", ctypes.c_char_p), - ("yolo_path", ctypes.c_char_p), - ("cuda_device", ctypes.c_int), - ("skip_body_model", ctypes.c_int), - ("person_thresh", ctypes.c_float), - ("person_nms_iou", ctypes.c_float), - ("max_persons", ctypes.c_int), - ("focal_x", ctypes.c_float), - ("focal_y", ctypes.c_float), - ("principal_x", ctypes.c_float), - ("principal_y", ctypes.c_float), - ] - - -class FsbResult(ctypes.Structure): - _fields_ = [ - ("bbox", ctypes.c_float * 4), - ("focal_length", ctypes.c_float), - ("pred_cam_t", ctypes.c_float * 3), - ("global_rot", ctypes.c_float * 3), - ("body_pose", ctypes.c_float * 133), - ("shape", ctypes.c_float * 45), - ("scale", ctypes.c_float * 28), - ("hand_pose", ctypes.c_float * 108), - ("face_params", ctypes.c_float * 72), - ("yolo_kps", ctypes.c_float * 51), - ("has_yolo_kps", ctypes.c_int), - ("kps_3d", ctypes.c_float * 210), - ("kps_2d", ctypes.c_float * 140), - ("has_kps", ctypes.c_int), - ] - - -def load_library(lib_dir: str) -> ctypes.CDLL: - lib_path = os.path.join(lib_dir, "libfast_sam_3dbody.so") - if not os.path.exists(lib_path): - sys.exit(f"Library not found: {lib_path}\nBuild the project first.") - - prev = os.environ.get("LD_LIBRARY_PATH", "") - ort_lib = os.path.join(lib_dir, "onnxruntime_dl", "lib") - os.environ["LD_LIBRARY_PATH"] = ":".join(filter(None, [lib_dir, ort_lib, prev])) - - lib = ctypes.CDLL(lib_path) - lib.fsb_create.restype = ctypes.c_void_p - lib.fsb_create.argtypes = [] - lib.fsb_destroy.restype = None - lib.fsb_destroy.argtypes = [ctypes.c_void_p] - lib.fsb_load.restype = ctypes.c_int - lib.fsb_load.argtypes = [ctypes.c_void_p, ctypes.POINTER(FsbConfig)] - lib.fsb_process_bgr.restype = ctypes.c_int - lib.fsb_process_bgr.argtypes = [ - ctypes.c_void_p, - ctypes.POINTER(ctypes.c_uint8), - ctypes.c_int, ctypes.c_int, - ctypes.POINTER(FsbResult), - ctypes.c_int, - ] - return lib +from fsb_ctypes import FsbConfig, FsbResult, load_library # ────────────────────────────────────────────────────────────────────────────── diff --git a/python/fast_sam_3dbody_dump_dpose_compat_csv.py b/python/fast_sam_3dbody_dump_dpose_compat_csv.py index 5e39613..dd2f6a5 100644 --- a/python/fast_sam_3dbody_dump_dpose_compat_csv.py +++ b/python/fast_sam_3dbody_dump_dpose_compat_csv.py @@ -157,74 +157,7 @@ def _build_hand_map(): # ctypes structs (must match fast_sam_3dbody_capi.h exactly) # ────────────────────────────────────────────────────────────────────────────── -class FsbConfig(ctypes.Structure): - _fields_ = [ - ("onnx_dir", ctypes.c_char_p), - ("gguf_path", ctypes.c_char_p), - ("yolo_path", ctypes.c_char_p), - ("cuda_device", ctypes.c_int), - ("skip_body_model", ctypes.c_int), - ("person_thresh", ctypes.c_float), - ("person_nms_iou", ctypes.c_float), - ("max_persons", ctypes.c_int), - ("focal_x", ctypes.c_float), - ("focal_y", ctypes.c_float), - ("principal_x", ctypes.c_float), - ("principal_y", ctypes.c_float), - ("zero_face_params", ctypes.c_int), - ("detector", ctypes.c_int), - ] - - -class FsbResult(ctypes.Structure): - _fields_ = [ - ("bbox", ctypes.c_float * 4), - ("focal_length", ctypes.c_float), - ("pred_cam_t", ctypes.c_float * 3), - ("global_rot", ctypes.c_float * 3), - ("body_pose", ctypes.c_float * 133), - ("shape", ctypes.c_float * 45), - ("scale", ctypes.c_float * 28), - ("hand_pose", ctypes.c_float * 108), - ("face_params", ctypes.c_float * 72), - ("yolo_kps", ctypes.c_float * 51), - ("has_yolo_kps", ctypes.c_int), - ("kps_3d", ctypes.c_float * 210), - ("kps_2d", ctypes.c_float * 140), - ("has_kps", ctypes.c_int), - ("pred_pose_raw", ctypes.c_float * 266), - ("pred_cam_raw", ctypes.c_float * 3), - ("mhr_model_params", ctypes.c_float * 204), - ("skel_3d", ctypes.c_float * 381), # [127 × 3] - ("has_skel", ctypes.c_int), - ] - - -def load_library(lib_dir: str) -> ctypes.CDLL: - lib_path = os.path.join(lib_dir, "libfast_sam_3dbody.so") - if not os.path.exists(lib_path): - sys.exit(f"Library not found: {lib_path}\nBuild the project first.") - - prev = os.environ.get("LD_LIBRARY_PATH", "") - ort_lib = os.path.join(lib_dir, "onnxruntime_dl", "lib") - os.environ["LD_LIBRARY_PATH"] = ":".join(filter(None, [lib_dir, ort_lib, prev])) - - lib = ctypes.CDLL(lib_path) - lib.fsb_create.restype = ctypes.c_void_p - lib.fsb_create.argtypes = [] - lib.fsb_destroy.restype = None - lib.fsb_destroy.argtypes = [ctypes.c_void_p] - lib.fsb_load.restype = ctypes.c_int - lib.fsb_load.argtypes = [ctypes.c_void_p, ctypes.POINTER(FsbConfig)] - lib.fsb_process_bgr.restype = ctypes.c_int - lib.fsb_process_bgr.argtypes = [ - ctypes.c_void_p, - ctypes.POINTER(ctypes.c_uint8), - ctypes.c_int, ctypes.c_int, - ctypes.POINTER(FsbResult), - ctypes.c_int, - ] - return lib +from fsb_ctypes import FsbConfig, FsbResult, load_library # ────────────────────────────────────────────────────────────────────────────── diff --git a/python/fast_sam_3dbody_frontend-3D.py b/python/fast_sam_3dbody_frontend-3D.py index 9806931..7025663 100644 --- a/python/fast_sam_3dbody_frontend-3D.py +++ b/python/fast_sam_3dbody_frontend-3D.py @@ -31,67 +31,7 @@ # ctypes structs (must match fast_sam_3dbody_capi.h exactly) # ────────────────────────────────────────────────────────────────────────────── -class FsbConfig(ctypes.Structure): - _fields_ = [ - ("onnx_dir", ctypes.c_char_p), - ("gguf_path", ctypes.c_char_p), - ("yolo_path", ctypes.c_char_p), - ("cuda_device", ctypes.c_int), - ("skip_body_model", ctypes.c_int), - ("person_thresh", ctypes.c_float), - ("person_nms_iou", ctypes.c_float), - ("max_persons", ctypes.c_int), - ("focal_x", ctypes.c_float), - ("focal_y", ctypes.c_float), - ("principal_x", ctypes.c_float), - ("principal_y", ctypes.c_float), - ] - - -class FsbResult(ctypes.Structure): - _fields_ = [ - ("bbox", ctypes.c_float * 4), - ("focal_length", ctypes.c_float), - ("pred_cam_t", ctypes.c_float * 3), # camera translation [tx, ty, tz] in camera space - ("global_rot", ctypes.c_float * 3), - ("body_pose", ctypes.c_float * 133), - ("shape", ctypes.c_float * 45), - ("scale", ctypes.c_float * 28), - ("hand_pose", ctypes.c_float * 108), - ("face_params", ctypes.c_float * 72), - ("yolo_kps", ctypes.c_float * 51), - ("has_yolo_kps", ctypes.c_int), - ("kps_3d", ctypes.c_float * 210), - ("kps_2d", ctypes.c_float * 140), - ("has_kps", ctypes.c_int), - ] - - -def load_library(lib_dir: str) -> ctypes.CDLL: - lib_path = os.path.join(lib_dir, "libfast_sam_3dbody.so") - if not os.path.exists(lib_path): - sys.exit(f"Library not found: {lib_path}\nBuild the C++ project first.") - - prev = os.environ.get("LD_LIBRARY_PATH", "") - ort_lib = os.path.join(lib_dir, "onnxruntime_dl", "lib") - os.environ["LD_LIBRARY_PATH"] = ":".join(filter(None, [lib_dir, ort_lib, prev])) - - lib = ctypes.CDLL(lib_path) - lib.fsb_create.restype = ctypes.c_void_p - lib.fsb_create.argtypes = [] - lib.fsb_destroy.restype = None - lib.fsb_destroy.argtypes = [ctypes.c_void_p] - lib.fsb_load.restype = ctypes.c_int - lib.fsb_load.argtypes = [ctypes.c_void_p, ctypes.POINTER(FsbConfig)] - lib.fsb_process_bgr.restype = ctypes.c_int - lib.fsb_process_bgr.argtypes = [ - ctypes.c_void_p, - ctypes.POINTER(ctypes.c_uint8), - ctypes.c_int, ctypes.c_int, - ctypes.POINTER(FsbResult), - ctypes.c_int, - ] - return lib +from fsb_ctypes import FsbConfig, FsbResult, load_library # ────────────────────────────────────────────────────────────────────────────── diff --git a/python/fast_sam_3dbody_frontend.py b/python/fast_sam_3dbody_frontend.py index 005bb74..c91abb9 100644 --- a/python/fast_sam_3dbody_frontend.py +++ b/python/fast_sam_3dbody_frontend.py @@ -3,7 +3,7 @@ fast_sam_3dbody_frontend.py Python frontend for the C++ SAM-3D-Body pipeline. -Loads libfast_sam_3dbody.so via ctypes, runs inference, +Loads the native shared library via ctypes, runs inference, and draws COCO skeleton + MHR pose info on the frame. Usage: @@ -29,83 +29,7 @@ # ctypes structs matching fast_sam_3dbody_capi.h # ────────────────────────────────────────────────────────────────────────────── -class FsbConfig(ctypes.Structure): - _fields_ = [ - ("onnx_dir", ctypes.c_char_p), - ("gguf_path", ctypes.c_char_p), - ("yolo_path", ctypes.c_char_p), - ("cuda_device", ctypes.c_int), - ("skip_body_model",ctypes.c_int), - ("person_thresh", ctypes.c_float), - ("person_nms_iou", ctypes.c_float), - ("max_persons", ctypes.c_int), - ("focal_x", ctypes.c_float), - ("focal_y", ctypes.c_float), - ("principal_x", ctypes.c_float), - ("principal_y", ctypes.c_float), - ("zero_face_params", ctypes.c_int), # 0/1 — force face expression to neutral - ] - -class FsbResult(ctypes.Structure): - _fields_ = [ - ("bbox", ctypes.c_float * 4), - ("focal_length", ctypes.c_float), - ("pred_cam_t", ctypes.c_float * 3), - ("global_rot", ctypes.c_float * 3), - ("body_pose", ctypes.c_float * 133), - ("shape", ctypes.c_float * 45), - ("scale", ctypes.c_float * 28), - ("hand_pose", ctypes.c_float * 108), - ("face_params", ctypes.c_float * 72), - ("yolo_kps", ctypes.c_float * 51), - ("has_yolo_kps", ctypes.c_int), - ("kps_3d", ctypes.c_float * 210), - ("kps_2d", ctypes.c_float * 140), - ("has_kps", ctypes.c_int), - # ── Second-pass raw fields (must stay at end — appended after v1 ABI) ── - # pred_pose_raw[266]: raw MHR FFN output, layout global_rot_6d[6] + body_cont[260]. - # pred_cam_raw[3]: raw cam FFN output [s, tx, ty] before nonlinear decode. - # Both are consumed by two_pass.py to build prev_estimate for forward_decoder. - # HIGH RISK: any offset here shifts ALL ctypes reads for this struct. - ("pred_pose_raw", ctypes.c_float * 266), - ("pred_cam_raw", ctypes.c_float * 3), - # mhr_model_params[204]: assembled model_params used by native C LBS. - # Layout: [0:3]=global_trans*10, [3:6]=global_rot ZYX, [6:136]=body_pose[:130], - # [136:204]=scale_out. Mirrors Python mhr_forward(return_model_params=True). - ("mhr_model_params", ctypes.c_float * 204), - ] - - -def load_library(lib_dir: str) -> ctypes.CDLL: - lib_path = os.path.join(lib_dir, "libfast_sam_3dbody.so") - if not os.path.exists(lib_path): - sys.exit(f"Library not found: {lib_path}\nBuild the project first.") - - # Add the lib directory to LD_LIBRARY_PATH so transitive .so deps are found - prev = os.environ.get("LD_LIBRARY_PATH", "") - ort_lib = os.path.join(lib_dir, "onnxruntime_dl", "lib") - os.environ["LD_LIBRARY_PATH"] = ":".join(filter(None, [lib_dir, ort_lib, prev])) - - lib = ctypes.CDLL(lib_path) - - lib.fsb_create.restype = ctypes.c_void_p - lib.fsb_create.argtypes = [] - - lib.fsb_destroy.restype = None - lib.fsb_destroy.argtypes = [ctypes.c_void_p] - - lib.fsb_load.restype = ctypes.c_int - lib.fsb_load.argtypes = [ctypes.c_void_p, ctypes.POINTER(FsbConfig)] - - lib.fsb_process_bgr.restype = ctypes.c_int - lib.fsb_process_bgr.argtypes = [ - ctypes.c_void_p, - ctypes.POINTER(ctypes.c_uint8), - ctypes.c_int, ctypes.c_int, - ctypes.POINTER(FsbResult), - ctypes.c_int, - ] - return lib +from fsb_ctypes import FsbConfig, FsbResult, load_library # ────────────────────────────────────────────────────────────────────────────── @@ -433,7 +357,7 @@ def parse_args(): build = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "build") p.add_argument("--lib-dir", default=build, - help="Directory containing libfast_sam_3dbody.so") + help="CMake build/output directory containing the native shared library") p.add_argument("--onnx-dir", default=onnx) p.add_argument("--gguf", default=os.path.join(onnx, "pipeline.gguf")) p.add_argument("--yolo", default=os.path.join(onnx, "yolo.onnx")) diff --git a/python/fsb_ctypes.py b/python/fsb_ctypes.py new file mode 100644 index 0000000..05345bc --- /dev/null +++ b/python/fsb_ctypes.py @@ -0,0 +1,163 @@ +"""Shared, ABI-checked ctypes binding for the SAM3DBody C API. + +Keep these structs synchronized with fast_sam_3dbody_capi.h. The native ABI +version must increase even when fields are only appended: array strides change. +This module has no dependency on NumPy, OpenCV, or PyTorch. +""" + +import ctypes +import os +from pathlib import Path +import sys + + +ABI_VERSION = 1 + + +class FsbConfig(ctypes.Structure): + _fields_ = [ + ("onnx_dir", ctypes.c_char_p), + ("gguf_path", ctypes.c_char_p), + ("yolo_path", ctypes.c_char_p), + ("cuda_device", ctypes.c_int), + ("skip_body_model", ctypes.c_int), + ("person_thresh", ctypes.c_float), + ("person_nms_iou", ctypes.c_float), + ("max_persons", ctypes.c_int), + ("focal_x", ctypes.c_float), + ("focal_y", ctypes.c_float), + ("principal_x", ctypes.c_float), + ("principal_y", ctypes.c_float), + ("zero_face_params", ctypes.c_int), + ("detector", ctypes.c_int), + ] + + +class FsbResult(ctypes.Structure): + _fields_ = [ + ("bbox", ctypes.c_float * 4), + ("focal_length", ctypes.c_float), + ("pred_cam_t", ctypes.c_float * 3), + ("global_rot", ctypes.c_float * 3), + ("body_pose", ctypes.c_float * 133), + ("shape", ctypes.c_float * 45), + ("scale", ctypes.c_float * 28), + ("hand_pose", ctypes.c_float * 108), + ("face_params", ctypes.c_float * 72), + ("yolo_kps", ctypes.c_float * 51), + ("has_yolo_kps", ctypes.c_int), + ("kps_3d", ctypes.c_float * 210), + ("kps_2d", ctypes.c_float * 140), + ("has_kps", ctypes.c_int), + ("pred_pose_raw", ctypes.c_float * 266), + ("pred_cam_raw", ctypes.c_float * 3), + ("mhr_model_params", ctypes.c_float * 204), + ("skel_3d", ctypes.c_float * 381), + ("has_skel", ctypes.c_int), + ] + + +def _check_abi(lib): + """Reject stale/incompatible libraries before they can read or write structs.""" + checks = ( + ("fsb_abi_version", ctypes.c_uint, ABI_VERSION), + ("fsb_config_size", ctypes.c_size_t, ctypes.sizeof(FsbConfig)), + ("fsb_result_size", ctypes.c_size_t, ctypes.sizeof(FsbResult)), + ) + for name, result_type, expected in checks: + try: + query = getattr(lib, name) + except AttributeError as exc: + raise RuntimeError( + f"Native library is missing {name}; rebuild it from the same " + "checkout as the Python frontend." + ) from exc + query.argtypes = [] + query.restype = result_type + actual = query() + if actual != expected: + raise RuntimeError( + f"Native ABI mismatch: {name} returned {actual}, expected " + f"{expected}. Rebuild the library and use matching Python files." + ) + + +def load_library(lib_dir: str) -> ctypes.CDLL: + """Load a CMake build directory (or a shared-library path) on Windows/Linux.""" + root = Path(lib_dir).expanduser().resolve() + if sys.platform == "win32": + names = ("fast_sam_3dbody.dll", "libfast_sam_3dbody.dll") + elif sys.platform == "darwin": + names = ("libfast_sam_3dbody.dylib",) + else: + names = ("libfast_sam_3dbody.so",) + + if root.is_file(): + lib_path = root + root = root.parent + else: + directories = [root] + [root / config for config in ( + "Release", "RelWithDebInfo", "Debug", "MinSizeRel" + )] + candidates = [directory / name for directory in directories for name in names] + lib_path = next((path for path in candidates if path.is_file()), None) + if lib_path is None: + raise FileNotFoundError( + f"Native library not found under {root}; expected {names[0]}. " + "Build the project first, or set --lib-dir to its output directory." + ) + + dll_directories = [] + if sys.platform == "win32": + # Python 3.8+ ignores PATH for dependent DLL lookup unless directories are + # explicitly registered. Retain handles for providers loaded after CDLL. + search_dirs = [lib_path.parent, root] + if not (lib_path.parent / "onnxruntime.dll").is_file(): + build_root = root.parent if root.name in ( + "Release", "RelWithDebInfo", "Debug", "MinSizeRel" + ) else root + cache_root = build_root / "onnxruntime_dl" + packages = sorted(path for path in cache_root.glob("onnxruntime-win-x64*") + if path.is_dir() and any( + (path / folder / "onnxruntime.dll").is_file() + for folder in ("lib", "bin"))) + # Windows does not specify an order among AddDllDirectory entries. + # Never expose both cached CPU and GPU ORT DLLs to that search. + if len(packages) > 1: + raise OSError( + "Multiple ONNX Runtime packages are cached, but the selected " + "output has no staged onnxruntime.dll. Rebuild the target " + "and load its output directory so the matching DLL is used." + ) + ort_root = packages[0] if packages else cache_root # legacy flat cache + search_dirs += [ort_root / "lib", ort_root / "bin"] + search_dirs += [Path(part) for part in os.environ.get("PATH", "").split(os.pathsep) + if part] + seen = set() + for directory in search_dirs: + directory = directory.resolve() + if directory.is_dir() and directory not in seen: + dll_directories.append(os.add_dll_directory(str(directory))) + seen.add(directory) + + try: + # The exported C API uses cdecl, including on Windows: use CDLL, not WinDLL. + lib = ctypes.CDLL(str(lib_path)) + _check_abi(lib) + lib.fsb_create.restype = ctypes.c_void_p + lib.fsb_create.argtypes = [] + lib.fsb_destroy.restype = None + lib.fsb_destroy.argtypes = [ctypes.c_void_p] + lib.fsb_load.restype = ctypes.c_int + lib.fsb_load.argtypes = [ctypes.c_void_p, ctypes.POINTER(FsbConfig)] + lib.fsb_process_bgr.restype = ctypes.c_int + lib.fsb_process_bgr.argtypes = [ + ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), + ctypes.c_int, ctypes.c_int, ctypes.POINTER(FsbResult), ctypes.c_int, + ] + except Exception: + for directory in dll_directories: + directory.close() + raise + lib._fsb_dll_directories = dll_directories + return lib diff --git a/python/ros_demo_webcam.py b/python/ros_demo_webcam.py index 633a22a..0671c7b 100644 --- a/python/ros_demo_webcam.py +++ b/python/ros_demo_webcam.py @@ -138,67 +138,7 @@ # ctypes structs (must match fast_sam_3dbody_capi.h exactly) # ────────────────────────────────────────────────────────────────────────────── -class FsbConfig(ctypes.Structure): - _fields_ = [ - ("onnx_dir", ctypes.c_char_p), - ("gguf_path", ctypes.c_char_p), - ("yolo_path", ctypes.c_char_p), - ("cuda_device", ctypes.c_int), - ("skip_body_model", ctypes.c_int), - ("person_thresh", ctypes.c_float), - ("person_nms_iou", ctypes.c_float), - ("max_persons", ctypes.c_int), - ("focal_x", ctypes.c_float), - ("focal_y", ctypes.c_float), - ("principal_x", ctypes.c_float), - ("principal_y", ctypes.c_float), - ] - - -class FsbResult(ctypes.Structure): - _fields_ = [ - ("bbox", ctypes.c_float * 4), - ("focal_length", ctypes.c_float), - ("pred_cam_t", ctypes.c_float * 3), - ("global_rot", ctypes.c_float * 3), - ("body_pose", ctypes.c_float * 133), - ("shape", ctypes.c_float * 45), - ("scale", ctypes.c_float * 28), - ("hand_pose", ctypes.c_float * 108), - ("face_params", ctypes.c_float * 72), - ("yolo_kps", ctypes.c_float * 51), - ("has_yolo_kps", ctypes.c_int), - ("kps_3d", ctypes.c_float * 210), - ("kps_2d", ctypes.c_float * 140), - ("has_kps", ctypes.c_int), - ] - - -def load_library(lib_dir: str) -> ctypes.CDLL: - lib_path = os.path.join(lib_dir, "libfast_sam_3dbody.so") - if not os.path.exists(lib_path): - sys.exit(f"Library not found: {lib_path}\nBuild the project first.") - - prev = os.environ.get("LD_LIBRARY_PATH", "") - ort_lib = os.path.join(lib_dir, "onnxruntime_dl", "lib") - os.environ["LD_LIBRARY_PATH"] = ":".join(filter(None, [lib_dir, ort_lib, prev])) - - lib = ctypes.CDLL(lib_path) - lib.fsb_create.restype = ctypes.c_void_p - lib.fsb_create.argtypes = [] - lib.fsb_destroy.restype = None - lib.fsb_destroy.argtypes = [ctypes.c_void_p] - lib.fsb_load.restype = ctypes.c_int - lib.fsb_load.argtypes = [ctypes.c_void_p, ctypes.POINTER(FsbConfig)] - lib.fsb_process_bgr.restype = ctypes.c_int - lib.fsb_process_bgr.argtypes = [ - ctypes.c_void_p, - ctypes.POINTER(ctypes.c_uint8), - ctypes.c_int, ctypes.c_int, - ctypes.POINTER(FsbResult), - ctypes.c_int, - ] - return lib +from fsb_ctypes import FsbConfig, FsbResult, load_library def rotmat_to_quat(R: np.ndarray) -> np.ndarray: @@ -549,7 +489,7 @@ def parse_arguments(): # C library / model paths p.add_argument('--lib-dir', default=os.path.join(cpp_dir, 'build'), - help='Directory containing libfast_sam_3dbody.so') + help='CMake build/output directory containing the native shared library') p.add_argument('--onnx-dir', default=os.path.join(cpp_dir, 'onnx'), help='Directory containing ONNX/GGUF model files') p.add_argument('--gguf', default=None, help='Override path to pipeline.gguf') diff --git a/scripts/build_windows.ps1 b/scripts/build_windows.ps1 new file mode 100644 index 0000000..3a66d5e --- /dev/null +++ b/scripts/build_windows.ps1 @@ -0,0 +1,42 @@ +#Requires -Version 5.1 +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$OpenCVDir, + [string]$OnnxRuntimeDir, + [string]$GLEWRoot, + [string]$BuildDir, + [string]$Generator = 'Visual Studio 17 2022', + [ValidateSet('Release', 'RelWithDebInfo', 'Debug')][string]$Configuration = 'Release', + [string]$CudaArchitectures, + [ValidateRange(1, 64)][int]$Jobs = 4, + [switch]$Gpu, + [switch]$Headless, + [switch]$TestOpenGL, + [switch]$SkipTests, + [string[]]$CMakeArgs = @() +) +$ErrorActionPreference = 'Stop' +$repoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path +if (-not $BuildDir) { $BuildDir = Join-Path $repoRoot 'build\windows' } +if (-not (Get-Command cmake -ErrorAction SilentlyContinue)) { throw 'Install CMake 3.21+ and add it to PATH.' } +$opencvPath = (Resolve-Path -LiteralPath $OpenCVDir).Path +$configureArgs = @('-S', $repoRoot, '-B', $BuildDir, '-G', $Generator, + '-DFETCHCONTENT_UPDATES_DISCONNECTED=ON', + "-DOpenCV_DIR=$opencvPath", "-DCMAKE_BUILD_TYPE=$Configuration", + "-DSAM3D_ONNX_CUDA=$(@{ $true='ON'; $false='OFF' }[[bool]$Gpu])", + "-DSAM3D_BUILD_RENDERER=$(@{ $true='OFF'; $false='ON' }[[bool]$Headless])", + "-DSAM3D_TEST_OPENGL=$(@{ $true='ON'; $false='OFF' }[[bool]$TestOpenGL])") +if ($Generator -like 'Visual Studio*') { $configureArgs += @('-A', 'x64') } +if ($OnnxRuntimeDir) { $configureArgs += "-DONNX_RUNTIME_DIR=$((Resolve-Path -LiteralPath $OnnxRuntimeDir).Path)" } +if ($GLEWRoot) { $configureArgs += "-DGLEW_ROOT=$((Resolve-Path -LiteralPath $GLEWRoot).Path)" } +if ($CudaArchitectures) { $configureArgs += "-DCMAKE_CUDA_ARCHITECTURES=$CudaArchitectures" } +$configureArgs += $CMakeArgs +& cmake @configureArgs +if ($LASTEXITCODE -ne 0) { throw "CMake configure failed ($LASTEXITCODE)." } +& cmake --build $BuildDir --config $Configuration --parallel $Jobs +if ($LASTEXITCODE -ne 0) { throw "Windows build failed ($LASTEXITCODE)." } +if (-not $SkipTests) { + & ctest --test-dir $BuildDir -C $Configuration --output-on-failure + if ($LASTEXITCODE -ne 0) { throw "Windows regression tests failed ($LASTEXITCODE)." } +} +Write-Host "Windows $Configuration build is ready in $BuildDir. Models are downloaded separately with tools/fetch_model.ps1." diff --git a/src/AmMatrix/matrix4x4Tools.c b/src/AmMatrix/matrix4x4Tools.c index e95ae0c..b411a37 100644 --- a/src/AmMatrix/matrix4x4Tools.c +++ b/src/AmMatrix/matrix4x4Tools.c @@ -79,7 +79,7 @@ enum mat4x4EItem 12 13 14 15 */ -const float __attribute__((aligned(16))) identityMatrix4x4[16]={1.0,0.0,0.0,0.0, +AMMATRIX_ALIGN16 const float identityMatrix4x4[16]={1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,0.0,0.0,1.0}; @@ -1581,7 +1581,7 @@ static inline void multiplyTwo4x4FMatrices_SSE3(float * result ,const float * ma __m128 matrixA_r3 = _mm_load_ps(&matrixA[12]); /* //Instead of _MM_TRANSPOSE4_PS we can transpose the matrixB naively.. kills us @ 0.98% time.. - float __attribute__((aligned(16))) transposedMatrixB[16]={ + AMMATRIX_ALIGN16 float transposedMatrixB[16]={ matrixB[0],matrixB[4],matrixB[8] ,matrixB[12], matrixB[1],matrixB[5],matrixB[9] ,matrixB[13], matrixB[2],matrixB[6],matrixB[10],matrixB[14], diff --git a/src/AmMatrix/matrix4x4Tools.h b/src/AmMatrix/matrix4x4Tools.h index 6502ca9..27c4fc5 100644 --- a/src/AmMatrix/matrix4x4Tools.h +++ b/src/AmMatrix/matrix4x4Tools.h @@ -6,6 +6,11 @@ #ifndef MATRIX4X4TOOLS_H_INCLUDED #define MATRIX4X4TOOLS_H_INCLUDED +#if defined(_MSC_VER) +#define AMMATRIX_ALIGN16 __declspec(align(16)) +#else +#define AMMATRIX_ALIGN16 __attribute__((aligned(16))) +#endif #ifdef __cplusplus extern "C" @@ -65,7 +70,7 @@ struct Matrix4x4OfFloats I31 , I32 , I33 , I34 , I41 , I42 , I43 , I44 */ - float __attribute__((aligned(16))) m[16]; + AMMATRIX_ALIGN16 float m[16]; }; @@ -79,7 +84,7 @@ struct Vector4x1OfFloats IRC => Item Row/Column => I11, I12, I13, I14 */ - float __attribute__((aligned(16))) m[4]; + AMMATRIX_ALIGN16 float m[4]; }; diff --git a/src/GraphicsEngine/ModelLoader/model_loader_tri.c b/src/GraphicsEngine/ModelLoader/model_loader_tri.c index a70f6d1..7ff0283 100644 --- a/src/GraphicsEngine/ModelLoader/model_loader_tri.c +++ b/src/GraphicsEngine/ModelLoader/model_loader_tri.c @@ -608,7 +608,7 @@ int tri_loadModel(const char * filename , struct TRI_Model * triModel) if (triModel->header.floatSize!=sizeof(float)) { - fprintf(stderr,"Size of float (%u/%lu) is different , cannot load \n",triModel->header.floatSize,sizeof(float)); + fprintf(stderr,"Size of float (%u/%zu) is different , cannot load \n",triModel->header.floatSize,sizeof(float)); fclose(fd); return 0; } diff --git a/src/GraphicsEngine/MotionCaptureLoader/calculate/bvh_to_tri_pose.c b/src/GraphicsEngine/MotionCaptureLoader/calculate/bvh_to_tri_pose.c index 1615839..593aed8 100644 --- a/src/GraphicsEngine/MotionCaptureLoader/calculate/bvh_to_tri_pose.c +++ b/src/GraphicsEngine/MotionCaptureLoader/calculate/bvh_to_tri_pose.c @@ -1,6 +1,7 @@ #include #include #include "bvh_to_tri_pose.h" +#include "../../System/portable_getline.h" #include "../../TrajectoryParser/InputParser_C.h" @@ -187,7 +188,7 @@ int bvh_loadBVHToTRIAssociationFile( InputParser_SetDelimeter(ipc,6,13); - ssize_t read; + ptrdiff_t read; char * line = NULL; size_t len = 0; @@ -196,7 +197,7 @@ int bvh_loadBVHToTRIAssociationFile( bvhtri->numberOfJointAssociations=0; unsigned int jID=0; - while ((read = getline(&line, &len, fp)) != -1) + while ((read = fsb_getline(&line, &len, fp)) != -1) { int num = InputParser_SeperateWords(ipc,line,1); diff --git a/src/GraphicsEngine/MotionCaptureLoader/edit/bvh_cut_paste.c b/src/GraphicsEngine/MotionCaptureLoader/edit/bvh_cut_paste.c index c10f4e6..9b7c6c6 100644 --- a/src/GraphicsEngine/MotionCaptureLoader/edit/bvh_cut_paste.c +++ b/src/GraphicsEngine/MotionCaptureLoader/edit/bvh_cut_paste.c @@ -206,7 +206,7 @@ int bvh_GrowMocapFileByCopyingOtherMocapFile( if (mcSource->motionValuesSize==0) { fprintf(stderr,"Data to repeat has zero data size\n"); return 0; } //------------------------------------------------------------------------------------------------- fprintf(stderr,"Asked to copy %s to %s\n",mc->fileName,mcSource->fileName); - fprintf(stderr,"Will now try to allocate %lu KB of memory\n",(sizeof(float) * (mc->motionValuesSize+ mcSource->motionValuesSize)) / 1024); + fprintf(stderr,"Will now try to allocate %zu KB of memory\n",(sizeof(float) * (mc->motionValuesSize+ mcSource->motionValuesSize)) / 1024); if (mc->jointHierarchySize!=mcSource->jointHierarchySize) { @@ -263,7 +263,7 @@ int bvh_GrowMocapFileByCopyingExistingMotions( if (mc->motionValuesSize==0) { fprintf(stderr,"Data to repeat has zero data size\n"); return 0; } //------------------------------------------------------------------------------------------------- fprintf(stderr,"Asked to repeat %u times the %u existing motion records\n",timesToRepeat,mc->numberOfFramesEncountered); - fprintf(stderr,"Will now try to allocate %lu KB of memory\n",(sizeof(float) * mc->motionValuesSize * (timesToRepeat+1)) / 1024); + fprintf(stderr,"Will now try to allocate %zu KB of memory\n",(sizeof(float) * mc->motionValuesSize * (timesToRepeat+1)) / 1024); float * newMotionValues = (float*) malloc(sizeof(float) * mc->motionValuesSize * (timesToRepeat+1) ); if (newMotionValues==0) { fprintf(stderr,"Could not allocate new motion values\n"); return 0; } diff --git a/src/GraphicsEngine/MotionCaptureLoader/edit/cTextFileToMemory.h b/src/GraphicsEngine/MotionCaptureLoader/edit/cTextFileToMemory.h index 5df765a..b08210e 100644 --- a/src/GraphicsEngine/MotionCaptureLoader/edit/cTextFileToMemory.h +++ b/src/GraphicsEngine/MotionCaptureLoader/edit/cTextFileToMemory.h @@ -202,7 +202,7 @@ static int ctftm_loadTextFileToMemory(struct cTextFileToMemory * ctftm, const ch if (ctftm->strings!=0) { - ssize_t read = ctftm_parselines(ctftm); + unsigned int read = ctftm_parselines(ctftm); return (read>0); } } diff --git a/src/GraphicsEngine/MotionCaptureLoader/import/fromBVH.c b/src/GraphicsEngine/MotionCaptureLoader/import/fromBVH.c index aee937e..06b7a37 100644 --- a/src/GraphicsEngine/MotionCaptureLoader/import/fromBVH.c +++ b/src/GraphicsEngine/MotionCaptureLoader/import/fromBVH.c @@ -1,4 +1,5 @@ #include "fromBVH.h" +#include "../../System/portable_getline.h" #include #include "../../TrajectoryParser/InputParser_C.h" @@ -203,10 +204,10 @@ int fastBVHFileToDetermineNumberOfJointsAndMotionFields(struct BVH_MotionCapture unsigned int hierarchyLevel=0; char * line = NULL; size_t len = 0; - ssize_t read; + ptrdiff_t read; int done=0; - while ( (!done) && ((read = getline(&line, &len, fd)) != -1) ) + while ( (!done) && ((read = fsb_getline(&line, &len, fd)) != -1) ) { ++bvhMotion->linesParsed; //printf("Retrieved line of length %zu :\n %s", read,line); @@ -393,10 +394,10 @@ int readBVHHeader(struct BVH_MotionCapture * bvhMotion,FILE * fd) unsigned int hierarchyLevel=0; char * line = NULL; size_t len = 0; - ssize_t read; + ptrdiff_t read; int done=0; - while ( (!done) && ((read = getline(&line, &len, fd)) != -1) ) + while ( (!done) && ((read = fsb_getline(&line, &len, fd)) != -1) ) { ++bvhMotion->linesParsed; //printf("Retrieved line of length %zu :\n %s", read,line); @@ -722,7 +723,7 @@ int readBVHHeader(struct BVH_MotionCapture * bvhMotion,FILE * fd) else { //Unexpected input.. - fprintf(stderr,"BVH Header, Unexpected line num (%u) of length %zd :\n" , bvhMotion->linesParsed , read); + fprintf(stderr,"BVH Header, Unexpected line num (%u) of length %td :\n" , bvhMotion->linesParsed , read); fprintf(stderr,"%s\n", line); //exit(0); } @@ -797,9 +798,9 @@ int readBVHMotion(struct BVH_MotionCapture * bvhMotion , FILE * fd ) char str[MAX_BVH_FILE_LINE_SIZE+1]={0}; char * line = NULL; size_t len = 0; - ssize_t read; + ptrdiff_t read; - while ((read = getline(&line, &len, fd)) != -1) + while ((read = fsb_getline(&line, &len, fd)) != -1) { ++bvhMotion->linesParsed; diff --git a/src/GraphicsEngine/System/portable_getline.h b/src/GraphicsEngine/System/portable_getline.h new file mode 100644 index 0000000..5af02e3 --- /dev/null +++ b/src/GraphicsEngine/System/portable_getline.h @@ -0,0 +1,47 @@ +#ifndef FSB_PORTABLE_GETLINE_H +#define FSB_PORTABLE_GETLINE_H + +#include +#include + +#ifdef _MSC_VER +#include +#include +#include + +/* getline's growing buffer and final unterminated line semantics, for the + * MSVC CRT. Use a private name instead of introducing POSIX types globally. */ +static ptrdiff_t fsb_getline(char **line, size_t *capacity, FILE *stream) +{ + size_t length = 0; + int ch; + if (!line || !capacity || !stream) { + errno = EINVAL; + return -1; + } + if (!*line) *capacity = 0; + while ((ch = fgetc(stream)) != EOF) { + if (length + 1 >= *capacity) { + size_t next = *capacity ? *capacity * 2 : 256; + char *grown; + if (next <= *capacity || next > PTRDIFF_MAX) { + errno = ENOMEM; + return -1; + } + grown = (char *)realloc(*line, next); + if (!grown) return -1; + *line = grown; + *capacity = next; + } + (*line)[length++] = (char)ch; + if (ch == '\n') break; + } + if (length == 0 || ferror(stream)) return -1; + (*line)[length] = '\0'; + return (ptrdiff_t)length; +} +#else +#define fsb_getline getline +#endif + +#endif diff --git a/src/GraphicsEngine/System/wgl3.c b/src/GraphicsEngine/System/wgl3.c new file mode 100644 index 0000000..07c7d08 --- /dev/null +++ b/src/GraphicsEngine/System/wgl3.c @@ -0,0 +1,232 @@ +/* Win32/WGL implementation of the existing glx3.h window interface. + * Adapted from beemsoft's Windows port (MIT), PR #13: + * https://github.com/AmmarkoV/SAM3DBody-cpp/pull/13 + * Extended for OpenGL 3.3, the current title/callback API, and checked teardown. + */ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include +#include +#include +#include +#include "glx3.h" + +#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091 +#define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092 +#define WGL_CONTEXT_PROFILE_MASK_ARB 0x9126 +#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001 + +typedef HGLRC (WINAPI *CreateContextAttribsProc)(HDC, HGLRC, const int *); +typedef BOOL (WINAPI *SwapIntervalProc)(int); + +extern int handleUserInput(int key, int state, int x, int y); +extern int windowSizeUpdated(unsigned int width, unsigned int height); + +static const wchar_t class_name[] = L"SAM3DBodyWGLWindow"; +static const wchar_t default_title[] = L"SAM3DBody-cpp OpenGL3.x+ Visualization"; +static wchar_t window_title[256] = L"SAM3DBody-cpp OpenGL3.x+ Visualization"; +static HWND window_handle = NULL; +static HDC device_context = NULL; +static HGLRC render_context = NULL; +static int close_requested = 0; +static int swap_interval = 1; + +/* Some Windows drivers return a non-NULL sentinel for an absent extension. */ +static PROC get_wgl_proc(const char *name) +{ + PROC proc = wglGetProcAddress(name); + intptr_t value = (intptr_t)proc; + return (value == 0 || value == 1 || value == 2 || value == 3 || value == -1) + ? NULL : proc; +} + +static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM key, LPARAM data) +{ + switch (message) { + case WM_CLOSE: + /* Keep the surface alive until the renderer saves outputs and tears down. */ + glx3_request_close(); + return 0; + case WM_DESTROY: + glx3_request_close(); + return 0; + case WM_KEYDOWN: + case WM_KEYUP: + if (key == VK_ESCAPE && message == WM_KEYDOWN) glx3_request_close(); + else handleUserInput((int)key, message == WM_KEYDOWN, 0, 0); + return 0; + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + case WM_MBUTTONDOWN: + case WM_MBUTTONUP: + case WM_RBUTTONDOWN: + case WM_RBUTTONUP: + { + int button = (message == WM_LBUTTONDOWN || message == WM_LBUTTONUP) ? 1 : + (message == WM_MBUTTONDOWN || message == WM_MBUTTONUP) ? 2 : 3; + int pressed = message == WM_LBUTTONDOWN || message == WM_MBUTTONDOWN || + message == WM_RBUTTONDOWN; + POINT position = { GET_X_LPARAM(data), GET_Y_LPARAM(data) }; + ClientToScreen(window, &position); + handleUserInput(button, pressed, position.x, position.y); + return 0; + } + case WM_SIZE: + if (key != SIZE_MINIMIZED) windowSizeUpdated(LOWORD(data), HIWORD(data)); + return 0; + case WM_ERASEBKGND: + return 1; + } + return DefWindowProcW(window, message, key, data); +} + +void glx3_set_window_title(const char *title) +{ + const int capacity = (int)(sizeof(window_title) / sizeof(window_title[0])); + if (title && *title) { + if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + title, -1, window_title, capacity)) return; + /* main(char **) receives ANSI argv on Windows unless the executable + * explicitly opts into UTF-8. Preserve native CLI titles as well. */ + if (MultiByteToWideChar(CP_ACP, 0, title, -1, window_title, capacity)) return; + } + wcscpy_s(window_title, capacity, default_title); +} + +int disableVSync(void) +{ + swap_interval = 0; + if (render_context) { + SwapIntervalProc swap = (SwapIntervalProc)get_wgl_proc("wglSwapIntervalEXT"); + return swap ? (int)swap(0) : 0; + } + return 1; /* Apply the request after context creation. */ +} + +int stop_glx3_stuff(void) +{ + if (render_context) { + wglMakeCurrent(NULL, NULL); + wglDeleteContext(render_context); + render_context = NULL; + } + if (device_context) { + ReleaseDC(window_handle, device_context); + device_context = NULL; + } + if (window_handle) { + DestroyWindow(window_handle); + window_handle = NULL; + } + UnregisterClassW(class_name, GetModuleHandleW(NULL)); + close_requested = 1; + return 1; +} + +int start_glx3_stuff(int width, int height, int viewWindow, int argc, const char **argv) +{ + HINSTANCE instance = GetModuleHandleW(NULL); + WNDCLASSW window_class = {0}; + PIXELFORMATDESCRIPTOR pixel_format = {0}; + RECT rectangle = {0, 0, width, height}; + DWORD style = viewWindow ? WS_OVERLAPPEDWINDOW : WS_POPUP; + CreateContextAttribsProc create_context; + HGLRC modern_context; + int format; + const int context_attributes[] = { + WGL_CONTEXT_MAJOR_VERSION_ARB, 3, + WGL_CONTEXT_MINOR_VERSION_ARB, 3, + WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB, 0 + }; + (void)argc; + (void)argv; + + if (window_handle || render_context) stop_glx3_stuff(); + close_requested = 0; + if (width <= 0 || height <= 0) { + fprintf(stderr, "WGL: invalid surface size %dx%d\n", width, height); + close_requested = 1; + return 0; + } + + window_class.style = CS_OWNDC; + window_class.lpfnWndProc = window_proc; + window_class.hInstance = instance; + window_class.lpszClassName = class_name; + window_class.hCursor = LoadCursorW(NULL, MAKEINTRESOURCEW(32512)); /* IDC_ARROW */ + if (!RegisterClassW(&window_class)) goto fail; + if (!AdjustWindowRect(&rectangle, style, FALSE)) goto fail; + window_handle = CreateWindowExW(0, class_name, window_title, style, + CW_USEDEFAULT, CW_USEDEFAULT, rectangle.right - rectangle.left, + rectangle.bottom - rectangle.top, NULL, NULL, instance, NULL); + if (!window_handle) goto fail; + device_context = GetDC(window_handle); + if (!device_context) goto fail; + + pixel_format.nSize = sizeof(pixel_format); + pixel_format.nVersion = 1; + pixel_format.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; + pixel_format.iPixelType = PFD_TYPE_RGBA; + pixel_format.cColorBits = 32; + pixel_format.cDepthBits = 24; + pixel_format.cStencilBits = 8; + pixel_format.iLayerType = PFD_MAIN_PLANE; + format = ChoosePixelFormat(device_context, &pixel_format); + if (!format || !SetPixelFormat(device_context, format, &pixel_format)) goto fail; + + /* A temporary legacy context is needed to resolve the modern WGL entrypoint. */ + render_context = wglCreateContext(device_context); + if (!render_context || !wglMakeCurrent(device_context, render_context)) goto fail; + create_context = (CreateContextAttribsProc)get_wgl_proc("wglCreateContextAttribsARB"); + if (!create_context) { + fprintf(stderr, "WGL: OpenGL 3.3 is required; install the GPU vendor's display driver.\n"); + goto fail; + } + modern_context = create_context(device_context, NULL, context_attributes); + if (!modern_context) goto fail; + wglMakeCurrent(NULL, NULL); + wglDeleteContext(render_context); + render_context = modern_context; + if (!wglMakeCurrent(device_context, render_context)) goto fail; + + { + SwapIntervalProc swap = (SwapIntervalProc)get_wgl_proc("wglSwapIntervalEXT"); + if (swap) swap(viewWindow ? swap_interval : 0); + } + /* Headless mode still uses an unshown Win32 window, with no mapped surface. */ + if (viewWindow) ShowWindow(window_handle, SW_SHOW); + fprintf(stderr, "WGL OpenGL %s context ready (%s)\n", glGetString(GL_VERSION), + viewWindow ? "windowed" : "hidden"); + return 1; + +fail: + fprintf(stderr, "WGL context creation failed (Win32 error %lu).\n", GetLastError()); + stop_glx3_stuff(); + return 0; +} + +int glx3_endRedraw(void) +{ + return device_context ? (int)SwapBuffers(device_context) : 0; +} + +int glx3_should_close(void) { return close_requested; } +void glx3_request_close(void) { close_requested = 1; } + +int glx3_checkEvents(void) +{ + MSG message; + if (!window_handle || close_requested) return 0; + while (PeekMessageW(&message, NULL, 0, 0, PM_REMOVE)) { + if (message.message == WM_QUIT) glx3_request_close(); + else { + TranslateMessage(&message); + DispatchMessageW(&message); + } + if (close_requested) return 0; + } + return 1; +} diff --git a/src/SAM3DBODY-cpp/cli_common.h b/src/SAM3DBODY-cpp/cli_common.h index a5f356a..a175d72 100644 --- a/src/SAM3DBODY-cpp/cli_common.h +++ b/src/SAM3DBODY-cpp/cli_common.h @@ -61,11 +61,50 @@ #include // resolve_backbone_defaults(): probe for backbone_fp16.onnx #include #include // ensure_models(): sentinel file list +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +// Legacy Win16 pointer qualifiers collide with renderer/math identifiers. +#ifdef near +#undef near +#endif +#ifdef far +#undef far +#endif +#else #include // resolve_detector_defaults(): find libreyolo*.onnx in onnx_dir #include // ensure_trt_models(): readlink("/proc/self/exe") to locate setup_trt.sh +#endif #include "fast_sam_3dbody.h" // for fsb::PipelineConfig +inline std::string cli_executable_directory() +{ +#if defined(_WIN32) + std::vector buffer(512); + while (buffer.size() <= 32768) { + const DWORD count = GetModuleFileNameW(nullptr, buffer.data(), + static_cast(buffer.size())); + if (count == 0) return {}; + if (count < buffer.size()) + return std::filesystem::path(std::wstring(buffer.data(), count)) + .parent_path().u8string(); + buffer.resize(buffer.size() * 2); + } + return {}; +#else + char buffer[4096]; + const ssize_t count = ::readlink("/proc/self/exe", buffer, sizeof(buffer) - 1); + if (count <= 0) return {}; + return std::filesystem::path(std::string(buffer, count)).parent_path().string(); +#endif +} + struct CommonConfig { @@ -105,6 +144,7 @@ struct CommonConfig bool yolo_path_set = false; // --yolo given bool thresh_set = false; // --thresh / --detector-threshold given bool backbone_name_set = false; // --backbone given (pins the model; disables fp16 auto-prefer) + bool decoder_name_set = false; // --decoder given (disables CPU/TRT auto-selection) // ── Input source ──────────────────────────────────────────────────────── std::string from; // file / webcam index / empty = required @@ -178,6 +218,8 @@ inline bool parse_common_arg(int argc, const char* const* argv, int& i, // won't silently auto-upgrade them to backbone_fp16.onnx. if (std::strcmp(argv[i], "--backbone") == 0 && i + 1 < argc) { c.backbone_name = argv[++i]; c.backbone_name_set = true; return true; } + if (std::strcmp(argv[i], "--decoder") == 0 && i + 1 < argc) + { c.decoder_name = argv[++i]; c.decoder_name_set = true; return true; } CLI_STR ("--from", from) CLI_INT ("--frames", max_frames) CLI_INT ("--start", start_frame) @@ -302,7 +344,9 @@ inline void ensure_models(CommonConfig& c, bool refined_pose = false) { const bool cpu = (c.cuda_device < 0); const char* profile; - if (cpu) profile = "cpu"; // CPU EP: fp32 backbone + fp16 decoder + if (c.backbone_name_set && c.decoder_name_set) + profile = "shared"; // both model choices are explicit + else if (cpu) profile = "cpu"; // CPU EP: fp32 backbone + fp16 decoder else if (c.use_trt) profile = "shared"; // the TRT pair is ensure_trt_models()' job else if (c.backbone_name_set) profile = "shared"; // user is driving the model choice else profile = "cuda"; // bf16 backbone + bf16 decoder @@ -319,15 +363,26 @@ inline void ensure_models(CommonConfig& c, bool refined_pose = false) "pipeline.gguf", "body_model.lbs", "yolo.onnx", "correctives.bin", "keypoint_mapping.bin" }; + auto require_model = [&](const std::string& name) { + sentinels.push_back(name); + // These distributed ONNX graphs use external weights. Custom exports + // may be self-contained; ORT validates their own external-data mapping. + const auto filename = std::filesystem::u8path(name).filename().u8string(); + if (filename == "backbone.onnx" || filename == "backbone_fp32.onnx" || + filename == "backbone_fp16.onnx" || filename == "backbone_fp16_trt.onnx" || + filename == "decoder_fp16.onnx") + sentinels.push_back(name + ".data"); + }; if (cpu) { - // Fetched even when --backbone is pinned: pinning the backbone says - // nothing about the decoder, and the bf16 decoder.onnx has no CPU kernels. - sentinels.insert(sentinels.end(), { - "backbone_fp32.onnx", "backbone_fp32.onnx.data", - "decoder_fp16.onnx", "decoder_fp16.onnx.data" }); - } else if (!c.use_trt && !c.backbone_name_set) { - sentinels.insert(sentinels.end(), { - "backbone.onnx", "backbone.onnx.data", "decoder.onnx" }); + require_model(c.backbone_name_set ? c.backbone_name : "backbone_fp32.onnx"); + require_model(c.decoder_name_set ? c.decoder_name : "decoder_fp16.onnx"); + } else if (!c.use_trt) { + require_model(c.backbone_name); + require_model(c.decoder_name); + } else { + // The unpinned TRT defaults remain ensure_trt_models()' responsibility. + if (c.backbone_name_set) require_model(c.backbone_name); + if (c.decoder_name_set) require_model(c.decoder_name); } if (refined_pose) { // --refined-pose's iterative decoders (the 'refined' profile in @@ -370,26 +425,28 @@ inline void ensure_models(CommonConfig& c, bool refined_pose = false) // Where the executable lives, so we can find both the repo's onnx/ and the // fetch script without depending on the working directory. - std::string exe_dir; - { - char buf[4096]; - ssize_t n = ::readlink("/proc/self/exe", buf, sizeof(buf) - 1); - if (n > 0) { - buf[n] = '\0'; - std::string exe(buf); - size_t s = exe.find_last_of('/'); - if (s != std::string::npos) exe_dir = exe.substr(0, s); - } - } + const std::string exe_dir = cli_executable_directory(); // Retarget the default './onnx' at the repo's onnx/ when the cwd has none. // Only ever fires in the case that fails outright today. if (c.onnx_dir == "./onnx" && !std::filesystem::exists("./onnx") && !exe_dir.empty()) { std::string repo_onnx = exe_dir + "/../onnx"; +#if defined(_WIN32) + // Also support a packaged executable and MSVC's build/Release layout. + for (const auto& candidate : {exe_dir + "/onnx", exe_dir + "/../onnx", + exe_dir + "/../../onnx"}) { + std::error_code candidate_ec; + if (std::filesystem::is_directory(std::filesystem::u8path(candidate), candidate_ec)) { + repo_onnx = candidate; + break; + } + } +#endif std::error_code ec; - std::filesystem::path canon = std::filesystem::weakly_canonical(repo_onnx, ec); + std::filesystem::path canon = std::filesystem::weakly_canonical( + std::filesystem::u8path(repo_onnx), ec); if (!ec) { - c.onnx_dir = canon.string(); + c.onnx_dir = canon.u8string(); // gguf_path / yolo_path default to "./onnx/..." independently of // onnx_dir, so they have to move with it — otherwise the backbone // loads from the repo while YOLO still looks in the (absent) ./onnx. @@ -415,6 +472,14 @@ inline void ensure_models(CommonConfig& c, bool refined_pose = false) }; if (!missing()) return; +#if defined(_WIN32) + std::fprintf(stderr, + "[cli] models missing from '%s'. Download the required model files from\n" + " https://huggingface.co/AmmarkoV/SAM3DBody-cpp-onnx-models\n" + " and select their directory with --onnx-dir. Native Windows does not\n" + " launch Bash or download models automatically. See knowledge/WINDOWS.md.\n", + c.onnx_dir.c_str()); +#else std::string script; if (!exe_dir.empty() && std::ifstream(exe_dir + "/../tools/fetch_model.sh").good()) script = exe_dir + "/../tools/fetch_model.sh"; @@ -443,6 +508,7 @@ inline void ensure_models(CommonConfig& c, bool refined_pose = false) "[cli] model fetch did not complete — continuing; the load below will " "report what is still missing.\n"); } +#endif } // Resolve the "auto" detector default and the per-detector confidence default. @@ -464,6 +530,27 @@ inline void resolve_detector_defaults(CommonConfig& c) if (c.detector == "auto") { // Prefer a LibreYOLO model on disk when the user hasn't pinned --yolo. if (!c.yolo_path_set) { +#if defined(_WIN32) + std::string first_model; + std::error_code ec; + std::filesystem::directory_iterator it(std::filesystem::u8path(c.onnx_dir), ec), end; + while (!ec && it != end) { + const auto name = it->path().filename().u8string(); + if (name.compare(0, 9, "libreyolo") == 0 && + it->path().extension() == ".onnx" && it->is_regular_file(ec)) { + const auto path = it->path().u8string(); + // Match glob's stable lexical choice, independent of directory order. + if (first_model.empty() || path < first_model) first_model = path; + } + it.increment(ec); + } + if (!first_model.empty()) { + c.yolo_path = first_model; + std::fprintf(stderr, + "[cli] --detector auto: found LibreYOLO model '%s'; " + "preferring it over yolo-pose\n", c.yolo_path.c_str()); + } +#else std::string pattern = c.onnx_dir + "/libreyolo*.onnx"; glob_t g{}; if (glob(pattern.c_str(), 0, nullptr, &g) == 0 && g.gl_pathc > 0) { @@ -473,6 +560,7 @@ inline void resolve_detector_defaults(CommonConfig& c) "preferring it over yolo-pose\n", c.yolo_path.c_str()); } globfree(&g); +#endif } c.detector = path_looks_like_libreyolo(c.yolo_path) ? "libreyolo" : "yolo-pose"; @@ -512,6 +600,13 @@ inline void ensure_trt_models(const CommonConfig& c) if (exists("backbone_fp16_trt.onnx") && exists("decoder_fp16.onnx")) return; // already have them +#if defined(_WIN32) + std::fprintf(stderr, + "[cli] TRT: backbone_fp16_trt.onnx / decoder_fp16.onnx missing from '%s'.\n" + " Download the TRT profile from the model repository and pass --onnx-dir.\n" + " Automatic Bash setup is disabled on native Windows; falling back to CUDA EP.\n", + c.onnx_dir.c_str()); +#else // Locate setup_trt.sh relative to this executable (binaries live in build/, // so ../tools/), with the working dir as a fallback. std::string script; @@ -557,6 +652,7 @@ inline void ensure_trt_models(const CommonConfig& c) std::fprintf(stderr, "[cli] TRT: models not fetched (declined or unavailable) — " "falling back to CUDA EP.\n"); +#endif } // On CUDA, auto-prefer a float16 backbone when one has been exported next to the @@ -594,7 +690,7 @@ inline void resolve_backbone_defaults(CommonConfig& c) // exactly how to get them when they're absent instead of leaving the user with // ORT's cryptic MatMul error. if (c.cuda_device < 0) { - if (c.decoder_name == "decoder.onnx") { + if (!c.decoder_name_set && c.decoder_name == "decoder.onnx") { if (exists("decoder_fp16.onnx")) { c.decoder_name = "decoder_fp16.onnx"; std::fprintf(stderr, @@ -638,7 +734,7 @@ inline void resolve_backbone_defaults(CommonConfig& c) // --output onnx/decoder_fp16.onnx). // Pick it up automatically under --trt so the decoder runs on TRT instead of // crashing. - if (c.use_trt && c.decoder_name == "decoder.onnx" && exists("decoder_fp16.onnx")) { + if (c.use_trt && !c.decoder_name_set && c.decoder_name == "decoder.onnx" && exists("decoder_fp16.onnx")) { c.decoder_name = "decoder_fp16.onnx"; std::fprintf(stderr, "[cli] TRT: using 'decoder_fp16.onnx' (bf16 decoder.onnx is not TRT-compatible).\n"); @@ -708,6 +804,8 @@ inline void print_common_args_help(FILE* fp) " backbone_fp16.onnx is auto-preferred when present — see\n" " tools/export_backbone_fp16.py; or backbone_int8.onnx via\n" " tools/quantize_backbone.py)\n" + " --decoder NAME Decoder filename within onnx-dir (default decoder.onnx;\n" + " pin decoder_fp16.onnx to use the FP16 export on CUDA EP)\n" " --gguf PATH pipeline.gguf (MHR + camera heads)\n" " --yolo PATH Detector model (.onnx); YOLO11-pose or a LibreYOLO/YOLOv9 export\n" " --detector NAME Bbox provider parsing --yolo output: auto (default; prefers a\n" diff --git a/src/SAM3DBODY-cpp/fast_sam_3dbody.cpp b/src/SAM3DBODY-cpp/fast_sam_3dbody.cpp index 095eb0f..d86d592 100644 --- a/src/SAM3DBODY-cpp/fast_sam_3dbody.cpp +++ b/src/SAM3DBODY-cpp/fast_sam_3dbody.cpp @@ -358,10 +358,18 @@ struct OrtSession // gated the same way, so use that instead: it dumps every op // with its execution provider and per-call duration, which is // strictly more actionable than a static assignment list. - std::string prefix = "/tmp/ort_profile_" + - std::filesystem::path(path).stem().string() + "_"; - opts.EnableProfiling(prefix.c_str()); - profiling_enabled = true; + std::error_code temp_error; + const auto temp_dir = std::filesystem::temp_directory_path(temp_error); + if (!temp_error) { + const auto prefix = temp_dir / std::filesystem::u8path( + "ort_profile_" + std::filesystem::u8path(path).stem().u8string() + "_"); + // The native path character type also matches ORT on Windows. + opts.EnableProfiling(prefix.c_str()); + profiling_enabled = true; + } else { + std::fprintf(stderr, "[ORT] profiling disabled: no usable temporary directory (%s)\n", + temp_error.message().c_str()); + } } try { @@ -413,7 +421,9 @@ struct OrtSession } // EP_CPU: append nothing — the default CPU EP runs. - session = new Ort::Session(e, path.c_str(), opts); + // ORT uses wchar_t paths on Windows and char paths on Unix. + const auto model_path = std::filesystem::u8path(path); + session = new Ort::Session(e, model_path.c_str(), opts); if (ep == EP_CPU && cuda) fprintf(stderr, "[ORT] WARNING: '%s' running on CPU (GPU EPs unavailable)\n", path.c_str()); diff --git a/src/SAM3DBODY-cpp/fast_sam_3dbody_capi.cpp b/src/SAM3DBODY-cpp/fast_sam_3dbody_capi.cpp index 594c15c..c0a158e 100644 --- a/src/SAM3DBODY-cpp/fast_sam_3dbody_capi.cpp +++ b/src/SAM3DBODY-cpp/fast_sam_3dbody_capi.cpp @@ -10,6 +10,10 @@ extern "C" { + unsigned int fsb_abi_version(void) { return FSB_ABI_VERSION; } + size_t fsb_config_size(void) { return sizeof(FsbConfig); } + size_t fsb_result_size(void) { return sizeof(FsbResult); } + FsbHandle fsb_create(void) { return static_cast(new fsb::Pipeline()); diff --git a/src/SAM3DBODY-cpp/fast_sam_3dbody_capi.h b/src/SAM3DBODY-cpp/fast_sam_3dbody_capi.h index c1d72c1..136cd39 100644 --- a/src/SAM3DBODY-cpp/fast_sam_3dbody_capi.h +++ b/src/SAM3DBODY-cpp/fast_sam_3dbody_capi.h @@ -4,6 +4,18 @@ // ============================================================================ #include +#include + +#if defined(_WIN32) && defined(FSB_BUILD_DLL) +# define FSB_API __declspec(dllexport) +#elif defined(_WIN32) +# define FSB_API __declspec(dllimport) +#else +# define FSB_API +#endif + +// Increment whenever either public struct changes, including appended fields. +#define FSB_ABI_VERSION 1u #ifdef __cplusplus extern "C" { @@ -66,8 +78,8 @@ typedef struct { // s/tx/ty → pred_cam_t conversion. Appended to prev_estimate when the // loaded Python model has an init_camera attribute. // - // IMPORTANT: these fields are appended at the END of FsbResult so that the - // ctypes struct layout for older code is not disturbed. + // Appending fields preserves earlier offsets, but changes sizeof(FsbResult) + // and array stride. Bindings must match the complete struct before inference. float pred_pose_raw[266]; // global_rot_6d[6] + body_cont[260] float pred_cam_raw[3]; // raw cam head output before s/tx/ty decode @@ -76,7 +88,7 @@ typedef struct { // [136:204]=scale_out. Mirrors Python mhr_forward(..., return_model_params=True). float mhr_model_params[204]; - // ── Full MHR skeleton (appended at END to preserve older ctypes layouts) ──── + // ── Full MHR skeleton ───────────────────────────────────────────────────── // 127 joint world positions in the same coordinate frame as kps_3d (y,z // negated, metres). Joint names/order are in src/mhr_joint_table.h. This // exposes joints absent from the 70 keypoints — notably root (≈pelvis) and @@ -86,18 +98,23 @@ typedef struct { } FsbResult; // ── Lifecycle ───────────────────────────────────────────────────────────────── -FsbHandle fsb_create(void); -void fsb_destroy(FsbHandle h); +// Call before passing any structs across a foreign-function boundary. +FSB_API unsigned int fsb_abi_version(void); +FSB_API size_t fsb_config_size(void); +FSB_API size_t fsb_result_size(void); + +FSB_API FsbHandle fsb_create(void); +FSB_API void fsb_destroy(FsbHandle h); // Returns 1 on success, 0 on failure. -int fsb_load(FsbHandle h, const FsbConfig* cfg); +FSB_API int fsb_load(FsbHandle h, const FsbConfig* cfg); // ── Inference ───────────────────────────────────────────────────────────────── // Process a BGR uint8 image. // results : pre-allocated array of FsbResult with at least max_results entries. // max_results: capacity of results[]. // Returns number of persons written (≤ max_results). -int fsb_process_bgr(FsbHandle h, +FSB_API int fsb_process_bgr(FsbHandle h, const uint8_t* bgr, int width, int height, diff --git a/src/SAM3DBODY-cpp/main.cpp b/src/SAM3DBODY-cpp/main.cpp index 1a192e9..d9258de 100644 --- a/src/SAM3DBODY-cpp/main.cpp +++ b/src/SAM3DBODY-cpp/main.cpp @@ -85,6 +85,8 @@ static void print_usage(const char* prog) printf(" --backbone NAME Backbone filename in onnx-dir (default backbone.onnx; on CUDA,\n"); printf(" backbone_fp16.onnx is auto-preferred when present — see\n"); printf(" tools/export_backbone_fp16.py; or backbone_int8.onnx via quantize_backbone.py)\n"); + printf(" --decoder NAME Decoder filename in onnx-dir (default decoder.onnx;\n"); + printf(" use decoder_fp16.onnx for the FP16 export on CUDA EP)\n"); printf(" --gguf PATH pipeline.gguf (MHR + camera heads)\n"); printf(" --yolo PATH YOLO pose model (.onnx or .engine)\n"); printf(" --from SRC Webcam index (0,1,..) or path to image/video\n"); @@ -94,7 +96,7 @@ static void print_usage(const char* prog) printf(" --trt Enable ONNX Runtime TensorRT EP\n"); printf(" --no-fp16 Disable FP16 for ONNX EP\n"); printf(" --ort-verbose Print ORT's per-node EP assignment + a chrome-trace profile per\n"); - printf(" session to /tmp/ort_profile__*.json (open in chrome://tracing)\n"); + printf(" session to the system temp directory (ort_profile__*.json)\n"); printf(" --skip-body Skip body model (no vertices / keypoints)\n"); printf(" --dev-face Enable face expression params (disabled by default)\n"); printf(" --detector-threshold F Person confidence (default 0.50; 0.25 for libreyolo). Alias: --thresh\n"); diff --git a/src/SAM3DBODY-cpp/offline_passes.cpp b/src/SAM3DBODY-cpp/offline_passes.cpp index fc0b7c7..883a639 100644 --- a/src/SAM3DBODY-cpp/offline_passes.cpp +++ b/src/SAM3DBODY-cpp/offline_passes.cpp @@ -547,7 +547,7 @@ build_global_tracks(std::vector& frames, const Config& cfg) // Retire any track we haven't seen in too long. live.erase(std::remove_if(live.begin(), live.end(), - [F](const LiveTrack& t){ return (F - t.last_frame) > MAX_MISSING; }), + [F, MAX_MISSING](const LiveTrack& t){ return (F - t.last_frame) > MAX_MISSING; }), live.end()); } @@ -930,12 +930,12 @@ void interpolate_jitter_pass(std::vector& frames, // anchor. These are joint Euler angles where naive linear // interpolation is unsafe (wrap / gimbal-lock). Picking the // closer non-jittered frame is a conservative substitute. - const auto& near = (t < 0.5f) ? aF : bF; - if (cF.body_pose.size() == near.body_pose.size()) - cF.body_pose = near.body_pose; - if (cF.hand_pose.size() == near.hand_pose.size()) - cF.hand_pose = near.hand_pose; - cF.mhr_model_params = near.mhr_model_params; + const auto& nearest_anchor = (t < 0.5f) ? aF : bF; + if (cF.body_pose.size() == nearest_anchor.body_pose.size()) + cF.body_pose = nearest_anchor.body_pose; + if (cF.hand_pose.size() == nearest_anchor.hand_pose.size()) + cF.hand_pose = nearest_anchor.hand_pose; + cF.mhr_model_params = nearest_anchor.mhr_model_params; frames[seq[i].first].was_interpolated[seq[i].second] = 1; ++n_interpolated; diff --git a/src/SAM3DBODY-cpp/pthreadWorkerPool.h b/src/SAM3DBODY-cpp/pthreadWorkerPool.h index 92a1b1a..47e73d0 100644 --- a/src/SAM3DBODY-cpp/pthreadWorkerPool.h +++ b/src/SAM3DBODY-cpp/pthreadWorkerPool.h @@ -9,6 +9,10 @@ #ifndef PTHREADWORKERPOOL_H_INCLUDED #define PTHREADWORKERPOOL_H_INCLUDED +#if defined(_WIN32) && defined(__cplusplus) +#include "windowsWorkerPool.h" +#else + //The star of the show #include #include @@ -693,6 +697,7 @@ static int threadpoolDestroy(struct workerPool *pool) } #endif +#endif // Windows C++ / POSIX implementation #endif // PTHREADWORKERPOOL_H_INCLUDED diff --git a/src/SAM3DBODY-cpp/windowsWorkerPool.h b/src/SAM3DBODY-cpp/windowsWorkerPool.h new file mode 100644 index 0000000..f3ca977 --- /dev/null +++ b/src/SAM3DBODY-cpp/windowsWorkerPool.h @@ -0,0 +1,143 @@ +#pragma once +// Windows implementation of the worker-pool protocol used by Pipeline::Impl. +// A generation counter makes both the initial wait and subsequent kicks safe +// against missed notifications and spurious condition-variable wakeups. +#include +#include +#include +#include +#include +#include + +struct workerPool; +struct threadContext { + void* argumentToPass = nullptr; + workerPool* pool = nullptr; + unsigned int threadID = 0; + std::uint64_t generation = 0; +}; + +struct workerPool { + std::mutex mutex; + std::condition_variable start, complete; + std::vector contexts; + std::vector threads; + unsigned int numberOfThreads = 0; + unsigned int ready = 0, active = 0; + std::uint64_t generation = 0; + bool initialized = false, stopping = false; +}; + +static int threadpoolWorkerInitialWait(threadContext* ctx) +{ + auto& p = *ctx->pool; + std::unique_lock lock(p.mutex); + ++p.ready; + p.complete.notify_all(); + p.start.wait(lock, [&] { return p.stopping || p.generation != ctx->generation; }); + ctx->generation = p.generation; + return !p.stopping; +} + +static int threadpoolWorkerLoopCondition(threadContext* ctx) +{ + std::lock_guard lock(ctx->pool->mutex); + return !ctx->pool->stopping; +} + +static int threadpoolWorkerLoopEnd(threadContext* ctx) +{ + auto& p = *ctx->pool; + std::unique_lock lock(p.mutex); + if (p.active > 0) --p.active; + p.complete.notify_all(); + p.start.wait(lock, [&] { return p.stopping || p.generation != ctx->generation; }); + ctx->generation = p.generation; + return !p.stopping; +} + +static int threadpoolMainThreadPrepareWorkForWorkers(workerPool* p) +{ + if (!p) return 0; + std::lock_guard lock(p->mutex); + return p->initialized && !p->stopping && p->active == 0; +} + +static int threadpoolMainThreadKickWorkers(workerPool* p) +{ + if (!p) return 0; + std::lock_guard lock(p->mutex); + if (!p->initialized || p->stopping || p->active != 0) return 0; + p->active = p->numberOfThreads; + ++p->generation; + p->start.notify_all(); + return 1; +} + +static int threadpoolMainThreadWaitForKickedWorkersToFinishTimeoutSeconds( + workerPool* p, unsigned int timeoutSeconds) +{ + if (!p) return 0; + std::unique_lock lock(p->mutex); + if (!p->initialized) return 0; + auto done = [&] { return p->active == 0 || p->stopping; }; + if (timeoutSeconds == 0) p->complete.wait(lock, done); + else if (!p->complete.wait_for(lock, std::chrono::seconds(timeoutSeconds), done)) return 0; + return !p->stopping; +} + +static int threadpoolMainThreadWaitForWorkersToFinishTimeoutSeconds( + workerPool* p, unsigned int timeoutSeconds) +{ + return threadpoolMainThreadKickWorkers(p) && + threadpoolMainThreadWaitForKickedWorkersToFinishTimeoutSeconds(p, timeoutSeconds); +} + +static int threadpoolMainThreadWaitForWorkersToFinish(workerPool* p) +{ + return threadpoolMainThreadWaitForWorkersToFinishTimeoutSeconds(p, 0); +} + +static int threadpoolDestroy(workerPool* p) +{ + if (!p) return 0; + { + std::lock_guard lock(p->mutex); + p->stopping = true; + } + p->start.notify_all(); + p->complete.notify_all(); + for (auto& t : p->threads) if (t.joinable()) t.join(); + p->threads.clear(); + p->contexts.clear(); + p->initialized = false; + p->numberOfThreads = p->active = p->ready = 0; + return 1; +} + +static int threadpoolCreate(workerPool* p, unsigned int count, + void* workerFunction, void* argument) +{ + if (!p || p->initialized || !count || !workerFunction) return 0; + p->stopping = false; + p->generation = 0; + p->ready = p->active = 0; + p->numberOfThreads = count; + try { + p->contexts.resize(count); + p->threads.reserve(count); + auto entry = reinterpret_cast(workerFunction); + for (unsigned int i = 0; i < count; ++i) { + p->contexts[i] = {argument, p, i, 0}; + p->threads.emplace_back(entry, &p->contexts[i]); + } + } catch (...) { + // Join any workers already started if thread creation/allocation fails. + threadpoolDestroy(p); + return 0; + } + std::unique_lock lock(p->mutex); + p->complete.wait(lock, [&] { return p->ready == count; }); + p->initialized = true; + return 1; +} diff --git a/src/SAM3DBODY-cpp/windows_worker_pool_test.cpp b/src/SAM3DBODY-cpp/windows_worker_pool_test.cpp new file mode 100644 index 0000000..0f76752 --- /dev/null +++ b/src/SAM3DBODY-cpp/windows_worker_pool_test.cpp @@ -0,0 +1,67 @@ +// No models or GPU needed: exercise kick/wait barriers, repeated generations, +// timeouts, idle destruction, and reuse of the Windows C++ worker pool. +#include "windowsWorkerPool.h" +#include +#include + +struct TestWork { + int values[4]{}; + int increment = 0; + std::atomic blocked{false}; +}; + +static void* worker(void* arg) +{ + auto* ctx = static_cast(arg); + auto* work = static_cast(ctx->argumentToPass); + threadpoolWorkerInitialWait(ctx); + while (threadpoolWorkerLoopCondition(ctx)) { + while (work->blocked.load()) std::this_thread::yield(); + work->values[ctx->threadID] += work->increment; + threadpoolWorkerLoopEnd(ctx); + } + return nullptr; +} + +int main() +{ + workerPool pool; + TestWork work; + auto fail = [&](const char* message) { + std::fprintf(stderr, "%s\n", message); + work.blocked = false; + threadpoolDestroy(&pool); + return 1; + }; + if (threadpoolCreate(&pool, 0, reinterpret_cast(&worker), &work)) + return fail("A zero-worker pool must be rejected"); + int expected = 0; + for (int cycle = 0; cycle < 2; ++cycle) { + if (!threadpoolCreate(&pool, 4, reinterpret_cast(&worker), &work)) + return fail("Could not create/recreate pool"); + for (int round = 1; round <= 2000; ++round) { + work.increment = round; + if (!threadpoolMainThreadPrepareWorkForWorkers(&pool) || + !threadpoolMainThreadKickWorkers(&pool) || + !threadpoolMainThreadWaitForKickedWorkersToFinishTimeoutSeconds(&pool, 5)) + return fail("Batch did not complete"); + expected += round; + for (int value : work.values) + if (value != expected) return fail("Lost or duplicated worker generation"); + } + if (!threadpoolDestroy(&pool)) return fail("Idle pool destruction failed"); + } + if (!threadpoolCreate(&pool, 4, reinterpret_cast(&worker), &work)) + return fail("Could not create timeout-test pool"); + work.increment = 1; + work.blocked = true; + if (!threadpoolMainThreadKickWorkers(&pool)) return fail("Timeout kick failed"); + if (threadpoolMainThreadWaitForKickedWorkersToFinishTimeoutSeconds(&pool, 1)) + return fail("Blocked work did not time out"); + work.blocked = false; + if (!threadpoolMainThreadWaitForKickedWorkersToFinishTimeoutSeconds(&pool, 5)) + return fail("Timed-out work could not be harvested"); + threadpoolDestroy(&pool); + std::puts("Windows worker pool: 4000 generations, timeout and reuse passed"); + return 0; +} diff --git a/src/multiview/CMakeLists.txt b/src/multiview/CMakeLists.txt index c252bc0..c880f7b 100644 --- a/src/multiview/CMakeLists.txt +++ b/src/multiview/CMakeLists.txt @@ -158,6 +158,13 @@ add_executable(sam_3dbody_multiview sam_3dbody_multiview.cpp) target_link_libraries(sam_3dbody_multiview PRIVATE multiview_frontend multiview_sync_io fast_sam_3dbody ${OpenCV_LIBS}) +foreach(_target sam_3dbody_multiview aruco_qr_probe sam_3dbody_extrinsics + sam_3dbody_synctime sam_3dbody_sync) + if(TARGET ${_target}) + sam3d_stage_runtime(${_target}) + endif() +endforeach() + if(MULTIVIEW_HAVE_ARUCO) message(STATUS " multiview: calib + aruco_qr + extrinsics + sync_time + sync_io (libs); tests: calib/extrinsics/sync_time/sync_io; tools: aruco_qr_probe, sam_3dbody_extrinsics, sam_3dbody_synctime, sam_3dbody_sync, sam_3dbody_multiview") else() diff --git a/src/multiview/extrinsics_test.cpp b/src/multiview/extrinsics_test.cpp index 20ff1d6..4e5665f 100644 --- a/src/multiview/extrinsics_test.cpp +++ b/src/multiview/extrinsics_test.cpp @@ -41,7 +41,7 @@ static double maxdiff(const Mat4& A, const Mat4& B) int main() { - const double D2R = M_PI/180.0; + const double D2R = std::acos(-1.0)/180.0; // Ground-truth camera world poses (T_world<-cam): Mat4 W0 = T_rotY(0,0,0,0); // reference Mat4 W1 = T_rotY( 20*D2R, 1.0, 0.0, 0.2); diff --git a/src/multiview/sam_3dbody_extrinsics.cpp b/src/multiview/sam_3dbody_extrinsics.cpp index 5e6d165..4ed0d31 100644 --- a/src/multiview/sam_3dbody_extrinsics.cpp +++ b/src/multiview/sam_3dbody_extrinsics.cpp @@ -33,9 +33,10 @@ static void euler_zyx_deg(const mv::Mat4& T, double& yaw, double& pitch, double& { // R = Rz(yaw)Ry(pitch)Rx(roll); extract from the row-major rotation block. double r00=T[0], r10=T[4], r20=T[8], r21=T[9], r22=T[10]; - pitch = std::asin(-std::max(-1.0,std::min(1.0,r20))) * 180.0/M_PI; - yaw = std::atan2(r10, r00) * 180.0/M_PI; - roll = std::atan2(r21, r22) * 180.0/M_PI; + const double radians_to_degrees = 180.0 / std::acos(-1.0); + pitch = std::asin(-std::max(-1.0,std::min(1.0,r20))) * radians_to_degrees; + yaw = std::atan2(r10, r00) * radians_to_degrees; + roll = std::atan2(r21, r22) * radians_to_degrees; } int main(int argc, char** argv) diff --git a/src/render/fast_sam_3dbody_render.cpp b/src/render/fast_sam_3dbody_render.cpp index 48fc081..11b95b1 100644 --- a/src/render/fast_sam_3dbody_render.cpp +++ b/src/render/fast_sam_3dbody_render.cpp @@ -29,7 +29,6 @@ // GLEW must come before any other GL header. #include #include -#include extern "C" { #include "../GraphicsEngine/System/glx3.h" @@ -56,7 +55,13 @@ extern "C" { #include #include #include -#include +#include +#include + +static long long monotonic_ns() { + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); +} // ── Inline GLSL shaders ────────────────────────────────────────────────────── @@ -401,7 +406,9 @@ int mat4_transpose(float * mat) extern "C" { // Called by glx3_checkEvents() on key/mouse events. - int handleUserInput(int key, int x, int y) { (void)key; (void)x; (void)y; return 1; } + int handleUserInput(int key, int state, int x, int y) { + (void)key; (void)state; (void)x; (void)y; return 1; + } // Called by glx3_checkEvents() when the window is resized. int windowSizeUpdated(unsigned int w, unsigned int h) { (void)w; (void)h; return 1; } } @@ -612,7 +619,51 @@ static inline float clamp01(float v) { return v < 0.f ? 0.f : (v > 1.f ? 1.f : v // ── Main ───────────────────────────────────────────────────────────────────── +static void print_usage(const char* program) { + printf("Usage: %s --onnx-dir DIR --gguf FILE --yolo FILE --from SOURCE [options]\n\n", + program); + printf("Live OpenGL body overlay for a webcam index, image, or video.\n" + "Run from the repository root to use the default shaders and BVH templates.\n\n"); + print_common_args_help(stdout); + printf("\nRenderer options:\n" + " -h, --help Print help and exit without loading models or opening a camera\n" + " --headless Render offscreen (still requires an OpenGL 3.3 driver)\n" + " --title TEXT Window title\n" + " --render-size W H Output surface dimensions (default: input dimensions)\n" + " --render-scale S Output scale relative to input; --render-size takes priority\n" + " --size W H Requested webcam capture resolution\n" + " --fps N Requested webcam capture frame rate\n" + " --mjpg Request MJPEG capture from the webcam\n" + " --no-drop Process every captured frame instead of dropping stale frames\n" + " --mesh PATH Mesh topology (default: /body_mesh.tri)\n" + " --lbs PATH Body model (default: /body_model.lbs)\n" + " --vert PATH / --frag PATH Override the default vertex / fragment shaders\n" + " --color R G B Mesh color, with channels in 0..255\n" + " --mesh-color R G B Mesh color, with channels in 0..1\n" + " --transparency F Mesh transparency, 0 = opaque and 1 = invisible\n" + " --shiny [F] Reflective shading (optional strength in 0..1)\n" + " --save-frames PREFIX Save rendered frames as numbered JPEG images\n" + " --save-depth PREFIX Save raw float32 depth buffers\n" + " --export-mesh PREFIX Export per-person, per-frame OBJ meshes\n" + " --export-mesh-stride N Export an OBJ every N frames (default 1)\n" + " --boxes PATH External person boxes, one x1 y1 x2 y2 per line\n" + " --fx F / --fy F Override camera focal lengths in pixels\n" + " --refined-pose Enable hand/wrist refinement (requires refinement models)\n" + " --no-pass2 Skip the second refinement pass\n" + " --butterworth Smooth body parameters and camera translation\n" + " --butterworth-root-rotation Smooth the root orientation\n" + " --dev-face Enable experimental face parameters\n\n" + "Close the window or press Escape to finish and save outputs.\n"); +} + int main(int argc, const char** argv) { + // Help must take precedence over model fetching, inference and camera setup. + for (int i = 1; i < argc; ++i) { + if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) { + print_usage(argv[0]); + return 0; + } + } std::string onnx_dir = "./onnx"; std::string gguf_path = "./onnx/pipeline.gguf"; std::string yolo_path = "./onnx/yolo.onnx"; @@ -631,8 +682,8 @@ int main(int argc, const char** argv) { int export_mesh_stride = 1; // --export-mesh-stride: every Nth frame std::string bvh_path = ""; std::string bvh_template = ""; - // --headless creates a GLX Pbuffer (offscreen surface) instead of a - // visible X11 window. Used by scripts/video.sh in --save mode so the + // --headless creates a GLX Pbuffer, or a hidden WGL window on Windows. + // Used by scripts/video.sh in --save mode so the // long-running render can't be killed by an accidental window close, // the screen-saver, or any window-manager interaction with a stale // long-lived window. The GL context is identical either way; only @@ -919,18 +970,16 @@ int main(int argc, const char** argv) { printf("[render] --render-scale %g: %dx%d -> %dx%d\n", render_scale, frame_w, frame_h, W, H); } - // ── GLX surface ─────────────────────────────────────────────────────────── - // viewWindow=1 → normal visible X11 window - // viewWindow=0 → offscreen GLX Pbuffer (no XMapWindow, no event source the - // user can interact with). Pbuffers were the standard - // pre-EGL way to get offscreen GL on Linux/X11 and the - // fixed-pipeline glReadPixels we use to save frames works - // identically on them. + // Visible window, or offscreen GLX Pbuffer / hidden Win32 WGL window. if (!window_title.empty()) glx3_set_window_title(window_title.c_str()); if (!start_glx3_stuff(W, H, headless ? 0 : 1, argc, argv)) { - fprintf(stderr, "Failed to start GLX %s\n", - headless ? "Pbuffer" : "window"); return 1; + fprintf(stderr, "Failed to start OpenGL %s\n", + headless ? "offscreen surface" : "window"); return 1; } + // Also release the native surface on shader/mesh initialization failures. + struct GLContextGuard { + ~GLContextGuard() { stop_glx3_stuff(); } + } context_guard; if (headless) printf("[headless] running offscreen — no GUI window\n"); glewExperimental = GL_TRUE; if (glewInit() != GLEW_OK) { @@ -1075,7 +1124,7 @@ int main(int argc, const char** argv) { glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // ── Render loop ─────────────────────────────────────────────────────────── -#define NS_NOW() ({ struct timespec _t; clock_gettime(CLOCK_MONOTONIC,&_t); (long long)_t.tv_sec*1000000000LL + _t.tv_nsec; }) +#define NS_NOW() monotonic_ns() long long t_last_frame = NS_NOW(); long long t_last_grab = NS_NOW(); // wall-clock of the last frame we pulled (live sync) long long t_session_start = NS_NOW(); // for measuring the effective live frame rate @@ -1193,10 +1242,7 @@ int main(int argc, const char** argv) { t_next_emit = now; // first frame, or lost the beat if (t_next_emit > now) { - struct timespec ts; - ts.tv_sec = (time_t)((t_next_emit - now) / 1000000000LL); - ts.tv_nsec = (long) ((t_next_emit - now) % 1000000000LL); - nanosleep(&ts, nullptr); + std::this_thread::sleep_for(std::chrono::nanoseconds(t_next_emit - now)); now = NS_NOW(); } t_next_emit += period; @@ -1624,6 +1670,5 @@ int main(int argc, const char** argv) { if (bvh_writer.is_open()) bvh_writer.close(); mhr_lbs_free(lbs); tri_freeModel(tri_model); - stop_glx3_stuff(); return 0; } diff --git a/src/render/offline_sam_3dbody_render.cpp b/src/render/offline_sam_3dbody_render.cpp index 1a474c3..a0c9e6d 100644 --- a/src/render/offline_sam_3dbody_render.cpp +++ b/src/render/offline_sam_3dbody_render.cpp @@ -93,6 +93,8 @@ static void print_usage(const char* prog) " --onnx-dir PATH Directory with backbone / decoder / body_model ONNX files\n" " --backbone NAME Backbone filename in onnx-dir (default backbone.onnx;\n" " use backbone_int8.onnx after tools/quantize_backbone.py)\n" + " --decoder NAME Decoder filename in onnx-dir (default decoder.onnx;\n" + " use decoder_fp16.onnx for the FP16 export on CUDA EP)\n" " --gguf PATH pipeline.gguf (MHR + camera heads)\n" " --yolo PATH YOLO pose model (.onnx)\n" " --from VIDEO Path to a video file. Webcams / streams / still images NOT supported.\n" @@ -109,7 +111,7 @@ static void print_usage(const char* prog) " the per-frame cost and fetches the 'refined' model profile on\n" " first run, and it is what fixes image alignment.\n" " --ort-verbose Print ORT's per-node EP assignment + a chrome-trace profile per\n" - " session to /tmp/ort_profile__*.json (open in chrome://tracing)\n" + " session to the system temp directory (ort_profile__*.json)\n" " --detector-threshold F Person confidence (default 0.50; 0.25 for libreyolo). Alias: --thresh\n" " --nms F Detector NMS IoU (default 0.45)\n" " --max-persons N Cap to top-N most-confident people (0 = unlimited)\n" @@ -258,6 +260,12 @@ static bool parse_args(int argc, char** argv, Config& c) int main(int argc, char** argv) { + for (int i = 1; i < argc; ++i) { + if (!std::strcmp(argv[i], "--help") || !std::strcmp(argv[i], "-h")) { + print_usage(argv[0]); + return 0; + } + } Config cfg; if (!parse_args(argc, argv, cfg)) return 1; diff --git a/tests/test_fetch_model.ps1 b/tests/test_fetch_model.ps1 new file mode 100644 index 0000000..1bdab8a --- /dev/null +++ b/tests/test_fetch_model.ps1 @@ -0,0 +1,80 @@ +#requires -Version 5.1 +# Offline checks only: no curl invocation and no model downloads. +[CmdletBinding()] +param() +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$repository = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +$scriptPath = Join-Path $repository 'tools\fetch_model.ps1' +$testRoot = Join-Path $repository ('build\fetch-model-tests-' + [Guid]::NewGuid().ToString('N')) +$previousAutoFetch = $env:SAM3D_AUTO_FETCH +$previousRevision = $env:SAM3D_HF_REVISION +$checks = 0 + +function Assert-True([bool] $Condition, [string] $Message) { + if (-not $Condition) { throw "FAIL: $Message" } + $script:checks++ +} + +function Assert-Fails([scriptblock] $Action, [string] $Pattern) { + $caught = $null + try { & $Action | Out-Null } catch { $caught = $_.Exception.Message } + Assert-True ($null -ne $caught -and $caught -match $Pattern) "Expected error: $Pattern; received: $caught" +} + +try { + $env:SAM3D_AUTO_FETCH = '0' + $env:SAM3D_HF_REVISION = 'test-revision' + $missing = Join-Path $testRoot 'not-created' + $models = @(& $scriptPath -Profile cpu,trt -OnnxDir $missing -List) + Assert-True (@($models | Where-Object Name -eq 'pipeline.gguf').Count -eq 1) 'Shared profile is implicit' + Assert-True (@($models | Where-Object Name -eq 'decoder_fp16.onnx').Count -eq 1) 'CPU/TRT duplicate is fetched once' + Assert-True (-not (Test-Path -LiteralPath $testRoot)) '-List must not create directories' + $all = @(& $scriptPath -Profile all -OnnxDir $missing -List) + Assert-True (@($all | Where-Object { $_.Profiles -contains 'refined' }).Count -eq 0) 'all excludes refined' + Assert-True (@($all | Where-Object { $_.Profiles -contains 'libreyolo' }).Count -eq 0) 'all matches Bash base profiles' + $optional = @(& $scriptPath -Profile all,refined,libreyolo -OnnxDir $missing -List) + Assert-True (@($optional | Where-Object Name -eq 'pipeline_refined.gguf').Count -eq 1) 'refined is available explicitly' + Assert-True (@($optional | Where-Object Name -eq 'libreyolo9.onnx').Count -eq 1) 'libreyolo is available explicitly' + Assert-Fails { & $scriptPath -Profile unknown -OnnxDir $missing -List } 'Unknown profile' + Assert-Fails { & $scriptPath -Profile shared -OnnxDir $missing -Revision "bad`nrevision" -List } 'Revision' + Assert-Fails { & $scriptPath -Profile shared -OnnxDir $missing -Yes } 'SAM3D_AUTO_FETCH=0' + Assert-True (-not (Test-Path -LiteralPath $testRoot)) 'Disabled downloads leave directories untouched' + + # A tiny fixture exercises SHA256 checking without creating/downloading models. + $fixtureTools = Join-Path $testRoot 'tools' + $fixtureModels = Join-Path $testRoot 'models' + [IO.Directory]::CreateDirectory($fixtureTools) | Out-Null + [IO.Directory]::CreateDirectory($fixtureModels) | Out-Null + $fixtureScript = Join-Path $fixtureTools 'fetch_model.ps1' + Copy-Item -LiteralPath $scriptPath -Destination $fixtureScript + $fixtureFile = Join-Path $fixtureModels 'fixture.bin' + [IO.File]::WriteAllBytes($fixtureFile, [byte[]]@(1, 2, 3, 4)) + $hash = (Get-FileHash -LiteralPath $fixtureFile -Algorithm SHA256).Hash.ToLowerInvariant() + $fixtureManifest = Join-Path $fixtureTools 'fetch_model.sh' + $manifest = "MANIFEST=(`n" + ' "shared|fixture.bin|4|' + $hash + '"' + "`n)`n" + [IO.File]::WriteAllText($fixtureManifest, $manifest) + $verified = @(& $fixtureScript -Profile shared -OnnxDir $fixtureModels -List) + Assert-True ($verified[0].Status -eq 'Verified') 'Correct existing file is verified' + $forced = @(& $fixtureScript -Profile shared -OnnxDir $fixtureModels -Force -List) + Assert-True ($forced[0].Status -eq 'Forced') '-Force schedules a verified existing file' + [IO.File]::WriteAllBytes($fixtureFile, [byte[]]@(4, 3, 2, 1)) + $invalid = @(& $fixtureScript -Profile shared -OnnxDir $fixtureModels -List) + Assert-True ($invalid[0].Status -eq 'Invalid') 'Same-size SHA256 corruption is not accepted' + Assert-Fails { & $fixtureScript -Profile shared -OnnxDir $fixtureModels -Yes } 'SAM3D_AUTO_FETCH=0' + Assert-True ([IO.File]::ReadAllBytes($fixtureFile)[0] -eq 4) 'Disabled fetch preserves an invalid existing file' + [IO.File]::WriteAllText($fixtureManifest, $manifest.Replace('fixture.bin', '..\escape.bin')) + Assert-Fails { & $fixtureScript -Profile shared -OnnxDir $fixtureModels -List } 'Unsafe model filename' + Write-Host "PASS: $checks offline fetch_model.ps1 checks." +} finally { + $env:SAM3D_AUTO_FETCH = $previousAutoFetch + $env:SAM3D_HF_REVISION = $previousRevision + if (Test-Path -LiteralPath $testRoot) { + $resolvedTestRoot = (Resolve-Path -LiteralPath $testRoot).Path + $expectedPrefix = Join-Path $repository 'build\fetch-model-tests-' + if (-not $resolvedTestRoot.StartsWith($expectedPrefix, [StringComparison]::OrdinalIgnoreCase)) { + throw 'Refusing to clean a test path outside the expected build directory.' + } + Remove-Item -LiteralPath $resolvedTestRoot -Recurse -Force + } +} diff --git a/tests/test_portable_getline.c b/tests/test_portable_getline.c new file mode 100644 index 0000000..73daa8e --- /dev/null +++ b/tests/test_portable_getline.c @@ -0,0 +1,29 @@ +#include "../src/GraphicsEngine/System/portable_getline.h" +#include +#include + +/* BVH motion rows can exceed a fixed input buffer and omit a final newline. */ +int main(void) +{ + FILE *input = tmpfile(); + char *line = NULL; + size_t capacity = 0; + int result = 1; + if (!input) return 1; + fputs("\n", input); + for (int i = 0; i < 8192; ++i) fputc('x', input); + fputs("\nlast", input); + rewind(input); + if (fsb_getline(&line, &capacity, input) != 1 || strcmp(line, "\n")) goto done; + if (fsb_getline(&line, &capacity, input) != 8193 || line[8192] != '\n' || line[8193]) goto done; + for (int i = 0; i < 8192; ++i) if (line[i] != 'x') goto done; + if (fsb_getline(&line, &capacity, input) != 4 || strcmp(line, "last")) goto done; + if (fsb_getline(&line, &capacity, input) != -1) goto done; + result = 0; +done: + free(line); + fclose(input); + if (result) fprintf(stderr, "FAIL: growing BVH line reader\n"); + else printf("PASS: empty line, long BVH row, final line and EOF\n"); + return result; +} diff --git a/tests/test_python_abi.py b/tests/test_python_abi.py new file mode 100644 index 0000000..554a971 --- /dev/null +++ b/tests/test_python_abi.py @@ -0,0 +1,116 @@ +"""C/Python ABI and native loader checks, without downloading any models. + +Pass --probe /path/to/test_python_abi_layout and optionally --lib-dir /build/dir. +Without --probe, only the failure guards and loader tests run. +""" + +import argparse +import ctypes +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import Mock, patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "python")) +import fsb_ctypes as fsb + +PROBE = os.environ.get("FSB_ABI_PROBE") +LIB_DIR = os.environ.get("FSB_TEST_LIB_DIR") + + +class NativeAbiTests(unittest.TestCase): + def test_c_struct_layout(self): + if not PROBE: + self.skipTest("Pass --probe for compiled C vs ctypes layout validation") + layout = json.loads(subprocess.check_output([PROBE], text=True)) + self.assertEqual(layout["abi_version"], fsb.ABI_VERSION) + for structure in (fsb.FsbConfig, fsb.FsbResult): + native = dict(layout[structure.__name__]) + self.assertEqual(native.pop("size"), ctypes.sizeof(structure)) + self.assertEqual(set(native), {name for name, _ in structure._fields_}) + for name, field_type in structure._fields_: + with self.subTest(structure=structure.__name__, field=name): + self.assertEqual(native[name], [getattr(structure, name).offset, + ctypes.sizeof(field_type)]) + + def test_real_library_exports_and_lifecycle(self): + if not LIB_DIR: + self.skipTest("Pass --lib-dir for built DLL/shared-library validation") + lib = fsb.load_library(LIB_DIR) + handle = lib.fsb_create() + self.assertTrue(handle) + try: + # Invalid inputs take the native guard path without loading models. + self.assertEqual(lib.fsb_load(handle, None), 0) + self.assertEqual(lib.fsb_process_bgr(handle, None, 0, 0, None, 0), 0) + finally: + lib.fsb_destroy(handle) + + +class AbiRejectionTests(unittest.TestCase): + @staticmethod + def library(**overrides): + values = { + "fsb_abi_version": fsb.ABI_VERSION, + "fsb_config_size": ctypes.sizeof(fsb.FsbConfig), + "fsb_result_size": ctypes.sizeof(fsb.FsbResult), + } + values.update(overrides) + return SimpleNamespace(**{name: Mock(return_value=value) + for name, value in values.items()}) + + def test_old_library_without_version_is_rejected(self): + with self.assertRaisesRegex(RuntimeError, "missing fsb_abi_version"): + fsb._check_abi(SimpleNamespace()) + + def test_changed_version_is_rejected(self): + with self.assertRaisesRegex(RuntimeError, "fsb_abi_version"): + fsb._check_abi(self.library(fsb_abi_version=fsb.ABI_VERSION + 1)) + + def test_short_config_is_rejected(self): + with self.assertRaisesRegex(RuntimeError, "fsb_config_size"): + fsb._check_abi(self.library(fsb_config_size=ctypes.sizeof(fsb.FsbConfig) - 8)) + + def test_old_result_stride_is_rejected(self): + # This is the previous frontend layout: it ended before skel_3d/has_skel. + with self.assertRaisesRegex(RuntimeError, "fsb_result_size"): + fsb._check_abi(self.library(fsb_result_size=fsb.FsbResult.skel_3d.offset)) + + def test_windows_release_loader_keeps_search_handles(self): + with tempfile.TemporaryDirectory(dir=Path(__file__).resolve().parent) as folder: + root = Path(folder) + output = root / "Release" + output.mkdir() + library_path = output / "fast_sam_3dbody.dll" + library_path.touch() + lib = self.library() + for name in ("fsb_create", "fsb_destroy", "fsb_load", "fsb_process_bgr"): + setattr(lib, name, Mock()) + with patch.object(fsb.sys, "platform", "win32"), \ + patch.dict(os.environ, {"PATH": ""}), \ + patch.object(fsb.os, "add_dll_directory", create=True) as add_directory, \ + patch.object(fsb.ctypes, "CDLL", return_value=lib) as cdll: + handle = Mock() + add_directory.return_value = handle + self.assertIs(fsb.load_library(str(root)), lib) + cdll.assert_called_once_with(str(library_path)) + self.assertEqual(add_directory.call_count, 2) + self.assertEqual(len(lib._fsb_dll_directories), 2) + handle.close.assert_not_called() + self.assertEqual(lib.fsb_process_bgr.argtypes[-2], + ctypes.POINTER(fsb.FsbResult)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--probe") + parser.add_argument("--lib-dir") + args, remainder = parser.parse_known_args() + PROBE = args.probe or PROBE + LIB_DIR = args.lib_dir or LIB_DIR + unittest.main(argv=[sys.argv[0], *remainder]) diff --git a/tests/test_python_abi_layout.c b/tests/test_python_abi_layout.c new file mode 100644 index 0000000..a592613 --- /dev/null +++ b/tests/test_python_abi_layout.c @@ -0,0 +1,49 @@ +/* Compile against the real public C header; no model/runtime dependency. */ +#include "fast_sam_3dbody_capi.h" +#include + +#define FIELD(type, member) \ + printf(",\"" #member "\":[%zu,%zu]", offsetof(type, member), \ + sizeof(((type*)0)->member)) + +int main(void) +{ + printf("{\"abi_version\":%u,\"FsbConfig\":{\"size\":%zu", + FSB_ABI_VERSION, sizeof(FsbConfig)); + FIELD(FsbConfig, onnx_dir); + FIELD(FsbConfig, gguf_path); + FIELD(FsbConfig, yolo_path); + FIELD(FsbConfig, cuda_device); + FIELD(FsbConfig, skip_body_model); + FIELD(FsbConfig, person_thresh); + FIELD(FsbConfig, person_nms_iou); + FIELD(FsbConfig, max_persons); + FIELD(FsbConfig, focal_x); + FIELD(FsbConfig, focal_y); + FIELD(FsbConfig, principal_x); + FIELD(FsbConfig, principal_y); + FIELD(FsbConfig, zero_face_params); + FIELD(FsbConfig, detector); + printf("},\"FsbResult\":{\"size\":%zu", sizeof(FsbResult)); + FIELD(FsbResult, bbox); + FIELD(FsbResult, focal_length); + FIELD(FsbResult, pred_cam_t); + FIELD(FsbResult, global_rot); + FIELD(FsbResult, body_pose); + FIELD(FsbResult, shape); + FIELD(FsbResult, scale); + FIELD(FsbResult, hand_pose); + FIELD(FsbResult, face_params); + FIELD(FsbResult, yolo_kps); + FIELD(FsbResult, has_yolo_kps); + FIELD(FsbResult, kps_3d); + FIELD(FsbResult, kps_2d); + FIELD(FsbResult, has_kps); + FIELD(FsbResult, pred_pose_raw); + FIELD(FsbResult, pred_cam_raw); + FIELD(FsbResult, mhr_model_params); + FIELD(FsbResult, skel_3d); + FIELD(FsbResult, has_skel); + printf("}}\n"); + return 0; +} diff --git a/tests/test_wgl_context.c b/tests/test_wgl_context.c new file mode 100644 index 0000000..81ed9ec --- /dev/null +++ b/tests/test_wgl_context.c @@ -0,0 +1,120 @@ +/* Model-free Windows graphics smoke test: a GLSL 3.3 draw, pixel readback, + * UTF-8 title, resize notification, graceful close, and context recreation. + * Pass --windowed to exercise a visible surface; the default stays hidden. */ +#include +#include +#include +#include +#include +#include "../src/GraphicsEngine/System/glx3.h" + +static unsigned int last_width, last_height; +int handleUserInput(int key, int state, int x, int y) +{ (void)key; (void)state; (void)x; (void)y; return 1; } +int windowSizeUpdated(unsigned int width, unsigned int height) +{ last_width = width; last_height = height; return 1; } + +#define CHECK(condition, message) do { if (!(condition)) { \ + fprintf(stderr, "FAIL: %s\n", message); stop_glx3_stuff(); return 1; } } while (0) + +static GLuint compile_shader(GLenum kind, const char *source) +{ + GLuint shader = glCreateShader(kind); + GLint success = 0; + glShaderSource(shader, 1, &source, NULL); + glCompileShader(shader); + glGetShaderiv(shader, GL_COMPILE_STATUS, &success); + if (!success) { glDeleteShader(shader); return 0; } + return shader; +} + +int main(int argc, const char **argv) +{ + int visible = argc > 1 && strcmp(argv[1], "--windowed") == 0; + const char *vertex_source = "#version 330 core\n" + "const vec2 p[3]=vec2[3](vec2(-1,-1),vec2(3,-1),vec2(-1,3));" + "void main(){gl_Position=vec4(p[gl_VertexID],0,1);}"; + const char *fragment_source = "#version 330 core\n" + "out vec4 color; void main(){color=vec4(1,0,0,1);}"; + GLuint vertex, fragment, program, vao; + GLint linked = 0; + unsigned char pixel[4] = {0}; + HWND window; + RECT client_area; + wchar_t title[256]; + + CHECK(!start_glx3_stuff(0, 64, 0, argc, argv), "reject invalid size"); + glx3_set_window_title("WGL smoke \xE6\xB5\x8B\xE8\xAF\x95"); + disableVSync(); + CHECK(start_glx3_stuff(64, 64, visible, argc, argv), "create first context"); + glewExperimental = GL_TRUE; + CHECK(glewInit() == GLEW_OK && GLEW_VERSION_3_3, "OpenGL 3.3 entrypoints"); + while (glGetError() != GL_NO_ERROR) {} /* GLEW probes legacy extensions. */ + window = WindowFromDC(wglGetCurrentDC()); + CHECK(window != NULL, "context owns a window"); + GetWindowTextW(window, title, 256); + CHECK(wcscmp(title, L"WGL smoke \x6D4B\x8BD5") == 0, "UTF-8 title"); + CHECK((IsWindowVisible(window) != 0) == visible, "requested visibility"); + + vertex = compile_shader(GL_VERTEX_SHADER, vertex_source); + fragment = compile_shader(GL_FRAGMENT_SHADER, fragment_source); + CHECK(vertex && fragment, "compile GLSL 3.3"); + program = glCreateProgram(); + glAttachShader(program, vertex); + glAttachShader(program, fragment); + glLinkProgram(program); + glGetProgramiv(program, GL_LINK_STATUS, &linked); + CHECK(linked, "link GLSL program"); + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + glUseProgram(program); + glViewport(0, 0, 64, 64); + glDrawArrays(GL_TRIANGLES, 0, 3); + glReadBuffer(GL_BACK); + glReadPixels(32, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + CHECK(glGetError() == GL_NO_ERROR, "draw and read without GL error"); + CHECK(pixel[0] > 240 && pixel[1] < 10 && pixel[2] < 10, "red triangle readback"); + CHECK(glx3_endRedraw(), "swap buffers"); + glDeleteVertexArrays(1, &vao); + glDeleteProgram(program); + glDeleteShader(vertex); + glDeleteShader(fragment); + + CHECK(SetWindowPos(window, NULL, 0, 0, 96, 80, SWP_NOMOVE | SWP_NOZORDER), "resize window"); + CHECK(GetClientRect(window, &client_area), "read resized client area"); + CHECK(glx3_checkEvents() && last_width == (unsigned int)client_area.right && + last_height == (unsigned int)client_area.bottom, "resize callback matches client area"); + SendMessageW(window, WM_CLOSE, 0, 0); + CHECK(glx3_should_close() && !glx3_checkEvents(), "window close exits event loop"); + CHECK(wglGetCurrentContext() != NULL && IsWindow(window), "close preserves surface for output cleanup"); + stop_glx3_stuff(); + CHECK(!wglGetCurrentContext() && !glx3_checkEvents(), "teardown releases context"); + + glx3_set_window_title(NULL); + CHECK(start_glx3_stuff(64, 64, 0, argc, argv), "recreate context"); + CHECK(!glx3_should_close() && glx3_checkEvents(), "new context resets close state"); + window = WindowFromDC(wglGetCurrentDC()); + GetWindowTextW(window, title, 256); + CHECK(wcscmp(title, L"SAM3DBody-cpp OpenGL3.x+ Visualization") == 0, "reset default title"); + SendMessageW(window, WM_KEYDOWN, VK_ESCAPE, 0); + CHECK(glx3_should_close() && !glx3_checkEvents(), "Escape exits event loop"); + stop_glx3_stuff(); + stop_glx3_stuff(); + { + /* Exercise legacy Windows main(char **) input when the system code + * page can represent the title without replacement characters. */ + char ansi_title[256]; + BOOL used_default = FALSE; + if (GetACP() != CP_UTF8 && WideCharToMultiByte(CP_ACP, 0, L"WGL \x6D4B\x8BD5", -1, + ansi_title, sizeof(ansi_title), NULL, &used_default) && !used_default) { + glx3_set_window_title(ansi_title); + CHECK(start_glx3_stuff(64, 64, 0, argc, argv), "ANSI title context"); + window = WindowFromDC(wglGetCurrentDC()); + GetWindowTextW(window, title, 256); + CHECK(wcscmp(title, L"WGL \x6D4B\x8BD5") == 0, "native ANSI CLI title"); + stop_glx3_stuff(); + } + } + printf("PASS: WGL 3.3 shaders, readback, title, resize, close and recreation\n"); + return 0; +} diff --git a/tools/fetch_model.ps1 b/tools/fetch_model.ps1 new file mode 100644 index 0000000..d7a4059 --- /dev/null +++ b/tools/fetch_model.ps1 @@ -0,0 +1,217 @@ +#requires -Version 5.1 +<# +.SYNOPSIS +Fetch verified SAM3DBody model files with native Windows PowerShell and curl.exe. +.DESCRIPTION +Reads the authoritative MANIFEST from fetch_model.sh in the same directory. +Shared files are implicit. The all profile selects cpu, cuda and trt; refined +and libreyolo remain opt-in. Existing files are checked by size and SHA256. +Downloads use .partial files; only verified files replace the final model name. +SAM3D_AUTO_FETCH=0 refuses downloads even with -Yes. A value of 1 skips prompting. +HF_TOKEN, when present, is passed to curl through stdin rather than process args. +.EXAMPLE +.\tools\fetch_model.ps1 -Profile cpu -List +.EXAMPLE +.\tools\fetch_model.ps1 -Profile cuda,refined -OnnxDir .\onnx -Yes +.EXAMPLE +powershell.exe -NoProfile -ExecutionPolicy Bypass -File tools\fetch_model.ps1 -Profile cpu -Yes +#> +[CmdletBinding()] +param( + [string[]] $Profile = @('cuda'), + [string] $OnnxDir, + [string] $Revision = $env:SAM3D_HF_REVISION, + [switch] $List, + [switch] $Force, + [switch] $Yes +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +if (-not $PSBoundParameters.ContainsKey('OnnxDir')) { + $OnnxDir = Join-Path $PSScriptRoot '..\onnx' +} + +$allowedProfiles = @('shared', 'cpu', 'cuda', 'trt', 'refined', 'libreyolo') +$selectedProfiles = @('shared') +foreach ($argument in $Profile) { + # Also accept the comma-separated string passed by powershell.exe -File. + foreach ($item in $argument.Split(',')) { + $item = $item.Trim().ToLowerInvariant() + if ($item -eq 'all') { + $selectedProfiles += @('cpu', 'cuda', 'trt') + } elseif ($allowedProfiles -contains $item) { + $selectedProfiles += $item + } else { + throw "Unknown profile '$item'. Use shared, cpu, cuda, trt, refined, libreyolo or all." + } + } +} +$selectedProfiles = @($selectedProfiles | Select-Object -Unique) +if ([string]::IsNullOrWhiteSpace($Revision)) { $Revision = 'main' } +if ($Revision -match '[\x00-\x20\x7f]') { throw 'Revision must not contain whitespace or control characters.' } +if ([string]::IsNullOrWhiteSpace($OnnxDir)) { throw 'OnnxDir must not be empty.' } +$destinationRoot = [IO.Path]::GetFullPath( + $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OnnxDir)) +if ((Test-Path -LiteralPath $destinationRoot) -and + -not (Test-Path -LiteralPath $destinationRoot -PathType Container)) { + throw 'OnnxDir must name a directory, not an existing file.' +} + +function Get-ModelPath([string] $Name) { + # Manifest entries are filenames, never commands or paths. + if ($Name -notmatch '^[A-Za-z0-9][A-Za-z0-9_.-]*$' -or $Name.EndsWith('.')) { + throw "Unsafe model filename in manifest: '$Name'." + } + $path = [IO.Path]::GetFullPath([IO.Path]::Combine($destinationRoot, $Name)) + if (-not [string]::Equals([IO.Path]::GetDirectoryName($path).TrimEnd([IO.Path]::DirectorySeparatorChar), + $destinationRoot.TrimEnd([IO.Path]::DirectorySeparatorChar), + [StringComparison]::OrdinalIgnoreCase)) { + throw 'Model destination escapes the selected OnnxDir.' + } + return $path +} + +function Test-ModelFile([string] $Path, [long] $Size, [string] $Sha256) { + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } + if ((Get-Item -LiteralPath $Path).Length -ne $Size) { return $false } + return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash -eq $Sha256 +} + +$manifestPath = Join-Path $PSScriptRoot 'fetch_model.sh' +$inManifest = $false +$foundManifest = $false +$closedManifest = $false +$models = [ordered]@{} +foreach ($line in Get-Content -LiteralPath $manifestPath -Encoding UTF8) { + $entry = $line.Trim() + if (-not $inManifest) { + if ($entry -eq 'MANIFEST=(') { $inManifest = $true; $foundManifest = $true } + continue + } + if ($entry -eq ')') { $closedManifest = $true; break } + if (-not $entry -or $entry.StartsWith('#')) { continue } + if ($entry -notmatch '^"([^"|]+)\|([^"|]+)\|([0-9]+)\|([0-9a-fA-F]{64})"$') { + throw 'Malformed entry in fetch_model.sh MANIFEST.' + } + $entryProfile = $Matches[1] + $name = $Matches[2] + $size = [long]$Matches[3] + $sha = $Matches[4].ToLowerInvariant() + if ($allowedProfiles -notcontains $entryProfile -or $size -le 0) { + throw 'Invalid profile or size in fetch_model.sh MANIFEST.' + } + $path = Get-ModelPath $name + if ($models.Contains($name)) { + if ($models[$name].SizeBytes -ne $size -or $models[$name].SHA256 -ne $sha) { + throw "Conflicting manifest entries for '$name'." + } + $models[$name].Profiles += $entryProfile + } else { + $models[$name] = [pscustomobject]@{ + Name = $name + Profiles = @($entryProfile) + SizeBytes = $size + SHA256 = $sha + Path = $path + Status = 'Missing' + } + } +} +if (-not $foundManifest -or -not $closedManifest -or $models.Count -eq 0) { + throw 'Cannot find a complete MANIFEST in fetch_model.sh.' +} + +$selected = @($models.Values | Where-Object { + $matchingProfiles = @($_.Profiles | Where-Object { $selectedProfiles -contains $_ }) + $matchingProfiles.Count -gt 0 +}) +foreach ($model in $selected) { + if (Test-Path -LiteralPath $model.Path -PathType Container) { + throw "A directory occupies the model filename '$($model.Name)'." + } + if ($Force) { + $model.Status = 'Forced' + } elseif (Test-ModelFile $model.Path $model.SizeBytes $model.SHA256) { + $model.Status = 'Verified' + } elseif (Test-Path -LiteralPath $model.Path) { + $model.Status = 'Invalid' + } +} +$pending = @($selected | Where-Object { $_.Status -ne 'Verified' }) +$totalBytes = [long]0 +foreach ($model in $pending) { $totalBytes += $model.SizeBytes } +Write-Host ('fetch_model.ps1: {0} selected, {1} to fetch ({2:N2} GiB), revision {3}' -f + $selected.Count, $pending.Count, ($totalBytes / 1GB), $Revision) +if ($List) { + $selected | Select-Object Name, Profiles, SizeBytes, SHA256, Status, Path + return +} +if ($pending.Count -eq 0) { + Write-Host "All selected model files verified in $destinationRoot." + return +} +foreach ($model in $pending) { + Write-Host (' {0,-34} {1,12:N0} bytes {2}' -f $model.Name, $model.SizeBytes, $model.Status) +} +if ($env:SAM3D_AUTO_FETCH -eq '0') { + throw 'SAM3D_AUTO_FETCH=0: downloading is disabled. No model files were changed.' +} +if (-not $Yes -and $env:SAM3D_AUTO_FETCH -ne '1') { + if (-not [Environment]::UserInteractive -or [Console]::IsInputRedirected) { + throw 'Non-interactive download requires -Yes or SAM3D_AUTO_FETCH=1.' + } + $answer = Read-Host "Download $($pending.Count) model files to $destinationRoot ? [y/N]" + if ($answer -notmatch '^(?i)y(es)?$') { throw 'Download cancelled.' } +} + +$curlCommand = Get-Command curl.exe -CommandType Application -ErrorAction Stop +$curlConfig = '' +if ($env:HF_TOKEN) { + if ($env:HF_TOKEN -match '[\x00-\x1f\x7f]') { + throw 'HF_TOKEN contains invalid control characters.' + } + $escapedToken = $env:HF_TOKEN.Replace('\', '\\').Replace('"', '\"') + $curlConfig = 'header = "Authorization: Bearer ' + $escapedToken + '"' +} +[IO.Directory]::CreateDirectory($destinationRoot) | Out-Null +$encodedRevision = [Uri]::EscapeDataString($Revision) +$index = 0 +foreach ($model in $pending) { + $index++ + $destination = Get-ModelPath $model.Name + $partial = Get-ModelPath ($model.Name + '.partial') + if (Test-Path -LiteralPath $partial -PathType Container) { + throw "A directory occupies the partial filename for '$($model.Name)'." + } + $have = [long]0 + if (Test-Path -LiteralPath $partial -PathType Leaf) { + $have = (Get-Item -LiteralPath $partial).Length + } + Write-Host "[$index/$($pending.Count)] $($model.Name)" + if (-not $Force -and $have -gt $model.SizeBytes) { + throw "Partial file for '$($model.Name)' exceeds the expected size. Rerun with -Force to restart it." + } + if ($Force -or $have -lt $model.SizeBytes) { + $url = 'https://huggingface.co/AmmarkoV/SAM3DBody-cpp-onnx-models/resolve/' + + $encodedRevision + '/' + [Uri]::EscapeDataString($model.Name) + '?download=true' + # Disable user curl config, including any trace option which could log auth. + $curlArguments = @('--disable', '--config', '-', '--location', '--fail', + '--show-error', '--progress-bar', '--retry', '3', '--retry-delay', '2', + '--connect-timeout', '30', '--proto', '=https', '--proto-redir', '=https', + '--output', $partial, '--url', $url) + if (-not $Force -and $have -gt 0) { $curlArguments += @('--continue-at', '-') } + $curlConfig | & $curlCommand.Source @curlArguments + if ($LASTEXITCODE -ne 0) { + throw "curl failed for '$($model.Name)' (exit $LASTEXITCODE). Partial kept for retry." + } + } + if (-not (Test-ModelFile $partial $model.SizeBytes $model.SHA256)) { + throw "Size or SHA256 verification failed for '$($model.Name)'. Partial kept; rerun with -Force to restart it." + } + # Both absolute paths were checked to be direct children of the selected dir. + # Keep an existing model intact until its replacement has passed verification. + Move-Item -LiteralPath $partial -Destination $destination -Force + Write-Host " Verified $($model.Name)." +} +Write-Host "Done: $($pending.Count) verified model files in $destinationRoot." From ab376f515bc37ba3d1f59f41dded56a031469025 Mon Sep 17 00:00:00 2001 From: reymondmeking-dot Date: Wed, 9 Sep 2026 21:55:37 +0800 Subject: [PATCH 2/2] Keep sync round-trip test files in the build directory --- src/multiview/sync_io_test.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/multiview/sync_io_test.cpp b/src/multiview/sync_io_test.cpp index 67d0719..d789c19 100644 --- a/src/multiview/sync_io_test.cpp +++ b/src/multiview/sync_io_test.cpp @@ -35,7 +35,8 @@ int main() b.has_time = true; b.t0_ms = 1769516877724.0; b.fps_eff = 29.9421; b.resid_med_ms = 7.1; s.cameras.push_back(b); - const char* path = "/tmp/sync_io_roundtrip.sync"; + // CTest runs this in the build directory; /tmp need not exist on Windows. + const char* path = "sync_io_roundtrip.sync"; if (!mv::write_sync(path, s)) { fprintf(stderr,"write_sync failed\n"); return 1; } mv::SyncSession r;