diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 53e494768..2a5b05b17 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -34,7 +34,7 @@ concurrency: jobs: cpp-build: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -45,12 +45,12 @@ jobs: if: github.ref_type == 'branch' needs: [python-build] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@main with: arch: "amd64" branch: ${{ inputs.branch }} build_type: ${{ inputs.build_type || 'branch' }} - container_image: "rapidsai/ci-conda:25.12-latest" + container_image: "rapidsai/ci-conda:26.02-latest" date: ${{ inputs.date }} node_type: "gpu-l4-latest-1" script: "ci/build_docs.sh" @@ -58,7 +58,7 @@ jobs: python-build: needs: [cpp-build] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -68,7 +68,7 @@ jobs: upload-conda: needs: [cpp-build, python-build] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -76,7 +76,7 @@ jobs: sha: ${{ inputs.sha }} wheel-build: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -88,7 +88,7 @@ jobs: wheel-publish: needs: wheel-build secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 8885145e4..ef81679a1 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -18,7 +18,7 @@ jobs: - wheel-tests - telemetry-setup secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@main telemetry-setup: runs-on: ubuntu-latest continue-on-error: true @@ -33,44 +33,44 @@ jobs: checks: secrets: inherit needs: telemetry-setup - uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@main with: ignored_pr_jobs: telemetry-summarize conda-cpp-build: needs: checks secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@main with: build_type: pull-request script: ci/build_cpp.sh conda-python-build: needs: conda-cpp-build secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@main with: build_type: pull-request script: ci/build_python.sh conda-python-tests: needs: conda-python-build secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@main with: build_type: pull-request script: ci/test_python.sh docs-build: needs: conda-python-build secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@main with: build_type: pull-request node_type: "gpu-l4-latest-1" arch: "amd64" - container_image: "rapidsai/ci-conda:25.12-latest" + container_image: "rapidsai/ci-conda:26.02-latest" script: "ci/build_docs.sh" wheel-build: needs: checks secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: pull-request script: ci/build_wheel.sh @@ -79,7 +79,7 @@ jobs: wheel-tests: needs: wheel-build secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@main with: build_type: pull-request script: ci/test_wheel.sh diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 23488ff1b..f8d3bedf5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -25,7 +25,7 @@ on: jobs: conda-python-tests: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@main with: build_type: ${{ inputs.build_type }} branch: ${{ inputs.branch }} @@ -34,7 +34,7 @@ jobs: sha: ${{ inputs.sha }} wheel-tests: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@main with: build_type: ${{ inputs.build_type }} branch: ${{ inputs.branch }} diff --git a/.github/workflows/trigger-breaking-change-alert.yaml b/.github/workflows/trigger-breaking-change-alert.yaml index 0b885544d..c471e2a15 100644 --- a/.github/workflows/trigger-breaking-change-alert.yaml +++ b/.github/workflows/trigger-breaking-change-alert.yaml @@ -12,7 +12,7 @@ jobs: trigger-notifier: if: contains(github.event.pull_request.labels.*.name, 'breaking') secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/breaking-change-alert.yaml@release/25.12 + uses: rapidsai/shared-workflows/.github/workflows/breaking-change-alert.yaml@main with: sender_login: ${{ github.event.sender.login }} sender_avatar: ${{ github.event.sender.avatar_url }} diff --git a/.gitignore b/.gitignore index 40bbb6354..bc75731c5 100644 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,7 @@ conda-bld # Custom debug environment setup script for VS Code (used by scripts/debug_python) /scripts/debug_env.sh +*.tiff +buildbackuup/ +*_cpp_documentation.md +junit-cucim.xml diff --git a/conda/recipes/libcucim/conda_build_config.yaml b/conda/recipes/libcucim/conda_build_config.yaml index 1082f0d21..b3f92ba0b 100644 --- a/conda/recipes/libcucim/conda_build_config.yaml +++ b/conda/recipes/libcucim/conda_build_config.yaml @@ -15,3 +15,6 @@ c_stdlib_version: cmake_version: - ">=3.30.4" + +nvimgcodec_version: + - ">=0.6.0" diff --git a/conda/recipes/libcucim/meta.yaml b/conda/recipes/libcucim/meta.yaml index 5b64725bc..612d9c01c 100644 --- a/conda/recipes/libcucim/meta.yaml +++ b/conda/recipes/libcucim/meta.yaml @@ -58,6 +58,7 @@ requirements: - cuda-cudart-dev - libcufile-dev - libnvjpeg-dev + - libnvimgcodec-dev {{ nvimgcodec_version }} # nvImageCodec development headers and libraries - nvtx-c >=3.1.0 - openslide run: @@ -69,8 +70,10 @@ requirements: - libcufile - cuda-cudart - libnvjpeg + - libnvimgcodec0 {{ nvimgcodec_version }} # nvImageCodec runtime library run_constrained: - {{ pin_compatible('openslide') }} + - libnvimgcodec-dev {{ nvimgcodec_version }} # Optional: for development/debugging about: home: https://developer.nvidia.com/multidimensional-image-processing diff --git a/cpp/cmake/deps/nvimgcodec.cmake b/cpp/cmake/deps/nvimgcodec.cmake new file mode 100644 index 000000000..85df92cf4 --- /dev/null +++ b/cpp/cmake/deps/nvimgcodec.cmake @@ -0,0 +1,232 @@ +# +# cmake-format: off +# SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. +# SPDX-License-Identifier: Apache-2.0 +# cmake-format: on +# + +if (NOT TARGET deps::nvimgcodec) + # Option to automatically install nvImageCodec via conda + option(AUTO_INSTALL_NVIMGCODEC "Automatically install nvImageCodec via conda" ON) + set(NVIMGCODEC_VERSION "0.7.0" CACHE STRING "nvImageCodec version to install") + + # Automatic installation logic + if(AUTO_INSTALL_NVIMGCODEC) + message(STATUS "Configuring automatic nvImageCodec installation...") + + # Try to find micromamba or conda in various locations + find_program(MICROMAMBA_EXECUTABLE + NAMES micromamba + PATHS ${CMAKE_CURRENT_SOURCE_DIR}/../../../bin + ${CMAKE_CURRENT_SOURCE_DIR}/../../bin + ${CMAKE_CURRENT_SOURCE_DIR}/bin + $ENV{HOME}/micromamba/bin + $ENV{HOME}/.local/bin + /usr/local/bin + /opt/conda/bin + /opt/miniconda/bin + DOC "Path to micromamba executable" + ) + + find_program(CONDA_EXECUTABLE + NAMES conda mamba + PATHS $ENV{HOME}/miniconda3/bin + $ENV{HOME}/anaconda3/bin + /opt/conda/bin + /opt/miniconda/bin + /usr/local/bin + DOC "Path to conda/mamba executable" + ) + + # Determine which conda tool to use + set(CONDA_CMD "") + set(CONDA_TYPE "") + if(MICROMAMBA_EXECUTABLE) + set(CONDA_CMD ${MICROMAMBA_EXECUTABLE}) + set(CONDA_TYPE "micromamba") + message(STATUS "Found micromamba: ${MICROMAMBA_EXECUTABLE}") + elseif(CONDA_EXECUTABLE) + set(CONDA_CMD ${CONDA_EXECUTABLE}) + set(CONDA_TYPE "conda") + message(STATUS "Found conda/mamba: ${CONDA_EXECUTABLE}") + endif() + + if(CONDA_CMD) + # Check if nvImageCodec is already installed + message(STATUS "Checking for existing nvImageCodec installation...") + execute_process( + COMMAND ${CONDA_CMD} list libnvimgcodec-dev + RESULT_VARIABLE NVIMGCODEC_CHECK_RESULT + OUTPUT_VARIABLE NVIMGCODEC_CHECK_OUTPUT + ERROR_QUIET + ) + + # Parse version from output if installed + set(NVIMGCODEC_INSTALLED_VERSION "") + if(NVIMGCODEC_CHECK_RESULT EQUAL 0) + string(REGEX MATCH "libnvimgcodec-dev[ ]+([0-9]+\\.[0-9]+\\.[0-9]+)" + VERSION_MATCH "${NVIMGCODEC_CHECK_OUTPUT}") + if(CMAKE_MATCH_1) + set(NVIMGCODEC_INSTALLED_VERSION ${CMAKE_MATCH_1}) + endif() + endif() + + # Install or upgrade if needed + set(NEED_INSTALL FALSE) + if(NOT NVIMGCODEC_CHECK_RESULT EQUAL 0) + message(STATUS "nvImageCodec not found - installing version ${NVIMGCODEC_VERSION}") + set(NEED_INSTALL TRUE) + elseif(NVIMGCODEC_INSTALLED_VERSION AND NVIMGCODEC_INSTALLED_VERSION VERSION_LESS NVIMGCODEC_VERSION) + message(STATUS "nvImageCodec ${NVIMGCODEC_INSTALLED_VERSION} found - upgrading to ${NVIMGCODEC_VERSION}") + set(NEED_INSTALL TRUE) + else() + message(STATUS "nvImageCodec ${NVIMGCODEC_INSTALLED_VERSION} already installed (>= ${NVIMGCODEC_VERSION})") + endif() + + if(NEED_INSTALL) + # Install nvImageCodec with specific version + message(STATUS "Installing nvImageCodec ${NVIMGCODEC_VERSION} via ${CONDA_TYPE}...") + execute_process( + COMMAND ${CONDA_CMD} install + libnvimgcodec-dev=${NVIMGCODEC_VERSION} + libnvimgcodec0=${NVIMGCODEC_VERSION} + -c conda-forge -y + RESULT_VARIABLE CONDA_INSTALL_RESULT + OUTPUT_VARIABLE CONDA_INSTALL_OUTPUT + ERROR_VARIABLE CONDA_INSTALL_ERROR + TIMEOUT 300 # 5 minute timeout + ) + + if(CONDA_INSTALL_RESULT EQUAL 0) + message(STATUS "✓ Successfully installed nvImageCodec ${NVIMGCODEC_VERSION}") + else() + message(WARNING "✗ Failed to install nvImageCodec via ${CONDA_TYPE}") + message(WARNING "Error: ${CONDA_INSTALL_ERROR}") + + # Try alternative installation without version constraint + message(STATUS "Attempting installation without version constraint...") + execute_process( + COMMAND ${CONDA_CMD} install libnvimgcodec-dev libnvimgcodec0 -c conda-forge -y + RESULT_VARIABLE CONDA_FALLBACK_RESULT + OUTPUT_QUIET + ERROR_QUIET + ) + + if(CONDA_FALLBACK_RESULT EQUAL 0) + message(STATUS "✓ Fallback installation successful") + else() + message(WARNING "✗ Fallback installation also failed") + endif() + endif() + endif() + else() + message(STATUS "No conda/micromamba found - skipping automatic installation") + endif() + endif() + + # First try to find it as a package + find_package(nvimgcodec QUIET) + + if(nvimgcodec_FOUND) + # Use the found package + add_library(deps::nvimgcodec INTERFACE IMPORTED GLOBAL) + target_link_libraries(deps::nvimgcodec INTERFACE nvimgcodec::nvimgcodec) + message(STATUS "✓ nvImageCodec found via find_package") + else() + # Manual detection in various environments + set(NVIMGCODEC_LIB_PATH "") + set(NVIMGCODEC_INCLUDE_PATH "") + + # Try conda environment detection (both Python packages and native packages) + if(DEFINED ENV{CONDA_BUILD}) + # Conda build environment + set(NVIMGCODEC_LIB_PATH "$ENV{PREFIX}/lib/libnvimgcodec.so.0") + set(NVIMGCODEC_INCLUDE_PATH "$ENV{PREFIX}/include/") + if(NOT EXISTS "${NVIMGCODEC_LIB_PATH}") + set(NVIMGCODEC_LIB_PATH "$ENV{PREFIX}/lib/libnvimgcodec.so") + endif() + elseif(DEFINED ENV{CONDA_PREFIX}) + # Active conda environment - try native package first + set(CONDA_NATIVE_ROOT "$ENV{CONDA_PREFIX}") + if(EXISTS "${CONDA_NATIVE_ROOT}/include/nvimgcodec.h") + set(NVIMGCODEC_INCLUDE_PATH "${CONDA_NATIVE_ROOT}/include/") + if(EXISTS "${CONDA_NATIVE_ROOT}/lib/libnvimgcodec.so.0") + set(NVIMGCODEC_LIB_PATH "${CONDA_NATIVE_ROOT}/lib/libnvimgcodec.so.0") + elseif(EXISTS "${CONDA_NATIVE_ROOT}/lib/libnvimgcodec.so") + set(NVIMGCODEC_LIB_PATH "${CONDA_NATIVE_ROOT}/lib/libnvimgcodec.so") + endif() + else() + # Fallback: try Python site-packages in conda environment + foreach(PY_VER "3.13" "3.12" "3.11" "3.10" "3.9") + set(CONDA_PYTHON_ROOT "$ENV{CONDA_PREFIX}/lib/python${PY_VER}/site-packages/nvidia/nvimgcodec") + if(EXISTS "${CONDA_PYTHON_ROOT}/include/nvimgcodec.h") + set(NVIMGCODEC_INCLUDE_PATH "${CONDA_PYTHON_ROOT}/include/") + if(EXISTS "${CONDA_PYTHON_ROOT}/lib/libnvimgcodec.so.0") + set(NVIMGCODEC_LIB_PATH "${CONDA_PYTHON_ROOT}/lib/libnvimgcodec.so.0") + elseif(EXISTS "${CONDA_PYTHON_ROOT}/lib/libnvimgcodec.so") + set(NVIMGCODEC_LIB_PATH "${CONDA_PYTHON_ROOT}/lib/libnvimgcodec.so") + endif() + break() + endif() + endforeach() + endif() + else() + # Try Python site-packages detection + find_package(Python3 COMPONENTS Interpreter) + if(Python3_FOUND) + execute_process( + COMMAND ${Python3_EXECUTABLE} -c "import site; print(site.getsitepackages()[0])" + OUTPUT_VARIABLE PYTHON_SITE_PACKAGES + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + + if(PYTHON_SITE_PACKAGES) + set(NVIMGCODEC_PYTHON_ROOT "${PYTHON_SITE_PACKAGES}/nvidia/nvimgcodec") + if(EXISTS "${NVIMGCODEC_PYTHON_ROOT}/include/nvimgcodec.h") + set(NVIMGCODEC_INCLUDE_PATH "${NVIMGCODEC_PYTHON_ROOT}/include/") + if(EXISTS "${NVIMGCODEC_PYTHON_ROOT}/lib/libnvimgcodec.so.0") + set(NVIMGCODEC_LIB_PATH "${NVIMGCODEC_PYTHON_ROOT}/lib/libnvimgcodec.so.0") + elseif(EXISTS "${NVIMGCODEC_PYTHON_ROOT}/lib/libnvimgcodec.so") + set(NVIMGCODEC_LIB_PATH "${NVIMGCODEC_PYTHON_ROOT}/lib/libnvimgcodec.so") + endif() + endif() + endif() + endif() + + # System-wide installation fallback + if(NOT NVIMGCODEC_LIB_PATH) + if(EXISTS /usr/lib/x86_64-linux-gnu/libnvimgcodec.so.0) + set(NVIMGCODEC_LIB_PATH /usr/lib/x86_64-linux-gnu/libnvimgcodec.so.0) + set(NVIMGCODEC_INCLUDE_PATH "/usr/include/") + elseif(EXISTS /usr/lib/aarch64-linux-gnu/libnvimgcodec.so.0) + set(NVIMGCODEC_LIB_PATH /usr/lib/aarch64-linux-gnu/libnvimgcodec.so.0) + set(NVIMGCODEC_INCLUDE_PATH "/usr/include/") + elseif(EXISTS /usr/lib64/libnvimgcodec.so.0) # CentOS (x86_64) + set(NVIMGCODEC_LIB_PATH /usr/lib64/libnvimgcodec.so.0) + set(NVIMGCODEC_INCLUDE_PATH "/usr/include/") + endif() + endif() + endif() + + # Create the target if we found the library + if(NVIMGCODEC_LIB_PATH AND EXISTS "${NVIMGCODEC_LIB_PATH}") + add_library(deps::nvimgcodec SHARED IMPORTED GLOBAL) + set_target_properties(deps::nvimgcodec PROPERTIES + IMPORTED_LOCATION "${NVIMGCODEC_LIB_PATH}" + INTERFACE_INCLUDE_DIRECTORIES "${NVIMGCODEC_INCLUDE_PATH}" + ) + message(STATUS "✓ nvImageCodec found:") + message(STATUS " Library: ${NVIMGCODEC_LIB_PATH}") + message(STATUS " Headers: ${NVIMGCODEC_INCLUDE_PATH}") + else() + # Create a dummy target to prevent build failures + add_library(deps::nvimgcodec INTERFACE IMPORTED GLOBAL) + message(STATUS "✗ nvImageCodec not found - GPU acceleration disabled") + message(STATUS "To enable nvImageCodec support:") + message(STATUS " Option 1 (conda): micromamba install libnvimgcodec-dev -c conda-forge") + message(STATUS " Option 2 (pip): pip install nvidia-nvimgcodec-cu12[all]") + message(STATUS " Option 3 (cmake): cmake -DAUTO_INSTALL_NVIMGCODEC=ON ..") + endif() + endif() +endif() diff --git a/cpp/plugins/cucim.kit.cumed/.idea/.gitignore b/cpp/plugins/cucim.kit.cumed/.idea/.gitignore deleted file mode 100644 index 73f69e095..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/cpp/plugins/cucim.kit.cumed/.idea/.name b/cpp/plugins/cucim.kit.cumed/.idea/.name deleted file mode 100644 index 93c67f482..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -cumed diff --git a/cpp/plugins/cucim.kit.cumed/.idea/codeStyles/Project.xml b/cpp/plugins/cucim.kit.cumed/.idea/codeStyles/Project.xml deleted file mode 100644 index c8f84c353..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/cpp/plugins/cucim.kit.cumed/.idea/codeStyles/codeStyleConfig.xml b/cpp/plugins/cucim.kit.cumed/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 0f7bc519d..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/cpp/plugins/cucim.kit.cumed/.idea/cucim.kit.cumed.iml b/cpp/plugins/cucim.kit.cumed/.idea/cucim.kit.cumed.iml deleted file mode 100644 index 8afe22e01..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/cucim.kit.cumed.iml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/includes/NVIDIA_CMAKE_HEADER.cmake b/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/includes/NVIDIA_CMAKE_HEADER.cmake deleted file mode 100644 index f11461a8a..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/includes/NVIDIA_CMAKE_HEADER.cmake +++ /dev/null @@ -1,6 +0,0 @@ -# -# cmake-format: off -# SPDX-FileCopyrightText: Copyright (c) $YEAR, NVIDIA CORPORATION. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# cmake-format: on -# diff --git a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h b/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h deleted file mode 100644 index 0255849f3..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h +++ /dev/null @@ -1,4 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) $YEAR, NVIDIA CORPORATION. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ diff --git a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/C Header File.h b/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/C Header File.h deleted file mode 100644 index 9cb1d09e2..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/C Header File.h +++ /dev/null @@ -1,5 +0,0 @@ -#parse("NVIDIA_C_HEADER.h") -#[[#ifndef]]# ${INCLUDE_GUARD} -#[[#define]]# ${INCLUDE_GUARD} - -#[[#endif]]# //${INCLUDE_GUARD} diff --git a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/C Source File.c b/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/C Source File.c deleted file mode 100644 index b04dd6c62..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/C Source File.c +++ /dev/null @@ -1,4 +0,0 @@ -#parse("NVIDIA_C_HEADER.h") -#if (${HEADER_FILENAME}) -#[[#include]]# "${HEADER_FILENAME}" -#end diff --git a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/C++ Class Header.h b/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/C++ Class Header.h deleted file mode 100644 index f521fa555..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/C++ Class Header.h +++ /dev/null @@ -1,13 +0,0 @@ -#parse("NVIDIA_C_HEADER.h") -#[[#ifndef]]# ${INCLUDE_GUARD} -#[[#define]]# ${INCLUDE_GUARD} - -${NAMESPACES_OPEN} - -class ${NAME} { - -}; - -${NAMESPACES_CLOSE} - -#[[#endif]]# //${INCLUDE_GUARD} diff --git a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/C++ Class.cc b/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/C++ Class.cc deleted file mode 100644 index 42f43ccf4..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/C++ Class.cc +++ /dev/null @@ -1,2 +0,0 @@ -#parse("NVIDIA_C_HEADER.h") -#[[#include]]# "${HEADER_FILENAME}" diff --git a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/CMakeLists.txt.cmake b/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/CMakeLists.txt.cmake deleted file mode 100644 index 846356219..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/fileTemplates/internal/CMakeLists.txt.cmake +++ /dev/null @@ -1 +0,0 @@ -#parse("NVIDIA_CMAKE_HEADER.cmake") diff --git a/cpp/plugins/cucim.kit.cumed/.idea/misc.xml b/cpp/plugins/cucim.kit.cumed/.idea/misc.xml deleted file mode 100644 index 2019083a1..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/cpp/plugins/cucim.kit.cumed/.idea/vcs.xml b/cpp/plugins/cucim.kit.cumed/.idea/vcs.xml deleted file mode 100644 index fbbc5665e..000000000 --- a/cpp/plugins/cucim.kit.cumed/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/.gitignore b/cpp/plugins/cucim.kit.cuslide/.idea/.gitignore deleted file mode 100644 index 73f69e095..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/.name b/cpp/plugins/cucim.kit.cuslide/.idea/.name deleted file mode 100644 index cc09966de..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -cuslide diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/Project.xml b/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/Project.xml deleted file mode 100644 index c8f84c353..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/codeStyleConfig.xml b/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 0f7bc519d..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/cucim.kit.cuslide.iml b/cpp/plugins/cucim.kit.cuslide/.idea/cucim.kit.cuslide.iml deleted file mode 100644 index 08cda128a..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/cucim.kit.cuslide.iml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/includes/NVIDIA_CMAKE_HEADER.cmake b/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/includes/NVIDIA_CMAKE_HEADER.cmake deleted file mode 100644 index f11461a8a..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/includes/NVIDIA_CMAKE_HEADER.cmake +++ /dev/null @@ -1,6 +0,0 @@ -# -# cmake-format: off -# SPDX-FileCopyrightText: Copyright (c) $YEAR, NVIDIA CORPORATION. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# cmake-format: on -# diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h b/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h deleted file mode 100644 index 0255849f3..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h +++ /dev/null @@ -1,4 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) $YEAR, NVIDIA CORPORATION. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/C Header File.h b/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/C Header File.h deleted file mode 100644 index 9cb1d09e2..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/C Header File.h +++ /dev/null @@ -1,5 +0,0 @@ -#parse("NVIDIA_C_HEADER.h") -#[[#ifndef]]# ${INCLUDE_GUARD} -#[[#define]]# ${INCLUDE_GUARD} - -#[[#endif]]# //${INCLUDE_GUARD} diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/C Source File.c b/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/C Source File.c deleted file mode 100644 index b04dd6c62..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/C Source File.c +++ /dev/null @@ -1,4 +0,0 @@ -#parse("NVIDIA_C_HEADER.h") -#if (${HEADER_FILENAME}) -#[[#include]]# "${HEADER_FILENAME}" -#end diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/C++ Class Header.h b/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/C++ Class Header.h deleted file mode 100644 index f521fa555..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/C++ Class Header.h +++ /dev/null @@ -1,13 +0,0 @@ -#parse("NVIDIA_C_HEADER.h") -#[[#ifndef]]# ${INCLUDE_GUARD} -#[[#define]]# ${INCLUDE_GUARD} - -${NAMESPACES_OPEN} - -class ${NAME} { - -}; - -${NAMESPACES_CLOSE} - -#[[#endif]]# //${INCLUDE_GUARD} diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/C++ Class.cc b/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/C++ Class.cc deleted file mode 100644 index 42f43ccf4..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/C++ Class.cc +++ /dev/null @@ -1,2 +0,0 @@ -#parse("NVIDIA_C_HEADER.h") -#[[#include]]# "${HEADER_FILENAME}" diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/CMakeLists.txt.cmake b/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/CMakeLists.txt.cmake deleted file mode 100644 index 846356219..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/CMakeLists.txt.cmake +++ /dev/null @@ -1 +0,0 @@ -#parse("NVIDIA_CMAKE_HEADER.cmake") diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/misc.xml b/cpp/plugins/cucim.kit.cuslide/.idea/misc.xml deleted file mode 100644 index 2019083a1..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/vcs.xml b/cpp/plugins/cucim.kit.cuslide/.idea/vcs.xml deleted file mode 100644 index fbbc5665e..000000000 --- a/cpp/plugins/cucim.kit.cuslide/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/deps/libjpeg-turbo.cmake b/cpp/plugins/cucim.kit.cuslide/cmake/deps/libjpeg-turbo.cmake index ba750c92d..19f578ea9 100644 --- a/cpp/plugins/cucim.kit.cuslide/cmake/deps/libjpeg-turbo.cmake +++ b/cpp/plugins/cucim.kit.cuslide/cmake/deps/libjpeg-turbo.cmake @@ -31,7 +31,22 @@ if (NOT TARGET deps::libjpeg-turbo) # full path to the compiler, or to the compiler name if it is in the PATH. # yasm is available through `sudo apt-get install yasm` on Debian Linux. # See _deps/deps-libjpeg-turbo-src/simd/CMakeLists.txt:25. - set(CMAKE_ASM_NASM_COMPILER yasm) + + # Try to find yasm in conda environment first, then system paths + if(DEFINED ENV{CONDA_PREFIX}) + find_program(YASM_EXECUTABLE NAMES yasm PATHS $ENV{CONDA_PREFIX}/bin NO_DEFAULT_PATH) + endif() + if(NOT YASM_EXECUTABLE) + find_program(YASM_EXECUTABLE NAMES yasm) + endif() + + if(YASM_EXECUTABLE) + set(CMAKE_ASM_NASM_COMPILER ${YASM_EXECUTABLE}) + message(STATUS "Found yasm: ${YASM_EXECUTABLE}") + else() + set(CMAKE_ASM_NASM_COMPILER yasm) + message(WARNING "yasm not found, using 'yasm' and hoping it's in PATH") + endif() set(REQUIRE_SIMD 1) # CMP0077 message(STATUS "Fetching libjpeg-turbo sources") diff --git a/cpp/plugins/cucim.kit.cuslide/src/cuslide/tiff/ifd.cpp b/cpp/plugins/cucim.kit.cuslide/src/cuslide/tiff/ifd.cpp index e05ef48b6..a935ef266 100644 --- a/cpp/plugins/cucim.kit.cuslide/src/cuslide/tiff/ifd.cpp +++ b/cpp/plugins/cucim.kit.cuslide/src/cuslide/tiff/ifd.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2020-2021, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. * SPDX-License-Identifier: Apache-2.0 */ @@ -121,12 +121,17 @@ IFD::IFD(TIFF* tiff, uint16_t index, ifd_offset_t offset) : tiff_(tiff), ifd_ind // TIFFPrintDirectory(tif, stdout, TIFFPRINT_STRIPS); } +IFD::~IFD() +{ +} + bool IFD::read(const TIFF* tiff, const cucim::io::format::ImageMetadataDesc* metadata, const cucim::io::format::ImageReaderRegionRequestDesc* request, cucim::io::format::ImageDataDesc* out_image_data) { PROF_SCOPED_RANGE(PROF_EVENT(ifd_read)); + ::TIFF* tif = tiff->tiff_client_; uint16_t ifd_index = ifd_index_; @@ -160,8 +165,11 @@ bool IFD::read(const TIFF* tiff, raster = out_buf->data; } + fmt::print("🔎 Checking is_read_optimizable(): {}\n", is_read_optimizable()); + if (is_read_optimizable()) { + fmt::print("✅ Using optimized read path\n"); if (batch_size > 1) { ndim = 4; @@ -210,8 +218,11 @@ bool IFD::read(const TIFF* tiff, const IFD* ifd = this; + fmt::print("📍 location_len={}, batch_size={}, num_workers={}\n", location_len, batch_size, num_workers); + if (location_len > 1 || batch_size > 1 || num_workers > 0) { + fmt::print("📍 Entering multi-location/batch/worker path\n"); // Reconstruct location std::unique_ptr>* location_unique = reinterpret_cast>*>(request->location_unique); @@ -240,8 +251,16 @@ bool IFD::read(const TIFF* tiff, std::unique_ptr batch_processor; // Set raster_type to CUDA because loader will handle this with nvjpeg - if (out_device.type() == cucim::io::DeviceType::kCUDA) + // BUT: NvJpegProcessor only handles JPEG (not JPEG2000), so check compression + fmt::print("📍 Checking device type: {} compression: {}\n", + static_cast(out_device.type()), compression_); + + bool is_jpeg2000 = (compression_ == cuslide::jpeg2k::kAperioJpeg2kYCbCr || + compression_ == cuslide::jpeg2k::kAperioJpeg2kRGB); + + if (out_device.type() == cucim::io::DeviceType::kCUDA && !is_jpeg2000) { + fmt::print("📍 Using CUDA device path with nvjpeg loader\n"); raster_type = cucim::io::DeviceType::kCUDA; // The maximal number of tiles (x-axis) overapped with the given patch @@ -270,20 +289,32 @@ bool IFD::read(const TIFF* tiff, prefetch_factor = nvjpeg_processor->preferred_loader_prefetch_factor(); batch_processor = std::move(nvjpeg_processor); + fmt::print("📍 NvJpegProcessor created\n"); + } + else if (is_jpeg2000) + { + fmt::print("⚠️ JPEG2000 detected - skipping NvJpegProcessor (will use nvImageCodec/OpenJPEG)\n"); } + fmt::print("📍 Creating ThreadBatchDataLoader (location_len={}, batch_size={}, num_workers={})\n", + location_len, batch_size, num_workers); auto loader = std::make_unique( load_func, std::move(batch_processor), out_device, std::move(request_location), std::move(request_size), location_len, one_raster_size, batch_size, prefetch_factor, num_workers); + fmt::print("📍 ThreadBatchDataLoader created\n"); const uint32_t load_size = std::min(static_cast(batch_size) * (1 + prefetch_factor), location_len); + fmt::print("📍 Calling loader->request({})\n", load_size); loader->request(load_size); + fmt::print("📍 loader->request() completed\n"); // If it reads entire image with multi threads (using loader), fetch the next item. if (location_len == 1 && batch_size == 1) { + fmt::print("📍 Calling loader->next_data()\n"); raster = loader->next_data(); + fmt::print("📍 loader->next_data() returned\n"); } out_image_data->loader = loader.release(); // set loader to out_image_data @@ -665,20 +696,49 @@ bool IFD::read_region_tiles(const TIFF* tiff, (pixel_offset_ex - tile_pixel_offset_x + 1) * samples_per_pixel : (tw - tile_pixel_offset_x) * samples_per_pixel; auto decode_func = [=, &image_cache]() { - PROF_SCOPED_RANGE(PROF_EVENT_P(ifd_read_region_tiles_task, index_hash)); + fmt::print("🔍🔍🔍 INSIDE decode_func lambda! index={}\n", index); + fflush(stdout); + // TEMPORARY: Disable profiling macro - it's causing the segfault + // PROF_SCOPED_RANGE(PROF_EVENT_P(ifd_read_region_tiles_task, index_hash)); + + fmt::print("🔍 Calculating nbytes_tile_index: tile_pixel_offset_sy={}, tw={}, tile_pixel_offset_x={}, samples_per_pixel={}\n", + tile_pixel_offset_sy, tw, tile_pixel_offset_x, samples_per_pixel); + fflush(stdout); uint32_t nbytes_tile_index = (tile_pixel_offset_sy * tw + tile_pixel_offset_x) * samples_per_pixel; + fmt::print("🔍 nbytes_tile_index={}\n", nbytes_tile_index); + fflush(stdout); + uint32_t dest_pixel_index = dest_pixel_index_x; + fmt::print("🔍 dest_pixel_index={}\n", dest_pixel_index); + fflush(stdout); + uint8_t* tile_data = nullptr; + fmt::print("🔍 Checking tiledata_size: {}\n", tiledata_size); + fflush(stdout); if (tiledata_size > 0) { + fmt::print("🔍 Entered tiledata_size > 0 block\n"); + fflush(stdout); + std::unique_ptr tile_raster = std::unique_ptr(nullptr, cucim_free); - if (loader && loader->batch_data_processor()) + fmt::print("🔍 Created tile_raster unique_ptr\n"); + fflush(stdout); + + // TEMPORARY: Completely skip the loader path - it causes segfaults + // Go directly to the standard decode path (else block) + fmt::print("🔍 Skipping loader path, going to standard decode\n"); + fflush(stdout); + + if (false) // FORCE to skip loader path { + // This block is never executed switch (compression_method) { case COMPRESSION_JPEG: + case cuslide::jpeg2k::kAperioJpeg2kYCbCr: // 33003 + case cuslide::jpeg2k::kAperioJpeg2kRGB: // 33005 break; default: throw std::runtime_error("Unsupported compression method"); @@ -698,41 +758,92 @@ bool IFD::read_region_tiles(const TIFF* tiff, } else { + fmt::print("🔍 Entered else block - standard decode path\n"); + fflush(stdout); + auto key = image_cache.create_key(ifd_hash_value, index); + fmt::print("🔍 Created cache key\n"); + fflush(stdout); + image_cache.lock(index_hash); + fmt::print("🔍 Locked cache\n"); + fflush(stdout); + auto value = image_cache.find(key); - if (value) + fmt::print("🔍 Cache lookup complete\n"); + fflush(stdout); + + fmt::print("🔍 About to check if value exists (cache hit/miss)\n"); + fflush(stdout); + + bool value_exists = false; + try { + value_exists = (value != nullptr) && (value.get() != nullptr); + fmt::print("🔍 Value check complete: value_exists={}\n", value_exists); + fflush(stdout); + } catch (...) { + fmt::print("❌ Exception checking value!\n"); + fflush(stdout); + throw; + } + + if (value_exists) { + fmt::print("🔍 Cache HIT - using cached tile\n"); + fflush(stdout); image_cache.unlock(index_hash); tile_data = static_cast(value->data); } else { + fmt::print("🔍 Cache MISS - need to decode tile\n"); + fflush(stdout); + // Lifetime of tile_data is same with `value` // : do not access this data when `value` is not accessible. + fmt::print("🔍 Checking cache_type: {}\n", static_cast(cache_type)); + fflush(stdout); + if (cache_type != cucim::cache::CacheType::kNoCache) { + fmt::print("🔍 Allocating from image_cache, size={}\n", tile_raster_nbytes); + fflush(stdout); tile_data = static_cast(image_cache.allocate(tile_raster_nbytes)); + fmt::print("🔍 Allocated tile_data={}\n", static_cast(tile_data)); + fflush(stdout); } else { + fmt::print("🔍 Allocating temporary buffer with cucim_malloc\n"); + fflush(stdout); // Allocate temporary buffer for tile data tile_raster = std::unique_ptr( reinterpret_cast(cucim_malloc(tile_raster_nbytes)), cucim_free); tile_data = tile_raster.get(); + fmt::print("🔍 Allocated tile_data={}\n", static_cast(tile_data)); + fflush(stdout); } { - PROF_SCOPED_RANGE(PROF_EVENT(ifd_decompression)); + fmt::print("🔍 About to switch on compression_method={}\n", compression_method); + fflush(stdout); + // TEMPORARY: Disable profiling macro - it causes segfaults in lambdas + // PROF_SCOPED_RANGE(PROF_EVENT(ifd_decompression)); switch (compression_method) { case COMPRESSION_NONE: + fmt::print("🔍 Calling decode_raw\n"); + fflush(stdout); cuslide::raw::decode_raw(tiff_file, nullptr, tiledata_offset, tiledata_size, &tile_data, tile_raster_nbytes, out_device); break; case COMPRESSION_JPEG: + fmt::print("🔍 Calling decode_libjpeg\n"); + fflush(stdout); cuslide::jpeg::decode_libjpeg(tiff_file, nullptr, tiledata_offset, tiledata_size, jpegtable_data, jpegtable_count, &tile_data, out_device, jpeg_color_space); + fmt::print("🔍 decode_libjpeg completed\n"); + fflush(stdout); break; case COMPRESSION_ADOBE_DEFLATE: case COMPRESSION_DEFLATE: @@ -740,16 +851,27 @@ bool IFD::read_region_tiles(const TIFF* tiff, &tile_data, tile_raster_nbytes, out_device); break; case cuslide::jpeg2k::kAperioJpeg2kYCbCr: // 33003 + fmt::print("🔍 Calling decode_libopenjpeg (YCbCr)\n"); + fflush(stdout); cuslide::jpeg2k::decode_libopenjpeg(tiff_file, nullptr, tiledata_offset, tiledata_size, &tile_data, tile_raster_nbytes, out_device, cuslide::jpeg2k::ColorSpace::kSYCC); + fmt::print("🔍 decode_libopenjpeg (YCbCr) completed\n"); + fflush(stdout); break; case cuslide::jpeg2k::kAperioJpeg2kRGB: // 33005 + fmt::print("🔍 Calling decode_libopenjpeg (RGB), fd={}, offset={}, size={}\n", + tiff_file, tiledata_offset, tiledata_size); + fflush(stdout); cuslide::jpeg2k::decode_libopenjpeg(tiff_file, nullptr, tiledata_offset, tiledata_size, &tile_data, tile_raster_nbytes, out_device, cuslide::jpeg2k::ColorSpace::kRGB); + fmt::print("🔍 decode_libopenjpeg completed!\n"); + fflush(stdout); break; case COMPRESSION_LZW: + fmt::print("🔍 Calling decode_lzw\n"); + fflush(stdout); cuslide::lzw::decode_lzw(tiff_file, nullptr, tiledata_offset, tiledata_size, &tile_data, tile_raster_nbytes, out_device); // Apply unpredictor @@ -759,27 +881,97 @@ bool IFD::read_region_tiles(const TIFF* tiff, { cuslide::lzw::horAcc8(tile_data, tile_raster_nbytes, nbytes_tw); } + fmt::print("🔍 decode_lzw completed\n"); + fflush(stdout); break; default: + fmt::print("❌ Unsupported compression method: {}\n", compression_method); + fflush(stdout); throw std::runtime_error("Unsupported compression method"); } + fmt::print("🔍 Switch statement completed, decompression done\n"); + fflush(stdout); } + fmt::print("🔍 Creating cache value\n"); + fflush(stdout); value = image_cache.create_value(tile_data, tile_raster_nbytes); + fmt::print("🔍 Inserting into cache\n"); + fflush(stdout); image_cache.insert(key, value); + fmt::print("🔍 Unlocking cache\n"); + fflush(stdout); image_cache.unlock(index_hash); + fmt::print("🔍 Cache operations complete\n"); + fflush(stdout); } + fmt::print("🔍 Starting memcpy loop: tile_pixel_offset_sy={}, tile_pixel_offset_ey={}\n", + tile_pixel_offset_sy, tile_pixel_offset_ey); + fflush(stdout); + for (uint32_t ty = tile_pixel_offset_sy; ty <= tile_pixel_offset_ey; ++ty, dest_pixel_index += dest_pixel_step_y, nbytes_tile_index += nbytes_tw) { - memcpy(dest_start_ptr + dest_pixel_index, tile_data + nbytes_tile_index, - nbytes_tile_pixel_size_x); + fmt::print("🔍 memcpy iteration ty={}\n", ty); + fmt::print("🔍 dest_start_ptr={}, dest_pixel_index={}, dest_ptr={}\n", + static_cast(dest_start_ptr), dest_pixel_index, + static_cast(dest_start_ptr + dest_pixel_index)); + fmt::print("🔍 tile_data={}, nbytes_tile_index={}, src_ptr={}\n", + static_cast(tile_data), nbytes_tile_index, + static_cast(tile_data + nbytes_tile_index)); + fmt::print("🔍 nbytes_tile_pixel_size_x={} (copy size)\n", nbytes_tile_pixel_size_x); + fflush(stdout); + + // Validate pointers before memcpy + if (!dest_start_ptr) { + fmt::print("❌ ERROR: dest_start_ptr is NULL!\n"); + fflush(stdout); + throw std::runtime_error("dest_start_ptr is NULL"); + } + if (!tile_data) { + fmt::print("❌ ERROR: tile_data is NULL!\n"); + fflush(stdout); + throw std::runtime_error("tile_data is NULL"); + } + + fmt::print("🔍 Calling memcpy (device type={})...\n", static_cast(out_device.type())); + fflush(stdout); + + // Use appropriate copy method based on destination device + if (out_device.type() == cucim::io::DeviceType::kCUDA) + { + // Copy from CPU (tile_data) to GPU (dest_start_ptr) + cudaError_t cuda_status = cudaMemcpy( + dest_start_ptr + dest_pixel_index, + tile_data + nbytes_tile_index, + nbytes_tile_pixel_size_x, + cudaMemcpyHostToDevice + ); + if (cuda_status != cudaSuccess) + { + fmt::print(stderr, "❌ cudaMemcpy failed: {}\n", cudaGetErrorString(cuda_status)); + fflush(stderr); + throw std::runtime_error(fmt::format("cudaMemcpy failed: {}", cudaGetErrorString(cuda_status))); + } + } + else + { + // CPU to CPU copy + memcpy(dest_start_ptr + dest_pixel_index, tile_data + nbytes_tile_index, + nbytes_tile_pixel_size_x); + } + fmt::print("🔍 memcpy succeeded\n"); + fflush(stdout); } + fmt::print("🔍 memcpy loop completed\n"); + fflush(stdout); } } else { + fmt::print("🔍 tiledata_size <= 0, filling with background\n"); + fflush(stdout); if (out_device.type() == cucim::io::DeviceType::kCPU) { for (uint32_t ty = tile_pixel_offset_sy; ty <= tile_pixel_offset_ey; @@ -797,16 +989,27 @@ bool IFD::read_region_tiles(const TIFF* tiff, tile_pixel_offset_ey - tile_pixel_offset_sy + 1)); } } + fmt::print("🔍🔍🔍 decode_func lambda COMPLETE! Exiting...\n"); + fflush(stdout); }; - if (loader && *loader) + // TEMPORARY: Force single-threaded execution to isolate segfault + bool force_single_threaded = true; + + if (force_single_threaded || !loader || !(*loader)) { - loader->enqueue(std::move(decode_func), - cucim::loader::TileInfo{ location_index, index, tiledata_offset, tiledata_size }); + fmt::print("🔍 Executing decode_func directly (FORCED SINGLE-THREADED)\n"); + fmt::print("🔍 index={}, tiledata_offset={}, tiledata_size={}\n", index, tiledata_offset, tiledata_size); + fmt::print("🔍 About to call decode_func()...\n"); + fflush(stdout); + decode_func(); + fmt::print("🔍 decode_func completed successfully\n"); + fflush(stdout); } else { - decode_func(); + loader->enqueue(std::move(decode_func), + cucim::loader::TileInfo{ location_index, index, tiledata_offset, tiledata_size }); } dest_pixel_index_x += nbytes_tile_pixel_size_x; @@ -975,7 +1178,8 @@ bool IFD::read_region_tiles_boundary(const TIFF* tiff, uint32_t dest_pixel_index_orig = dest_pixel_index_x; auto decode_func = [=, &image_cache]() { - PROF_SCOPED_RANGE(PROF_EVENT_P(ifd_read_region_tiles_boundary_task, index_hash)); + // TEMPORARY: Disable profiling macro - it causes segfaults in lambdas + // PROF_SCOPED_RANGE(PROF_EVENT_P(ifd_read_region_tiles_boundary_task, index_hash)); uint32_t nbytes_tile_index = nbytes_tile_index_orig; uint32_t dest_pixel_index = dest_pixel_index_orig; @@ -1022,6 +1226,8 @@ bool IFD::read_region_tiles_boundary(const TIFF* tiff, switch (compression_method) { case COMPRESSION_JPEG: + case cuslide::jpeg2k::kAperioJpeg2kYCbCr: // 33003 + case cuslide::jpeg2k::kAperioJpeg2kRGB: // 33005 break; default: throw std::runtime_error("Unsupported compression method"); @@ -1099,7 +1305,8 @@ bool IFD::read_region_tiles_boundary(const TIFF* tiff, tile_data = tile_raster.get(); } { - PROF_SCOPED_RANGE(PROF_EVENT(ifd_decompression)); + // TEMPORARY: Disable profiling macro - it causes segfaults in lambdas + // PROF_SCOPED_RANGE(PROF_EVENT(ifd_decompression)); switch (compression_method) { case COMPRESSION_NONE: @@ -1107,9 +1314,13 @@ bool IFD::read_region_tiles_boundary(const TIFF* tiff, &tile_data, tile_raster_nbytes, out_device); break; case COMPRESSION_JPEG: + fmt::print("🔍 Calling decode_libjpeg\n"); + fflush(stdout); cuslide::jpeg::decode_libjpeg(tiff_file, nullptr, tiledata_offset, tiledata_size, jpegtable_data, jpegtable_count, &tile_data, out_device, jpeg_color_space); + fmt::print("🔍 decode_libjpeg completed\n"); + fflush(stdout); break; case COMPRESSION_ADOBE_DEFLATE: case COMPRESSION_DEFLATE: @@ -1117,14 +1328,23 @@ bool IFD::read_region_tiles_boundary(const TIFF* tiff, &tile_data, tile_raster_nbytes, out_device); break; case cuslide::jpeg2k::kAperioJpeg2kYCbCr: // 33003 + fmt::print("🔍 Calling decode_libopenjpeg (YCbCr)\n"); + fflush(stdout); cuslide::jpeg2k::decode_libopenjpeg(tiff_file, nullptr, tiledata_offset, tiledata_size, &tile_data, tile_raster_nbytes, out_device, cuslide::jpeg2k::ColorSpace::kSYCC); + fmt::print("🔍 decode_libopenjpeg (YCbCr) completed\n"); + fflush(stdout); break; case cuslide::jpeg2k::kAperioJpeg2kRGB: // 33005 + fmt::print("🔍 Calling decode_libopenjpeg (RGB), fd={}, offset={}, size={}\n", + tiff_file, tiledata_offset, tiledata_size); + fflush(stdout); cuslide::jpeg2k::decode_libopenjpeg(tiff_file, nullptr, tiledata_offset, tiledata_size, &tile_data, tile_raster_nbytes, out_device, cuslide::jpeg2k::ColorSpace::kRGB); + fmt::print("🔍 decode_libopenjpeg completed!\n"); + fflush(stdout); break; case COMPRESSION_LZW: cuslide::lzw::decode_lzw(tiff_file, nullptr, tiledata_offset, tiledata_size, @@ -1208,14 +1428,23 @@ bool IFD::read_region_tiles_boundary(const TIFF* tiff, } }; - if (loader && *loader) + // TEMPORARY: Force single-threaded execution to isolate segfault + bool force_single_threaded = true; + + if (force_single_threaded || !loader || !(*loader)) { - loader->enqueue(std::move(decode_func), - cucim::loader::TileInfo{ location_index, index, tiledata_offset, tiledata_size }); + fmt::print("🔍 Executing decode_func directly (FORCED SINGLE-THREADED)\n"); + fmt::print("🔍 index={}, tiledata_offset={}, tiledata_size={}\n", index, tiledata_offset, tiledata_size); + fmt::print("🔍 About to call decode_func()...\n"); + fflush(stdout); + decode_func(); + fmt::print("🔍 decode_func completed successfully\n"); + fflush(stdout); } else { - decode_func(); + loader->enqueue(std::move(decode_func), + cucim::loader::TileInfo{ location_index, index, tiledata_offset, tiledata_size }); } dest_pixel_index_x += nbytes_tile_pixel_size_x; diff --git a/cpp/plugins/cucim.kit.cuslide/src/cuslide/tiff/ifd.h b/cpp/plugins/cucim.kit.cuslide/src/cuslide/tiff/ifd.h index da8c3734d..2bba25751 100644 --- a/cpp/plugins/cucim.kit.cuslide/src/cuslide/tiff/ifd.h +++ b/cpp/plugins/cucim.kit.cuslide/src/cuslide/tiff/ifd.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2020, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. * SPDX-License-Identifier: Apache-2.0 */ @@ -15,6 +15,7 @@ #include #include #include + //#include namespace cuslide::tiff @@ -27,7 +28,7 @@ class EXPORT_VISIBLE IFD : public std::enable_shared_from_this { public: IFD(TIFF* tiff, uint16_t index, ifd_offset_t offset); - ~IFD() = default; + ~IFD(); static bool read_region_tiles(const TIFF* tiff, const IFD* ifd, diff --git a/cpp/plugins/cucim.kit.cuslide2/.clang-format b/cpp/plugins/cucim.kit.cuslide2/.clang-format new file mode 100644 index 000000000..bcadc9d0b --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/.clang-format @@ -0,0 +1,86 @@ +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: false +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortFunctionsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +AllowShortCaseLabelsOnASingleLine : false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: false +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: true +BinPackArguments: true +BinPackParameters: false +BreakBeforeBinaryOperators: false +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace : true +BreakBeforeTernaryOperators: false +BreakConstructorInitializersBeforeComma: false +BreakStringLiterals: false +ColumnLimit: 120 +CommentPragmas: '' +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DerivePointerBinding: false +FixNamespaceComments: true +IndentCaseLabels: false +IndentPPDirectives: AfterHash +IndentFunctionDeclarationAfterType: false +IndentWidth: 4 +SortIncludes: false +IncludeCategories: + - Regex: '[<"](.*\/)?Defines.h[>"]' + Priority: 1 +# - Regex: '' +# Priority: 3 + - Regex: '<[[:alnum:]_.]+>' + Priority: 5 + - Regex: '<[[:alnum:]_.\/]+>' + Priority: 4 + - Regex: '".*"' + Priority: 2 +IncludeBlocks: Regroup +Language: Cpp +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: None +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 0 +PenaltyBreakComment: 1 +PenaltyBreakFirstLessLess: 0 +PenaltyBreakString: 1 +PenaltyExcessCharacter: 10 +PenaltyReturnTypeOnItsOwnLine: 1000 +PointerAlignment: Left +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +Standard: Cpp11 +ReflowComments: true +TabWidth: 4 +UseTab: Never diff --git a/cpp/plugins/cucim.kit.cuslide2/.editorconfig b/cpp/plugins/cucim.kit.cuslide2/.editorconfig new file mode 100644 index 000000000..c69a96fa2 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/.editorconfig @@ -0,0 +1,7 @@ +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +max_line_length = 120 +insert_final_newline = true diff --git a/cpp/plugins/cucim.kit.cuslide2/.gitignore b/cpp/plugins/cucim.kit.cuslide2/.gitignore new file mode 100644 index 000000000..84a73e644 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/.gitignore @@ -0,0 +1,2 @@ +cmake-build* +install diff --git a/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt b/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt new file mode 100644 index 000000000..2a657cdaa --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt @@ -0,0 +1,328 @@ +# Apache License, Version 2.0 +# cmake-format: off +# SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION +# SPDX-License-Identifier: Apache-2.0 +# cmake-format: on + +cmake_minimum_required(VERSION 3.24.0 FATAL_ERROR) + +################################################################################ +# Prerequisite statements +################################################################################ + +# Set VERSION +unset(VERSION CACHE) +file(STRINGS ${CMAKE_CURRENT_LIST_DIR}/../../../VERSION VERSION) +# strip alpha version info +string(REGEX REPLACE "a.*$" "" VERSION ${VERSION}) + +# Append local cmake module path +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake/modules") + +project(cuslide2 VERSION ${VERSION} DESCRIPTION "cuslide2" LANGUAGES C CXX) +set(CUCIM_PLUGIN_NAME "cucim.kit.cuslide2") + +################################################################################ +# Include utilities +################################################################################ +include(SuperBuildUtils) +include(CuCIMUtils) + +################################################################################ +# Set cmake policy +################################################################################ +if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.19") + cmake_policy(SET CMP0110 NEW) # For add_test() to support arbitrary characters in test name +endif() + +################################################################################ +# Basic setup +################################################################################ + +# Set default build type +set(DEFAULT_BUILD_TYPE "Release") +if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "Setting build type to '${DEFAULT_BUILD_TYPE}' as none was specified.") + set(CMAKE_BUILD_TYPE "${DEFAULT_BUILD_TYPE}" CACHE STRING "Choose the type of build." FORCE) + # Set the possible values of build type for cmake-gui + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") +endif () + +# Set default output directories +if (NOT CMAKE_ARCHIVE_OUTPUT_DIRECTORY) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/lib") +endif() +if (NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/lib") +endif() +if (NOT CMAKE_RUNTIME_OUTPUT_DIRECTORY) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin") +endif() + +# Find CUDAToolkit as rmm depends on it +find_package(CUDAToolkit REQUIRED) +# For Threads::Threads +find_package(Threads REQUIRED) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED YES) + +# Include CUDA headers explicitly for VSCode intelli-sense +include_directories(AFTER SYSTEM ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES}) + +# Disable visibility to not expose unnecessary symbols +set(CMAKE_CXX_VISIBILITY_PRESET hidden) +set(CMAKE_VISIBILITY_INLINES_HIDDEN YES) + +# Set RPATH +if (NOT APPLE) + set(CMAKE_INSTALL_RPATH $ORIGIN) +endif() + +# Set Installation setup +if (NOT CMAKE_INSTALL_PREFIX) + set(CMAKE_INSTALL_PREFIX ${CMAKE_CURRENT_LIST_DIR}/install) # CACHE PATH "install here" FORCE) +endif () + +include(GNUInstallDirs) +# Force to set CMAKE_INSTALL_LIBDIR to lib as the library can be built with Cent OS ('lib64' is set) and +# /usr/local/lib64 or /usr/local/lib is not part of ld.so.conf* (`cat /etc/ld.so.conf.d/* | grep lib64`) +# https://gitlab.kitware.com/cmake/cmake/-/issues/20565 +set(CMAKE_INSTALL_LIBDIR lib) + +include(ExternalProject) + +################################################################################ +# Options +################################################################################ + +# Setup CXX11 ABI +# : Adds CXX11 ABI definition to the compiler command line for targets in the current directory, +# whether added before or after this command is invoked, and for the ones in sub-directories added after. +add_definitions(-D_GLIBCXX_USE_CXX11_ABI=0) # TODO: create two library, one with CXX11 ABI and one without it. + +################################################################################ +# Define dependencies - PURE nvImageCodec (minimal dependencies) +################################################################################ +superbuild_depend(fmt) +# REMOVED: CPU decoder dependencies (nvImageCodec handles all compression) +# superbuild_depend(libjpeg-turbo) +# superbuild_depend(libopenjpeg) +# superbuild_depend(libtiff) +# superbuild_depend(libdeflate) +superbuild_depend(openslide) +# Testing dependencies +superbuild_depend(catch2) +superbuild_depend(googletest) +superbuild_depend(googlebenchmark) +superbuild_depend(cli11) +superbuild_depend(pugixml) +superbuild_depend(json) +superbuild_depend(nvimgcodec) + +################################################################################ +# Find cucim package +################################################################################ +if (NOT CUCIM_SDK_PATH) + get_filename_component(CUCIM_SDK_PATH "${CMAKE_SOURCE_DIR}/../../.." ABSOLUTE) + message("CUCIM_SDK_PATH is not set. Using '${CUCIM_SDK_PATH}'") +else() + message("CUCIM_SDK_PATH is set to ${CUCIM_SDK_PATH}") +endif() + +find_package(cucim CONFIG REQUIRED + HINTS ${CUCIM_SDK_PATH}/install/${CMAKE_INSTALL_LIBDIR}/cmake/cucim + $ENV{PREFIX}/include/cmake/cucim # In case conda build is used + ) + + +################################################################################ +# Define compile options +################################################################################ + +if(NOT BUILD_SHARED_LIBS) + set(BUILD_SHARED_LIBS ON) +endif() + +################################################################################ +# Add library: cucim +################################################################################ + +# NOTE: Commented out for infrastructure-only PR. Will be enabled in follow-up PR +# with actual implementation once source files are added. +# TODO: Uncomment this section when src/ directory is added with implementation + +message(STATUS "=============================================================") +message(STATUS "cuslide2 PURE nvImageCodec - Dependencies:") +message(STATUS " ✓ fmt (logging)") +message(STATUS " ✓ nvImageCodec (GPU-accelerated JPEG/JPEG2000/deflate/LZW)") +message(STATUS " ✓ pugixml (XML metadata parsing)") +message(STATUS " ✓ json (JSON metadata)") +message(STATUS " ✓ openslide (for testing)") +message(STATUS " ✓ googletest, catch2, googlebenchmark, cli11 (testing)") +message(STATUS "") +message(STATUS "Build configured: Pure GPU-accelerated decoding with tests!") +message(STATUS "=============================================================") + +# Add library - PURE nvImageCodec implementation (no CPU fallbacks) +add_library(${CUCIM_PLUGIN_NAME} + # Main plugin interface + ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/cuslide.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/cuslide.h + # TIFF structure management (uses nvImageCodec for parsing) + ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/tiff/ifd.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/tiff/ifd.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/tiff/tiff.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/tiff/tiff.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/tiff/types.h + # nvImageCodec decoding and TIFF parsing (GPU-accelerated) + ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/nvimgcodec/nvimgcodec_decoder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/nvimgcodec/nvimgcodec_decoder.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.h) + +# No special source file properties needed for pure nvImageCodec implementation + +# Compile options +set_target_properties(${CUCIM_PLUGIN_NAME} + PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO + SOVERSION ${PROJECT_VERSION_MAJOR} + VERSION ${PROJECT_VERSION} +) +target_compile_features(${CUCIM_PLUGIN_NAME} PRIVATE cxx_std_17) +# Use generator expression to avoid `nvcc fatal : Value '-std=c++17' is not defined for option 'Werror'` +target_compile_options(${CUCIM_PLUGIN_NAME} PRIVATE $<$:-Werror -Wall -Wextra>) + +# Link libraries - Core dependencies (always required) +target_link_libraries(${CUCIM_PLUGIN_NAME} + PRIVATE + deps::fmt + cucim::cucim + deps::pugixml + deps::json + ) + +# Conditionally link nvImageCodec if available +if(TARGET deps::nvimgcodec) + get_target_property(NVIMGCODEC_LOCATION deps::nvimgcodec IMPORTED_LOCATION) + get_target_property(NVIMGCODEC_INTERFACE_LINK deps::nvimgcodec INTERFACE_LINK_LIBRARIES) + + # Check if it's a real target (has location or interface links) vs dummy target + if(NVIMGCODEC_LOCATION OR NVIMGCODEC_INTERFACE_LINK OR TARGET nvimgcodec::nvimgcodec) + target_link_libraries(${CUCIM_PLUGIN_NAME} PRIVATE deps::nvimgcodec) + target_compile_definitions(${CUCIM_PLUGIN_NAME} PRIVATE CUCIM_HAS_NVIMGCODEC) + message(STATUS "✓ nvImageCodec enabled - GPU-accelerated JPEG/JPEG2000 decoding available") + else() + message(STATUS "⚠ nvImageCodec target exists but is dummy - GPU acceleration disabled") + endif() +else() + message(STATUS "⚠ nvImageCodec target not found - GPU acceleration disabled") +endif() +if (TARGET CUDA::nvjpeg_static) + target_link_libraries(${CUCIM_PLUGIN_NAME} + PRIVATE + # Add nvjpeg before cudart so that nvjpeg.h in static library takes precedence. + CUDA::nvjpeg_static + # Add CUDA::culibos to link necessary methods for 'deps::nvjpeg_static' + CUDA::culibos + CUDA::cudart + ) +else() + target_link_libraries(${CUCIM_PLUGIN_NAME} + PRIVATE + CUDA::nvjpeg + CUDA::cudart + ) +endif() + +target_include_directories(${CUCIM_PLUGIN_NAME} + PUBLIC + $ + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../cucim.kit.cuslide/src + ${CMAKE_CURRENT_SOURCE_DIR}/src # Include cuslide2 src for nvimgcodec headers + ) + +# Do not generate SONAME as this would be used as plugin +# Need to use IMPORTED_NO_SONAME when using this .so file. +set_target_properties(${CUCIM_PLUGIN_NAME} PROPERTIES NO_SONAME 1) +# Prevent relative path problem of .so with no DT_SONAME. +# : https://stackoverflow.com/questions/27261288/cmake-linking-shared-c-object-from-externalproject-produces-binaries-with-rel +target_link_options(${CUCIM_PLUGIN_NAME} PRIVATE "LINKER:-soname=${CUCIM_PLUGIN_NAME}@${PROJECT_VERSION}.so") + +# Do not add 'lib' prefix for the library +set_target_properties(${CUCIM_PLUGIN_NAME} PROPERTIES PREFIX "") +# Postfix version +set_target_properties(${CUCIM_PLUGIN_NAME} PROPERTIES OUTPUT_NAME "${CUCIM_PLUGIN_NAME}@${PROJECT_VERSION}") + +#set_target_properties(${CUCIM_PLUGIN_NAME} PROPERTIES LINK_FLAGS +# "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/cuslide.map") + +################################################################################ +# Add tests +################################################################################ +add_subdirectory(tests) +add_subdirectory(benchmarks) + +################################################################################ +# Install +################################################################################ +# NOTE: Disabled for infrastructure-only PR +# TODO: Uncomment when library target exists +set(INSTALL_TARGETS + ${CUCIM_PLUGIN_NAME} + # cuslide_tests # Disabled for infrastructure-only PR + # cuslide_benchmarks # Disabled for infrastructure-only PR + ) + +install(TARGETS ${INSTALL_TARGETS} + EXPORT ${CUCIM_PLUGIN_NAME}-targets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT ${CUCIM_PLUGIN_NAME}_Runtime + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + COMPONENT ${CUCIM_PLUGIN_NAME}_Runtime + NAMELINK_COMPONENT ${CUCIM_PLUGIN_NAME}_Development + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + COMPONENT ${CUCIM_PLUGIN_NAME}_Development + ) + +# Currently cuslide plugin doesn't have include path so comment out +# install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(EXPORT ${CUCIM_PLUGIN_NAME}-targets + FILE + ${CUCIM_PLUGIN_NAME}-targets.cmake + NAMESPACE + ${PROJECT_NAME}:: + DESTINATION + ${CMAKE_INSTALL_LIBDIR}/cmake/${CUCIM_PLUGIN_NAME}) + +# Write package configs +include(CMakePackageConfigHelpers) +configure_package_config_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/${CUCIM_PLUGIN_NAME}-config.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/cmake/${CUCIM_PLUGIN_NAME}-config.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${CUCIM_PLUGIN_NAME} +) +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/cmake/${CUCIM_PLUGIN_NAME}-config-version.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY AnyNewerVersion +) +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/cmake/${CUCIM_PLUGIN_NAME}-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/cmake/${CUCIM_PLUGIN_NAME}-config-version.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${CUCIM_PLUGIN_NAME} +) + + +set(CMAKE_EXPORT_PACKAGE_REGISTRY ON) +export(PACKAGE ${CUCIM_PLUGIN_NAME}) + +# REMOVED: endif() - no longer needed since we removed the if(TRUE) wrapper + +unset(BUILD_SHARED_LIBS CACHE) diff --git a/cpp/plugins/cucim.kit.cuslide2/benchmarks/CMakeLists.txt b/cpp/plugins/cucim.kit.cuslide2/benchmarks/CMakeLists.txt new file mode 100644 index 000000000..32c13ab73 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/benchmarks/CMakeLists.txt @@ -0,0 +1,37 @@ +# +# cmake-format: off +# SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. +# SPDX-License-Identifier: Apache-2.0 +# cmake-format: on +# + +################################################################################ +# Add executable: cuslide_benchmarks +################################################################################ +add_executable(cuslide_benchmarks main.cpp config.h) +#set_source_files_properties(main.cpp PROPERTIES LANGUAGE CUDA) # failed with CLI11 library + +set_target_properties(cuslide_benchmarks + PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO +) +target_compile_features(cuslide_benchmarks PRIVATE ${CUCIM_REQUIRED_FEATURES}) +# Use generator expression to avoid `nvcc fatal : Value '-std=c++17' is not defined for option 'Werror'` +target_compile_options(cuslide_benchmarks PRIVATE $<$:-Werror -Wall -Wextra>) +target_compile_definitions(cuslide_benchmarks + PUBLIC + CUSLIDE_VERSION=${PROJECT_VERSION} + CUSLIDE_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} + CUSLIDE_VERSION_MINOR=${PROJECT_VERSION_MINOR} + CUSLIDE_VERSION_PATCH=${PROJECT_VERSION_PATCH} + CUSLIDE_VERSION_BUILD=${PROJECT_VERSION_BUILD} +) +target_link_libraries(cuslide_benchmarks + PRIVATE + cucim::cucim + deps::googlebenchmark + deps::openslide + deps::cli11 + ) diff --git a/cpp/plugins/cucim.kit.cuslide2/benchmarks/config.h b/cpp/plugins/cucim.kit.cuslide2/benchmarks/config.h new file mode 100644 index 000000000..11071ce78 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/benchmarks/config.h @@ -0,0 +1,69 @@ +/* + * Apache License, Version 2.0 + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef CUSLIDE_CONFIG_H +#define CUSLIDE_CONFIG_H + +#include + +struct AppConfig +{ + std::string test_folder; + std::string test_file; + bool discard_cache = false; + int random_seed = 0; + bool random_start_location = false; + + int64_t image_width = 0; + int64_t image_height = 0; + + // Pseudo configurations for google benchmark + bool benchmark_list_tests = false; + std::string benchmark_filter; // + int benchmark_min_time = 0; // + int benchmark_repetitions = 0; // + bool benchmark_report_aggregates_only = false; + bool benchmark_display_aggregates_only = false; + std::string benchmark_format; // + std::string benchmark_out; // + std::string benchmark_out_format; // + std::string benchmark_color; // {auto|true|false} + std::string benchmark_counters_tabular; + std::string v; // + + std::string get_input_path(const std::string default_value = "generated/tiff_stripe_4096x4096_256.tif") const + { + // If `test_file` is absolute path + if (!test_folder.empty() && test_file.substr(0, 1) == "/") + { + return test_file; + } + else + { + std::string test_data_folder = test_folder; + if (test_data_folder.empty()) + { + if (const char* env_p = std::getenv("CUCIM_TESTDATA_FOLDER")) + { + test_data_folder = env_p; + } + else + { + test_data_folder = "test_data"; + } + } + if (test_file.empty()) + { + return test_data_folder + "/" + default_value; + } + else + { + return test_data_folder + "/" + test_file; + } + } + } +}; + +#endif // CUSLIDE_CONFIG_H diff --git a/cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp b/cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp new file mode 100644 index 000000000..01605dca0 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2020-2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "config.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "cucim/core/framework.h" +#include "cucim/io/format/image_format.h" +#include "cucim/memory/memory_manager.h" + +#define XSTR(x) STR(x) +#define STR(x) #x + +//#include + +CUCIM_FRAMEWORK_GLOBALS("cuslide.app") + +static AppConfig g_config; + + +static void test_basic(benchmark::State& state) +{ + std::string input_path = g_config.get_input_path(); + + int arg = -1; + for (auto state_item : state) + { + state.PauseTiming(); + { + // Use a different start random seed for the different argument + if (arg != state.range()) + { + arg = state.range(); + srand(g_config.random_seed + arg); + } + + if (g_config.discard_cache) + { + int fd = open(input_path.c_str(), O_RDONLY); + fdatasync(fd); + posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED); + close(fd); + } + } + state.ResumeTiming(); + + // auto start = std::chrono::high_resolution_clock::now(); + cucim::Framework* framework = cucim::acquire_framework("cuslide.app"); + if (!framework) + { + fmt::print("framework is not available!\n"); + return; + } + + cucim::io::format::IImageFormat* image_format = + framework->acquire_interface_from_library( + "cucim.kit.cuslide@" XSTR(CUSLIDE_VERSION) ".so"); + // std::cout << image_format->formats[0].get_format_name() << std::endl; + if (image_format == nullptr) + { + fmt::print("plugin library is not available!\n"); + return; + } + + std::string input_path = g_config.get_input_path(); + std::shared_ptr* file_handle_shared = reinterpret_cast*>( + image_format->formats[0].image_parser.open(input_path.c_str())); + + std::shared_ptr file_handle = *file_handle_shared; + delete file_handle_shared; + + // Set deleter to close the file handle + file_handle->set_deleter(image_format->formats[0].image_parser.close); + + cucim::io::format::ImageMetadata metadata{}; + image_format->formats[0].image_parser.parse(file_handle.get(), &metadata.desc()); + + cucim::io::format::ImageReaderRegionRequestDesc request{}; + int64_t request_location[2] = { 0, 0 }; + if (g_config.random_start_location) + { + request_location[0] = rand() % (g_config.image_width - state.range(0)); + request_location[1] = rand() % (g_config.image_height - state.range(0)); + } + + request.location = request_location; + request.level = 0; + int64_t request_size[2] = { state.range(0), state.range(0) }; + request.size = request_size; + request.device = const_cast("cpu"); + + cucim::io::format::ImageDataDesc image_data; + + image_format->formats[0].image_reader.read( + file_handle.get(), &metadata.desc(), &request, &image_data, nullptr /*out_metadata*/); + cucim_free(image_data.container.data); + + // auto end = std::chrono::high_resolution_clock::now(); + // auto elapsed_seconds = std::chrono::duration_cast>(end - start); + // state.SetIterationTime(elapsed_seconds.count()); + } +} + +static void test_openslide(benchmark::State& state) +{ + std::string input_path = g_config.get_input_path(); + + int arg = -1; + for (auto _ : state) + { + state.PauseTiming(); + { + // Use a different start random seed for the different argument + if (arg != state.range()) + { + arg = state.range(); + srand(g_config.random_seed + arg); + } + + if (g_config.discard_cache) + { + int fd = open(input_path.c_str(), O_RDONLY); + fdatasync(fd); + posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED); + close(fd); + } + } + state.ResumeTiming(); + + openslide_t* slide = openslide_open(input_path.c_str()); + uint32_t* buf = static_cast(cucim_malloc(state.range(0) * state.range(0) * 4)); + int64_t request_location[2] = { 0, 0 }; + if (g_config.random_start_location) + { + request_location[0] = rand() % (g_config.image_width - state.range(0)); + request_location[1] = rand() % (g_config.image_height - state.range(0)); + } + openslide_read_region(slide, buf, request_location[0], request_location[1], 0, state.range(0), state.range(0)); + cucim_free(buf); + openslide_close(slide); + } +} + +BENCHMARK(test_basic)->Unit(benchmark::kMicrosecond)->RangeMultiplier(2)->Range(1, 4096); //->UseManualTime(); +BENCHMARK(test_openslide)->Unit(benchmark::kMicrosecond)->RangeMultiplier(2)->Range(1, 4096); + +static bool remove_help_option(int* argc, char** argv) +{ + for (int i = 1; argc && i < *argc; ++i) + { + if (strncmp(argv[i], "-h", 3) == 0 || strncmp(argv[i], "--help", 7) == 0) + { + for (int j = i + 1; argc && j < *argc; ++j) + { + argv[j - 1] = argv[j]; + } + --(*argc); + argv[*argc] = nullptr; + return true; + } + } + return false; +} + +static bool setup_configuration() +{ + std::string input_path = g_config.get_input_path(); + openslide_t* slide = openslide_open(input_path.c_str()); + if (slide == nullptr) + { + fmt::print("[Error] Cannot load {}!\n", input_path); + return false; + } + + int64_t w, h; + openslide_get_level0_dimensions(slide, &w, &h); + + g_config.image_width = w; + g_config.image_height = h; + + openslide_close(slide); + + return true; +} + +// BENCHMARK_MAIN(); +int main(int argc, char** argv) +{ + // Skip processing help option + bool has_help_option = remove_help_option(&argc, argv); + + ::benchmark::Initialize(&argc, argv); + // if (::benchmark::ReportUnrecognizedArguments(argc, argv)) + // return 1; + CLI::App app{ "benchmark: cuSlide" }; + app.add_option("--test_folder", g_config.test_folder, "An input test folder path"); + app.add_option("--test_file", g_config.test_file, "An input test image file path"); + app.add_option("--discard_cache", g_config.discard_cache, "Discard page cache for the input file for each iteration"); + app.add_option("--random_seed", g_config.random_seed, "A random seed number"); + app.add_option( + "--random_start_location", g_config.random_start_location, "Randomize start location of read_region()"); + + // Pseudo benchmark options + app.add_option("--benchmark_list_tests", g_config.benchmark_list_tests, "{true|false}"); + app.add_option("--benchmark_filter", g_config.benchmark_filter, ""); + app.add_option("--benchmark_min_time", g_config.benchmark_min_time, ""); + app.add_option("--benchmark_repetitions", g_config.benchmark_repetitions, ""); + app.add_option("--benchmark_report_aggregates_only", g_config.benchmark_report_aggregates_only, "{true|false}"); + app.add_option("--benchmark_display_aggregates_only", g_config.benchmark_display_aggregates_only, "{true|false}"); + app.add_option("--benchmark_format", g_config.benchmark_format, ""); + app.add_option("--benchmark_out", g_config.benchmark_out, ""); + app.add_option("--benchmark_out_format", g_config.benchmark_out_format, ""); + app.add_option("--benchmark_color", g_config.benchmark_color, "{auto|true|false}"); + app.add_option("--benchmark_counters_tabular", g_config.benchmark_counters_tabular, "{true|false}"); + app.add_option("--v", g_config.v, ""); + + // Append help option if exists + if (has_help_option) + { + argv[argc] = const_cast("--help"); + ++argc; + // https://github.com/matepek/vscode-catch2-test-adapter detects google benchmark binaries by the following + // text: + printf("benchmark [--benchmark_list_tests={true|false}]\n"); + } + CLI11_PARSE(app, argc, argv); + + if (!setup_configuration()) + { + return 1; + } + ::benchmark::RunSpecifiedBenchmarks(); +} diff --git a/cpp/plugins/cucim.kit.cuslide2/cmake/cucim.kit.cuslide-config.cmake.in b/cpp/plugins/cucim.kit.cuslide2/cmake/cucim.kit.cuslide-config.cmake.in new file mode 100644 index 000000000..2b9bae2fc --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/cmake/cucim.kit.cuslide-config.cmake.in @@ -0,0 +1,15 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 2020, NVIDIA CORPORATION. +# SPDX-License-Identifier: Apache-2.0 +# + +@PACKAGE_INIT@ + +# Find dependent libraries +# ... +include(CMakeFindDependencyMacro) +#find_dependency(Boost x.x.x REQUIRED) + +if(NOT TARGET cuslide::cuslide) + include(${CMAKE_CURRENT_LIST_DIR}/cucim.kit.cuslide-targets.cmake) +endif() diff --git a/cpp/plugins/cucim.kit.cuslide2/cmake/cucim.kit.cuslide2-config.cmake.in b/cpp/plugins/cucim.kit.cuslide2/cmake/cucim.kit.cuslide2-config.cmake.in new file mode 100644 index 000000000..4fbcf1e10 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/cmake/cucim.kit.cuslide2-config.cmake.in @@ -0,0 +1,13 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. +# SPDX-License-Identifier: Apache-2.0 +# + +@PACKAGE_INIT@ + +# Find dependent libraries +# ... +include(CMakeFindDependencyMacro) +#find_dependency(Boost x.x.x REQUIRED) + +include(${CMAKE_CURRENT_LIST_DIR}/cucim.kit.cuslide2-targets.cmake) diff --git a/cpp/plugins/cucim.kit.cuslide2/cmake/deps/nvimgcodec.cmake b/cpp/plugins/cucim.kit.cuslide2/cmake/deps/nvimgcodec.cmake new file mode 100644 index 000000000..f20da219b --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/cmake/deps/nvimgcodec.cmake @@ -0,0 +1,412 @@ +# +# cmake-format: off +# SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. +# SPDX-License-Identifier: Apache-2.0 +# cmake-format: on +# + +# nvImageCodec v0.7.0 internal release configuration +# =================================================== +# +# This cmake module configures nvImageCodec for cuslide2. It supports: +# 1. Pre-installed packages via NVIMGCODEC_ROOT +# 2. Download from URL via NVIMGCODEC_URL +# 3. Auto-detection in conda/pip/system paths +# +# For internal release v0.7.0, use one of these options: +# +# Option A - Specify local installation path (CUDA 12): +# cmake -DNVIMGCODEC_ROOT=/home/cdinea/Downloads/cucim_pr3/nvimgcodec/12 .. +# +# Option B - Specify local installation path (CUDA 13): +# cmake -DNVIMGCODEC_ROOT=/home/cdinea/Downloads/cucim_pr3/nvimgcodec/13 .. +# +# Option C - Use auto-detection with NVIMGCODEC_CUDA_VERSION: +# cmake -DNVIMGCODEC_DIR=/home/cdinea/Downloads/cucim_pr3/nvimgcodec -DNVIMGCODEC_CUDA_VERSION=12 .. +# +# Available packages for v0.7.0 Build 11: +# C Packages (CUDA 12.9 and 13.0): +# - linux-x86_64, linux-sbsa, linux-aarch64 (12.9 only), windows-x86_64 +# Python Packages: +# - CUDA 12 (12.9): linux-aarch64, linux-sbsa, linux-x86_64, windows-x86_64 +# - CUDA 13 (13.0): linux-sbsa, linux-x86_64, windows-x86_64 + +set(NVIMGCODEC_VERSION "0.7.0" CACHE STRING "nvImageCodec version to use") +set(NVIMGCODEC_ROOT "" CACHE PATH "Path to nvImageCodec installation directory (e.g., /path/to/nvimgcodec/12)") +set(NVIMGCODEC_DIR "" CACHE PATH "Path to nvImageCodec parent directory containing CUDA version subdirs") +set(NVIMGCODEC_CUDA_VERSION "" CACHE STRING "CUDA version to use (12 or 13) when NVIMGCODEC_DIR is set") +set(NVIMGCODEC_URL "" CACHE STRING "URL to download nvImageCodec tarball from internal release") + +# Default nvimgcodec location for this machine +set(NVIMGCODEC_DEFAULT_DIR "/home/cdinea/Downloads/cucim_pr3/nvimgcodec") + +if (NOT TARGET deps::nvimgcodec) + set(NVIMGCODEC_LIB_PATH "") + set(NVIMGCODEC_INCLUDE_PATH "") + set(NVIMGCODEC_EXTENSIONS_PATH "") + set(NVIMGCODEC_FOUND FALSE) + + message(STATUS "") + message(STATUS "=== nvImageCodec v${NVIMGCODEC_VERSION} Configuration ===") + + # ========================================================================= + # Determine the actual root path to use + # ========================================================================= + set(NVIMGCODEC_ACTUAL_ROOT "") + + # Priority 1: Direct NVIMGCODEC_ROOT specification + if(NVIMGCODEC_ROOT AND EXISTS "${NVIMGCODEC_ROOT}") + set(NVIMGCODEC_ACTUAL_ROOT "${NVIMGCODEC_ROOT}") + message(STATUS "Using NVIMGCODEC_ROOT: ${NVIMGCODEC_ACTUAL_ROOT}") + + # Priority 2: NVIMGCODEC_DIR + CUDA version + elseif(NVIMGCODEC_DIR AND EXISTS "${NVIMGCODEC_DIR}") + # Auto-detect CUDA version if not specified + if(NOT NVIMGCODEC_CUDA_VERSION) + # Try to detect from CUDAToolkit + if(CUDAToolkit_VERSION_MAJOR) + set(NVIMGCODEC_CUDA_VERSION "${CUDAToolkit_VERSION_MAJOR}") + message(STATUS "Auto-detected CUDA version: ${NVIMGCODEC_CUDA_VERSION}") + else() + set(NVIMGCODEC_CUDA_VERSION "12") + message(STATUS "Defaulting to CUDA version: ${NVIMGCODEC_CUDA_VERSION}") + endif() + endif() + + if(EXISTS "${NVIMGCODEC_DIR}/${NVIMGCODEC_CUDA_VERSION}") + set(NVIMGCODEC_ACTUAL_ROOT "${NVIMGCODEC_DIR}/${NVIMGCODEC_CUDA_VERSION}") + message(STATUS "Using NVIMGCODEC_DIR with CUDA ${NVIMGCODEC_CUDA_VERSION}: ${NVIMGCODEC_ACTUAL_ROOT}") + endif() + + # Priority 3: Check default location + elseif(EXISTS "${NVIMGCODEC_DEFAULT_DIR}") + # Auto-detect CUDA version + if(NOT NVIMGCODEC_CUDA_VERSION) + if(CUDAToolkit_VERSION_MAJOR) + set(NVIMGCODEC_CUDA_VERSION "${CUDAToolkit_VERSION_MAJOR}") + else() + set(NVIMGCODEC_CUDA_VERSION "12") + endif() + endif() + + if(EXISTS "${NVIMGCODEC_DEFAULT_DIR}/${NVIMGCODEC_CUDA_VERSION}") + set(NVIMGCODEC_ACTUAL_ROOT "${NVIMGCODEC_DEFAULT_DIR}/${NVIMGCODEC_CUDA_VERSION}") + message(STATUS "Using default location with CUDA ${NVIMGCODEC_CUDA_VERSION}: ${NVIMGCODEC_ACTUAL_ROOT}") + endif() + endif() + + # ========================================================================= + # Method 1: Use CMake config files from the package (preferred) + # ========================================================================= + if(NVIMGCODEC_ACTUAL_ROOT AND EXISTS "${NVIMGCODEC_ACTUAL_ROOT}/cmake/nvimgcodec/nvimgcodecConfig.cmake") + message(STATUS "Found nvImageCodec CMake config at: ${NVIMGCODEC_ACTUAL_ROOT}/cmake/nvimgcodec") + + # Add to CMAKE_PREFIX_PATH for find_package + list(APPEND CMAKE_PREFIX_PATH "${NVIMGCODEC_ACTUAL_ROOT}/cmake") + + find_package(nvimgcodec CONFIG QUIET + PATHS "${NVIMGCODEC_ACTUAL_ROOT}/cmake" + NO_DEFAULT_PATH + ) + + if(nvimgcodec_FOUND) + # The nvimgcodec CMake config sets these variables: + # nvimgcodec_INCLUDE_DIR, nvimgcodec_LIB_DIR, nvimgcodec_EXTENSIONS_DIR + # But it doesn't set INTERFACE_INCLUDE_DIRECTORIES on the target, so we must do it + + # Get library path from target + get_target_property(_nvimgcodec_loc nvimgcodec::nvimgcodec IMPORTED_LOCATION_RELEASE) + if(NOT _nvimgcodec_loc) + get_target_property(_nvimgcodec_loc nvimgcodec::nvimgcodec IMPORTED_LOCATION) + endif() + + # Use nvimgcodec_INCLUDE_DIR from the config (not from target property) + set(NVIMGCODEC_LIB_PATH "${_nvimgcodec_loc}") + set(NVIMGCODEC_INCLUDE_PATH "${nvimgcodec_INCLUDE_DIR}") + set(NVIMGCODEC_EXTENSIONS_PATH "${nvimgcodec_EXTENSIONS_DIR}") + + # Add include directory to the nvimgcodec target (it's missing from the CMake config) + set_target_properties(nvimgcodec::nvimgcodec PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${nvimgcodec_INCLUDE_DIR}" + ) + + # Create our wrapper target + add_library(deps::nvimgcodec INTERFACE IMPORTED GLOBAL) + target_link_libraries(deps::nvimgcodec INTERFACE nvimgcodec::nvimgcodec) + + set(NVIMGCODEC_FOUND TRUE) + + message(STATUS "✓ nvImageCodec v${NVIMGCODEC_VERSION} found via CMake config") + message(STATUS " Include dir: ${nvimgcodec_INCLUDE_DIR}") + endif() + endif() + + # ========================================================================= + # Method 2: Manual detection in specified root + # ========================================================================= + if(NOT NVIMGCODEC_FOUND AND NVIMGCODEC_ACTUAL_ROOT) + message(STATUS "Searching for nvImageCodec in: ${NVIMGCODEC_ACTUAL_ROOT}") + + # Check for header file + if(EXISTS "${NVIMGCODEC_ACTUAL_ROOT}/include/nvimgcodec.h") + set(NVIMGCODEC_INCLUDE_PATH "${NVIMGCODEC_ACTUAL_ROOT}/include") + endif() + + # Check for library file (lib64 first, then lib) + foreach(LIB_NAME "libnvimgcodec.so.0" "libnvimgcodec.so" "libnvimgcodec.so.${NVIMGCODEC_VERSION}") + foreach(LIB_DIR "lib64" "lib" "") + if(LIB_DIR) + set(LIB_CHECK_PATH "${NVIMGCODEC_ACTUAL_ROOT}/${LIB_DIR}/${LIB_NAME}") + else() + set(LIB_CHECK_PATH "${NVIMGCODEC_ACTUAL_ROOT}/${LIB_NAME}") + endif() + if(EXISTS "${LIB_CHECK_PATH}") + set(NVIMGCODEC_LIB_PATH "${LIB_CHECK_PATH}") + break() + endif() + endforeach() + if(NVIMGCODEC_LIB_PATH) + break() + endif() + endforeach() + + # Check for extensions directory + if(EXISTS "${NVIMGCODEC_ACTUAL_ROOT}/extensions") + set(NVIMGCODEC_EXTENSIONS_PATH "${NVIMGCODEC_ACTUAL_ROOT}/extensions") + endif() + + if(NVIMGCODEC_INCLUDE_PATH AND NVIMGCODEC_LIB_PATH) + set(NVIMGCODEC_FOUND TRUE) + message(STATUS "✓ nvImageCodec v${NVIMGCODEC_VERSION} found via manual detection") + endif() + endif() + + # ========================================================================= + # Method 3: Download from URL (internal release) + # ========================================================================= + if(NOT NVIMGCODEC_FOUND AND NVIMGCODEC_URL) + message(STATUS "Downloading nvImageCodec from: ${NVIMGCODEC_URL}") + + include(FetchContent) + + FetchContent_Declare( + deps-nvimgcodec + URL ${NVIMGCODEC_URL} + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + ) + + FetchContent_GetProperties(deps-nvimgcodec) + if(NOT deps-nvimgcodec_POPULATED) + message(STATUS "Fetching nvImageCodec v${NVIMGCODEC_VERSION}...") + FetchContent_Populate(deps-nvimgcodec) + message(STATUS "Fetching nvImageCodec v${NVIMGCODEC_VERSION} - done") + endif() + + set(NVIMGCODEC_DOWNLOAD_DIR "${deps-nvimgcodec_SOURCE_DIR}") + + # Search for headers and library in downloaded content + if(EXISTS "${NVIMGCODEC_DOWNLOAD_DIR}/include/nvimgcodec.h") + set(NVIMGCODEC_INCLUDE_PATH "${NVIMGCODEC_DOWNLOAD_DIR}/include") + endif() + + foreach(LIB_NAME "libnvimgcodec.so.0" "libnvimgcodec.so") + foreach(LIB_DIR "lib64" "lib" "") + if(LIB_DIR) + set(LIB_CHECK_PATH "${NVIMGCODEC_DOWNLOAD_DIR}/${LIB_DIR}/${LIB_NAME}") + else() + set(LIB_CHECK_PATH "${NVIMGCODEC_DOWNLOAD_DIR}/${LIB_NAME}") + endif() + if(EXISTS "${LIB_CHECK_PATH}") + set(NVIMGCODEC_LIB_PATH "${LIB_CHECK_PATH}") + break() + endif() + endforeach() + if(NVIMGCODEC_LIB_PATH) + break() + endif() + endforeach() + + if(EXISTS "${NVIMGCODEC_DOWNLOAD_DIR}/extensions") + set(NVIMGCODEC_EXTENSIONS_PATH "${NVIMGCODEC_DOWNLOAD_DIR}/extensions") + endif() + + if(NVIMGCODEC_INCLUDE_PATH AND NVIMGCODEC_LIB_PATH) + set(NVIMGCODEC_FOUND TRUE) + message(STATUS "✓ nvImageCodec v${NVIMGCODEC_VERSION} downloaded and extracted") + else() + message(WARNING "Downloaded nvImageCodec but couldn't find library or headers") + message(WARNING " Download dir: ${NVIMGCODEC_DOWNLOAD_DIR}") + endif() + endif() + + # ========================================================================= + # Method 4: Try find_package (works in conda and system installations) + # ========================================================================= + if(NOT NVIMGCODEC_FOUND) + find_package(nvimgcodec ${NVIMGCODEC_VERSION} QUIET CONFIG) + + if(nvimgcodec_FOUND) + add_library(deps::nvimgcodec INTERFACE IMPORTED GLOBAL) + target_link_libraries(deps::nvimgcodec INTERFACE nvimgcodec::nvimgcodec) + message(STATUS "✓ nvImageCodec found via find_package (version: ${nvimgcodec_VERSION})") + set(NVIMGCODEC_FOUND TRUE) + endif() + endif() + + # ========================================================================= + # Method 5: Manual detection in conda/pip/system paths + # ========================================================================= + if(NOT NVIMGCODEC_FOUND) + # Try conda environment + if(DEFINED ENV{CONDA_PREFIX}) + # Try native conda package (libnvimgcodec-dev) + set(CONDA_NATIVE_ROOT "$ENV{CONDA_PREFIX}") + if(EXISTS "${CONDA_NATIVE_ROOT}/include/nvimgcodec.h") + set(NVIMGCODEC_INCLUDE_PATH "${CONDA_NATIVE_ROOT}/include") + if(EXISTS "${CONDA_NATIVE_ROOT}/lib/libnvimgcodec.so.0") + set(NVIMGCODEC_LIB_PATH "${CONDA_NATIVE_ROOT}/lib/libnvimgcodec.so.0") + elseif(EXISTS "${CONDA_NATIVE_ROOT}/lib/libnvimgcodec.so") + set(NVIMGCODEC_LIB_PATH "${CONDA_NATIVE_ROOT}/lib/libnvimgcodec.so") + endif() + endif() + + # Fallback: try Python site-packages in conda environment + if(NOT NVIMGCODEC_LIB_PATH) + foreach(PY_VER "3.13" "3.12" "3.11" "3.10" "3.9") + set(CONDA_PYTHON_ROOT "$ENV{CONDA_PREFIX}/lib/python${PY_VER}/site-packages/nvidia/nvimgcodec") + if(EXISTS "${CONDA_PYTHON_ROOT}/include/nvimgcodec.h") + set(NVIMGCODEC_INCLUDE_PATH "${CONDA_PYTHON_ROOT}/include") + if(EXISTS "${CONDA_PYTHON_ROOT}/lib/libnvimgcodec.so.0") + set(NVIMGCODEC_LIB_PATH "${CONDA_PYTHON_ROOT}/lib/libnvimgcodec.so.0") + elseif(EXISTS "${CONDA_PYTHON_ROOT}/lib/libnvimgcodec.so") + set(NVIMGCODEC_LIB_PATH "${CONDA_PYTHON_ROOT}/lib/libnvimgcodec.so") + elseif(EXISTS "${CONDA_PYTHON_ROOT}/libnvimgcodec.so.0") + set(NVIMGCODEC_LIB_PATH "${CONDA_PYTHON_ROOT}/libnvimgcodec.so.0") + elseif(EXISTS "${CONDA_PYTHON_ROOT}/libnvimgcodec.so") + set(NVIMGCODEC_LIB_PATH "${CONDA_PYTHON_ROOT}/libnvimgcodec.so") + endif() + if(NVIMGCODEC_LIB_PATH) + break() + endif() + endif() + endforeach() + endif() + endif() + + # Try Python site-packages (outside conda or as additional fallback) + if(NOT NVIMGCODEC_LIB_PATH) + find_package(Python3 COMPONENTS Interpreter QUIET) + if(Python3_FOUND) + execute_process( + COMMAND ${Python3_EXECUTABLE} -c "import site; print(site.getusersitepackages())" + OUTPUT_VARIABLE PYTHON_USER_SITE_PACKAGES + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + execute_process( + COMMAND ${Python3_EXECUTABLE} -c "import site; print(site.getsitepackages()[0])" + OUTPUT_VARIABLE PYTHON_SITE_PACKAGES + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + + foreach(SITE_PKG_DIR ${PYTHON_USER_SITE_PACKAGES} ${PYTHON_SITE_PACKAGES}) + if(SITE_PKG_DIR) + set(NVIMGCODEC_PYTHON_ROOT "${SITE_PKG_DIR}/nvidia/nvimgcodec") + if(EXISTS "${NVIMGCODEC_PYTHON_ROOT}/include/nvimgcodec.h") + set(NVIMGCODEC_INCLUDE_PATH "${NVIMGCODEC_PYTHON_ROOT}/include") + foreach(LIB_SUBDIR "lib" "") + if(LIB_SUBDIR) + set(LIB_BASE "${NVIMGCODEC_PYTHON_ROOT}/${LIB_SUBDIR}") + else() + set(LIB_BASE "${NVIMGCODEC_PYTHON_ROOT}") + endif() + if(EXISTS "${LIB_BASE}/libnvimgcodec.so.0") + set(NVIMGCODEC_LIB_PATH "${LIB_BASE}/libnvimgcodec.so.0") + break() + elseif(EXISTS "${LIB_BASE}/libnvimgcodec.so") + set(NVIMGCODEC_LIB_PATH "${LIB_BASE}/libnvimgcodec.so") + break() + endif() + endforeach() + if(NVIMGCODEC_LIB_PATH) + break() + endif() + endif() + endif() + endforeach() + endif() + endif() + + # System-wide installation fallback + if(NOT NVIMGCODEC_LIB_PATH) + foreach(SYS_LIB_DIR "/usr/lib/x86_64-linux-gnu" "/usr/lib/aarch64-linux-gnu" "/usr/lib64") + if(EXISTS "${SYS_LIB_DIR}/libnvimgcodec.so.0") + set(NVIMGCODEC_LIB_PATH "${SYS_LIB_DIR}/libnvimgcodec.so.0") + set(NVIMGCODEC_INCLUDE_PATH "/usr/include") + break() + endif() + endforeach() + endif() + + if(NVIMGCODEC_LIB_PATH AND EXISTS "${NVIMGCODEC_LIB_PATH}") + set(NVIMGCODEC_FOUND TRUE) + endif() + endif() + + # ========================================================================= + # Create the target if nvImageCodec was found + # ========================================================================= + if(NVIMGCODEC_FOUND AND NOT TARGET deps::nvimgcodec) + if(NVIMGCODEC_LIB_PATH AND EXISTS "${NVIMGCODEC_LIB_PATH}") + add_library(deps::nvimgcodec SHARED IMPORTED GLOBAL) + set_target_properties(deps::nvimgcodec PROPERTIES + IMPORTED_LOCATION "${NVIMGCODEC_LIB_PATH}" + INTERFACE_INCLUDE_DIRECTORIES "${NVIMGCODEC_INCLUDE_PATH}" + ) + endif() + endif() + + if(NVIMGCODEC_FOUND) + message(STATUS "✓ nvImageCodec v${NVIMGCODEC_VERSION} configured successfully:") + message(STATUS " Library: ${NVIMGCODEC_LIB_PATH}") + message(STATUS " Headers: ${NVIMGCODEC_INCLUDE_PATH}") + if(NVIMGCODEC_EXTENSIONS_PATH) + message(STATUS " Extensions: ${NVIMGCODEC_EXTENSIONS_PATH}") + endif() + + # Cache the paths + set(NVIMGCODEC_INCLUDE_PATH ${NVIMGCODEC_INCLUDE_PATH} CACHE INTERNAL "" FORCE) + set(NVIMGCODEC_LIB_PATH ${NVIMGCODEC_LIB_PATH} CACHE INTERNAL "" FORCE) + set(NVIMGCODEC_EXTENSIONS_PATH ${NVIMGCODEC_EXTENSIONS_PATH} CACHE INTERNAL "" FORCE) + mark_as_advanced(NVIMGCODEC_INCLUDE_PATH NVIMGCODEC_LIB_PATH NVIMGCODEC_EXTENSIONS_PATH) + + # Export extensions path as compile definition (useful at runtime) + if(NVIMGCODEC_EXTENSIONS_PATH) + add_compile_definitions(NVIMGCODEC_EXTENSIONS_DIR="${NVIMGCODEC_EXTENSIONS_PATH}") + endif() + else() + message(STATUS "") + message(STATUS "✗ nvImageCodec v${NVIMGCODEC_VERSION} not found - GPU acceleration disabled") + message(STATUS "") + message(STATUS "To install nvImageCodec v${NVIMGCODEC_VERSION} (internal release Build 11):") + message(STATUS "") + message(STATUS " Option 1 - Use downloaded packages (CUDA 12):") + message(STATUS " cmake -DNVIMGCODEC_ROOT=/home/cdinea/Downloads/cucim_pr3/nvimgcodec/12 ..") + message(STATUS "") + message(STATUS " Option 2 - Use downloaded packages (CUDA 13):") + message(STATUS " cmake -DNVIMGCODEC_ROOT=/home/cdinea/Downloads/cucim_pr3/nvimgcodec/13 ..") + message(STATUS "") + message(STATUS " Option 3 - Auto-detect CUDA version:") + message(STATUS " cmake -DNVIMGCODEC_DIR=/home/cdinea/Downloads/cucim_pr3/nvimgcodec ..") + message(STATUS "") + message(STATUS " Available platforms for v${NVIMGCODEC_VERSION}:") + message(STATUS " C Packages: linux-x86_64, linux-sbsa, linux-aarch64 (12.9), windows-x86_64") + message(STATUS " Python (CUDA 12): linux-x86_64, linux-sbsa, linux-aarch64, windows-x86_64") + message(STATUS " Python (CUDA 13): linux-x86_64, linux-sbsa, windows-x86_64") + message(STATUS "") + endif() + + message(STATUS "=== End nvImageCodec Configuration ===") + message(STATUS "") +endif() diff --git a/cpp/plugins/cucim.kit.cuslide2/cmake/modules/CuCIMUtils.cmake b/cpp/plugins/cucim.kit.cuslide2/cmake/modules/CuCIMUtils.cmake new file mode 100644 index 000000000..7762e8338 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/cmake/modules/CuCIMUtils.cmake @@ -0,0 +1,52 @@ +# +# cmake-format: off +# SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. +# SPDX-License-Identifier: Apache-2.0 +# cmake-format: on +# + +# Store current BUILD_SHARED_LIBS setting in CUCIM_OLD_BUILD_SHARED_LIBS +if(NOT COMMAND cucim_set_build_shared_libs) + macro(cucim_set_build_shared_libs new_value) + set(CUCIM_OLD_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS}}) + if (DEFINED CACHE{BUILD_SHARED_LIBS}) + set(CUCIM_OLD_BUILD_SHARED_LIBS_CACHED TRUE) + else() + set(CUCIM_OLD_BUILD_SHARED_LIBS_CACHED FALSE) + endif() + set(BUILD_SHARED_LIBS ${new_value} CACHE BOOL "" FORCE) + endmacro() +endif() + +# Restore BUILD_SHARED_LIBS setting from CUCIM_OLD_BUILD_SHARED_LIBS +if(NOT COMMAND cucim_restore_build_shared_libs) + macro(cucim_restore_build_shared_libs) + if (CUCIM_OLD_BUILD_SHARED_LIBS_CACHED) + set(BUILD_SHARED_LIBS ${CUCIM_OLD_BUILD_SHARED_LIBS} CACHE BOOL "" FORCE) + else() + unset(BUILD_SHARED_LIBS CACHE) + set(BUILD_SHARED_LIBS ${CUCIM_OLD_BUILD_SHARED_LIBS}) + endif() + endmacro() +endif() + +# Define CMAKE_CUDA_ARCHITECTURES for the given architecture values +# +# Params: +# arch_list - architecture value list (e.g., '60;70;75;80;86') +if(NOT COMMAND cucim_define_cuda_architectures) + function(cucim_define_cuda_architectures arch_list) + set(arch_string "") + # Create SASS for all architectures in the list + foreach(arch IN LISTS arch_list) + set(arch_string "${arch_string}" "${arch}-real") + endforeach(arch) + + # Create PTX for the latest architecture for forward-compatibility. + list(GET arch_list -1 latest_arch) + foreach(arch IN LISTS arch_list) + set(arch_string "${arch_string}" "${latest_arch}-virtual") + endforeach(arch) + set(CMAKE_CUDA_ARCHITECTURES ${arch_string} PARENT_SCOPE) + endfunction() +endif() diff --git a/cpp/plugins/cucim.kit.cuslide2/cmake/modules/SuperBuildUtils.cmake b/cpp/plugins/cucim.kit.cuslide2/cmake/modules/SuperBuildUtils.cmake new file mode 100644 index 000000000..2edb82b6f --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/cmake/modules/SuperBuildUtils.cmake @@ -0,0 +1,24 @@ +# +# cmake-format: off +# SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. +# SPDX-License-Identifier: Apache-2.0 +# cmake-format: on +# + +include(FetchContent) + +# Local deps directory (for cuslide2-specific dependencies like nvimgcodec) +set(CMAKE_LOCAL_DEPS_DIR "${CMAKE_CURRENT_LIST_DIR}/../deps") +# Shared deps directory from cuslide plugin (for common dependencies) +set(CMAKE_SHARED_DEPS_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../cucim.kit.cuslide/cmake/deps") + +function(superbuild_depend module_name) + # Check local deps first (cuslide2-specific), then shared deps + if(EXISTS "${CMAKE_LOCAL_DEPS_DIR}/${module_name}.cmake") + include("${CMAKE_LOCAL_DEPS_DIR}/${module_name}.cmake") + elseif(EXISTS "${CMAKE_SHARED_DEPS_DIR}/${module_name}.cmake") + include("${CMAKE_SHARED_DEPS_DIR}/${module_name}.cmake") + else() + message(FATAL_ERROR "Dependency ${module_name}.cmake not found in local or shared deps") + endif() +endfunction() diff --git a/cpp/plugins/cucim.kit.cuslide2/cuslide.map b/cpp/plugins/cucim.kit.cuslide2/cuslide.map new file mode 100644 index 000000000..6ebbbdfbb --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/cuslide.map @@ -0,0 +1,4 @@ +CUSLIDE_0.1 { + local: + *; +}; diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.cpp b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.cpp new file mode 100644 index 000000000..60b0e3b4f --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.cpp @@ -0,0 +1,382 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ +#define CUCIM_EXPORTS + +#include "cuslide.h" + +#include "cucim/core/framework.h" +#include "cucim/core/plugin_util.h" +#include "cucim/io/format/image_format.h" +#include "tiff/tiff.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +const struct cucim::PluginImplDesc kPluginImpl = { + "cucim.kit.cuslide2", // name + { 0, 1, 0 }, // version + "dev", // build + "clara team", // author + "cuslide2", // description + "cuslide2 plugin with nvImageCodec support", // long_description + "Apache-2.0", // license + "https://github.com/rapidsai/cucim", // url + "linux", // platforms, + cucim::PluginHotReload::kDisabled, // hot_reload +}; + +// Using CARB_PLUGIN_IMPL_MINIMAL instead of CARB_PLUGIN_IMPL +// This minimal macro doesn't define global variables for logging, profiler, crash reporting, +// and also doesn't call for the client registration for those systems +CUCIM_PLUGIN_IMPL_MINIMAL(kPluginImpl, cucim::io::format::IImageFormat) +CUCIM_PLUGIN_IMPL_NO_DEPS() + + +static void set_enabled(bool val) +{ + (void)val; +} + +static bool is_enabled() +{ + return true; +} + +static const char* get_format_name() +{ + return "nvImageCodec TIFF"; +} + +static bool CUCIM_ABI checker_is_valid(const char* file_name, const char* buf, size_t size) +{ + (void)buf; + (void)size; + auto file = std::filesystem::path(file_name); + auto extension = file.extension().string(); + if (extension.compare(".tif") == 0 || extension.compare(".tiff") == 0 || extension.compare(".svs") == 0) + { + return true; + } + return false; +} + +static CuCIMFileHandle_share CUCIM_ABI parser_open(const char* file_path) +{ + auto tif = new cuslide::tiff::TIFF(file_path, O_RDONLY); + tif->construct_ifds(); + // Move the ownership of the file handle object to the caller (CuImage). + // CRITICAL: Use std::move to transfer ownership and avoid double-free + auto handle_t = std::move(tif->file_handle()); + tif->file_handle() = nullptr; // Now safe - original is already moved + CuCIMFileHandle_share handle = new std::shared_ptr(std::move(handle_t)); + return handle; +} + +static bool CUCIM_ABI parser_parse(CuCIMFileHandle_ptr handle_ptr, cucim::io::format::ImageMetadataDesc* out_metadata_desc) +{ + CuCIMFileHandle* handle = reinterpret_cast(handle_ptr); + if (!out_metadata_desc || !out_metadata_desc->handle) + { + throw std::runtime_error("out_metadata_desc shouldn't be nullptr!"); + } + cucim::io::format::ImageMetadata& out_metadata = + *reinterpret_cast(out_metadata_desc->handle); + + auto tif = static_cast(handle->client_data); + + size_t ifd_count = tif->ifd_count(); + size_t level_count = tif->level_count(); + + // Detect if this is an Aperio SVS file + // Try ImageDescription first (works with nvImageCodec 0.7.0+) + bool is_aperio_svs = (tif->ifd(0)->image_description().rfind("Aperio", 0) == 0); + + // Detect if this is a Philips TIFF file + // Philips TIFF also has multiple SubfileType=0 (by design) + bool is_philips_tiff = (tif->tiff_type() == cuslide::tiff::TiffType::Philips); + + // Fallback detection for nvImageCodec 0.6.0: check for multiple resolution levels + // Aperio SVS files typically have 3-6 IFDs with multiple resolution levels + // If we have multiple IFDs and they look like a pyramid, treat as Aperio/SVS + if (!is_aperio_svs && ifd_count >= 3 && level_count >= 3) + { + // Check if IFDs form a pyramid structure (decreasing sizes) + bool is_pyramid = true; + for (size_t i = 1; i < std::min(size_t(3), level_count); ++i) + { + auto ifd_curr = tif->level_ifd(i); + auto ifd_prev = tif->level_ifd(i-1); + if (ifd_curr->width() >= ifd_prev->width()) + { + is_pyramid = false; + break; + } + } + + if (is_pyramid) + { + #ifdef DEBUG + fmt::print("ℹ️ Detected pyramid structure → treating as Aperio SVS/multi-resolution TIFF\n"); + #endif // DEBUG + is_aperio_svs = true; + } + } + + // If not Aperio SVS, Philips TIFF, or multi-resolution pyramid, apply strict validation + if (!is_aperio_svs && !is_philips_tiff) + { + std::vector main_ifd_list; + for (size_t i = 0; i < ifd_count; i++) + { + const std::shared_ptr& ifd = tif->ifd(i); + uint64_t subfile_type = ifd->subfile_type(); + if (subfile_type == 0) + { + main_ifd_list.push_back(i); + } + } + + // Assume that the image has only one main (high resolution) image. + if (main_ifd_list.size() != 1) + { + throw std::runtime_error( + fmt::format("This format has more than one image with Subfile Type 0 so cannot be loaded!")); + } + } + + // + // Metadata Setup + // + + // Note: int-> uint16_t due to type differences between ImageMetadataDesc.ndim and DLTensor.ndim + const uint16_t ndim = 3; + auto& resource = out_metadata.get_resource(); + + std::string_view dims{ "YXC" }; + + const auto& level0_ifd = tif->level_ifd(0); + std::pmr::vector shape( + { level0_ifd->height(), level0_ifd->width(), level0_ifd->samples_per_pixel() }, &resource); + + DLDataType dtype{ kDLUInt, 8, 1 }; + + // TODO: Fill correct values for cucim::io::format::ImageMetadataDesc + uint8_t n_ch = level0_ifd->samples_per_pixel(); + if (n_ch != 3) + { + // Image loaded by a slow-path(libtiff) always will have 4 channel + // (by TIFFRGBAImageGet() method in libtiff) + n_ch = 4; + shape[2] = 4; + } + std::pmr::vector channel_names(&resource); + channel_names.reserve(n_ch); + if (n_ch == 3) + { + channel_names.emplace_back(std::string_view{ "R" }); + channel_names.emplace_back(std::string_view{ "G" }); + channel_names.emplace_back(std::string_view{ "B" }); + } + else + { + channel_names.emplace_back(std::string_view{ "R" }); + channel_names.emplace_back(std::string_view{ "G" }); + channel_names.emplace_back(std::string_view{ "B" }); + channel_names.emplace_back(std::string_view{ "A" }); + } + + // Spacing units + std::pmr::vector spacing_units(&resource); + spacing_units.reserve(ndim); + + std::pmr::vector spacing(&resource); + spacing.reserve(ndim); + const auto resolution_unit = level0_ifd->resolution_unit(); + const auto x_resolution = level0_ifd->x_resolution(); + const auto y_resolution = level0_ifd->y_resolution(); + + switch (resolution_unit) + { + case 1: // no absolute unit of measurement + spacing.emplace_back(y_resolution); + spacing.emplace_back(x_resolution); + spacing.emplace_back(1.0f); + + spacing_units.emplace_back(std::string_view{ "" }); + spacing_units.emplace_back(std::string_view{ "" }); + break; + case 2: // inch + spacing.emplace_back(y_resolution != 0 ? 25400 / y_resolution : 1.0f); + spacing.emplace_back(x_resolution != 0 ? 25400 / x_resolution : 1.0f); + spacing.emplace_back(1.0f); + + spacing_units.emplace_back(std::string_view{ "micrometer" }); + spacing_units.emplace_back(std::string_view{ "micrometer" }); + break; + case 3: // centimeter + spacing.emplace_back(y_resolution != 0 ? 10000 / y_resolution : 1.0f); + spacing.emplace_back(x_resolution != 0 ? 10000 / x_resolution : 1.0f); + spacing.emplace_back(1.0f); + + spacing_units.emplace_back(std::string_view{ "micrometer" }); + spacing_units.emplace_back(std::string_view{ "micrometer" }); + break; + default: + spacing.insert(spacing.end(), ndim, 1.0f); + } + + spacing_units.emplace_back(std::string_view{ "color" }); + + std::pmr::vector origin({ 0.0, 0.0, 0.0 }, &resource); + // Direction cosines (size is always 3x3) + // clang-format off + std::pmr::vector direction({ 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0}, &resource); + // clang-format on + + // The coordinate frame in which the direction cosines are measured (either 'LPS'(ITK/DICOM) or 'RAS'(NIfTI/3D + // Slicer)) + std::string_view coord_sys{ "LPS" }; + + const uint16_t level_ndim = 2; + std::pmr::vector level_dimensions(&resource); + level_dimensions.reserve(level_count * 2); + for (size_t i = 0; i < level_count; ++i) + { + const auto& level_ifd = tif->level_ifd(i); + level_dimensions.emplace_back(level_ifd->width()); + level_dimensions.emplace_back(level_ifd->height()); + } + + std::pmr::vector level_downsamples(&resource); + float orig_width = static_cast(shape[1]); + float orig_height = static_cast(shape[0]); + for (size_t i = 0; i < level_count; ++i) + { + const auto& level_ifd = tif->level_ifd(i); + level_downsamples.emplace_back(((orig_width / level_ifd->width()) + (orig_height / level_ifd->height())) / 2); + } + + std::pmr::vector level_tile_sizes(&resource); + level_tile_sizes.reserve(level_count * 2); + for (size_t i = 0; i < level_count; ++i) + { + const auto& level_ifd = tif->level_ifd(i); + level_tile_sizes.emplace_back(level_ifd->tile_width()); + level_tile_sizes.emplace_back(level_ifd->tile_height()); + } + + const size_t associated_image_count = tif->associated_image_count(); + std::pmr::vector associated_image_names(&resource); + for (const auto& associated_image : tif->associated_images()) + { + associated_image_names.emplace_back(std::string_view{ associated_image.first.c_str() }); + } + + auto& image_description = level0_ifd->image_description(); + std::string_view raw_data{ image_description.empty() ? "" : image_description.c_str() }; + + // Dynamically allocate memory for json_data (need to be freed manually); + const std::string& json_str = tif->metadata(); + char* json_data_ptr = static_cast(cucim_malloc(json_str.size() + 1)); + memcpy(json_data_ptr, json_str.data(), json_str.size() + 1); + std::string_view json_data{ json_data_ptr, json_str.size() }; + + out_metadata.ndim(ndim); + out_metadata.dims(std::move(dims)); + out_metadata.shape(std::move(shape)); + out_metadata.dtype(dtype); + out_metadata.channel_names(std::move(channel_names)); + out_metadata.spacing(std::move(spacing)); + out_metadata.spacing_units(std::move(spacing_units)); + out_metadata.origin(std::move(origin)); + out_metadata.direction(std::move(direction)); + out_metadata.coord_sys(std::move(coord_sys)); + out_metadata.level_count(level_count); + out_metadata.level_ndim(level_ndim); + out_metadata.level_dimensions(std::move(level_dimensions)); + out_metadata.level_downsamples(std::move(level_downsamples)); + out_metadata.level_tile_sizes(std::move(level_tile_sizes)); + out_metadata.image_count(associated_image_count); + out_metadata.image_names(std::move(associated_image_names)); + out_metadata.raw_data(raw_data); + out_metadata.json_data(json_data); + + return true; +} + +static bool CUCIM_ABI parser_close(CuCIMFileHandle_ptr handle_ptr) +{ + CuCIMFileHandle* handle = reinterpret_cast(handle_ptr); + + auto tif = static_cast(handle->client_data); + delete tif; + handle->client_data = nullptr; + return true; +} + +static bool CUCIM_ABI reader_read(const CuCIMFileHandle_ptr handle_ptr, + const cucim::io::format::ImageMetadataDesc* metadata, + const cucim::io::format::ImageReaderRegionRequestDesc* request, + cucim::io::format::ImageDataDesc* out_image_data, + cucim::io::format::ImageMetadataDesc* out_metadata = nullptr) +{ + CuCIMFileHandle* handle = reinterpret_cast(handle_ptr); + auto tif = static_cast(handle->client_data); + bool result = tif->read(metadata, request, out_image_data, out_metadata); + + return result; +} + +static bool CUCIM_ABI writer_write(const CuCIMFileHandle_ptr handle_ptr, + const cucim::io::format::ImageMetadataDesc* metadata, + const cucim::io::format::ImageDataDesc* image_data) +{ + CuCIMFileHandle* handle = reinterpret_cast(handle_ptr); + (void)handle; + (void)metadata; + (void)image_data; + + return true; +} + +void fill_interface(cucim::io::format::IImageFormat& iface) +{ + static cucim::io::format::ImageCheckerDesc image_checker = { 0, 0, checker_is_valid }; + static cucim::io::format::ImageParserDesc image_parser = { parser_open, parser_parse, parser_close }; + + static cucim::io::format::ImageReaderDesc image_reader = { reader_read }; + static cucim::io::format::ImageWriterDesc image_writer = { writer_write }; + + // clang-format off + static cucim::io::format::ImageFormatDesc image_format_desc = { + set_enabled, + is_enabled, + get_format_name, + image_checker, + image_parser, + image_reader, + image_writer + }; + // clang-format on + + // clang-format off + iface = + { + &image_format_desc, + 1 + }; + // clang-format on +} diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h new file mode 100644 index 000000000..e8b936e8b --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2020, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef CUSLIDE_CUSLIDE_H +#define CUSLIDE_CUSLIDE_H +#endif // CUSLIDE_CUSLIDE_H diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.cpp b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.cpp new file mode 100644 index 000000000..7c4de7d81 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.cpp @@ -0,0 +1,445 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "nvimgcodec_decoder.h" +#include "nvimgcodec_tiff_parser.h" + +#ifdef CUCIM_HAS_NVIMGCODEC +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef CUCIM_HAS_NVIMGCODEC +#include +#endif + +namespace cuslide2::nvimgcodec +{ + +#ifdef CUCIM_HAS_NVIMGCODEC + +// ============================================================================ +// RAII Helpers for nvImageCodec Resources +// ============================================================================ + +// RAII wrapper for nvimgcodecCodeStream_t (including sub-code streams) +// Per nvImageCodec team: each code stream (parent or sub) has its own state +// and MUST be explicitly destroyed. Sub-streams are NOT automatically cleaned +// up when the parent is destroyed. +struct CodeStreamDeleter +{ + void operator()(nvimgcodecCodeStream_t stream) const + { + if (stream) + { + nvimgcodecCodeStreamDestroy(stream); + } + } +}; +using UniqueCodeStream = std::unique_ptr, CodeStreamDeleter>; + +// RAII wrapper for nvimgcodecImage_t +struct ImageDeleter +{ + void operator()(nvimgcodecImage_t image) const + { + if (image) nvimgcodecImageDestroy(image); + } +}; +using UniqueImage = std::unique_ptr, ImageDeleter>; + +// RAII wrapper for nvimgcodecFuture_t +struct FutureDeleter +{ + void operator()(nvimgcodecFuture_t future) const + { + if (future) nvimgcodecFutureDestroy(future); + } +}; +using UniqueFuture = std::unique_ptr, FutureDeleter>; + +// RAII wrapper for decode buffer (handles both CPU and GPU memory) +class DecodeBuffer +{ +public: + DecodeBuffer() = default; + ~DecodeBuffer() { reset(); } + + // Non-copyable + DecodeBuffer(const DecodeBuffer&) = delete; + DecodeBuffer& operator=(const DecodeBuffer&) = delete; + + // Movable + DecodeBuffer(DecodeBuffer&& other) noexcept + : buffer_(other.buffer_), is_device_(other.is_device_) + { + other.buffer_ = nullptr; + } + + DecodeBuffer& operator=(DecodeBuffer&& other) noexcept + { + if (this != &other) + { + reset(); + buffer_ = other.buffer_; + is_device_ = other.is_device_; + other.buffer_ = nullptr; + } + return *this; + } + + bool allocate(size_t size, bool device_memory) + { + reset(); + is_device_ = device_memory; + if (device_memory) + { + cudaError_t status = cudaMalloc(&buffer_, size); + return status == cudaSuccess; + } + else + { + // Use standard malloc for CPU memory + // NOTE: Must use malloc() (not cudaMallocHost()) because cuCIM's cleanup + // code uses free(). Using cudaMallocHost() would require cudaFreeHost(), + // causing "free(): invalid pointer" crash when cuCIM calls free(). + buffer_ = malloc(size); + return buffer_ != nullptr; + } + } + + void reset() + { + if (buffer_) + { + if (is_device_) + cudaFree(buffer_); + else + free(buffer_); // Standard free (matches malloc) + buffer_ = nullptr; + } + } + + void* get() const { return buffer_; } + bool is_device() const { return is_device_; } + + // Release ownership (for passing to caller) + void* release() + { + void* tmp = buffer_; + buffer_ = nullptr; + return tmp; + } + +private: + void* buffer_ = nullptr; + bool is_device_ = false; +}; + +// ============================================================================ +// IFD-Level Region Decoding (Primary Decode Function) +// ============================================================================ + +bool decode_ifd_region_nvimgcodec(const IfdInfo& ifd_info, + nvimgcodecCodeStream_t main_code_stream, + uint32_t x, uint32_t y, + uint32_t width, uint32_t height, + uint8_t** output_buffer, + const cucim::io::Device& out_device) +{ + if (!main_code_stream) + { + #ifdef DEBUG + fmt::print("❌ Invalid main_code_stream\n"); + #endif + return false; + } + + #ifdef DEBUG + fmt::print("🚀 Decoding IFD[{}] region: [{},{}] {}x{}, codec: {}\n", + ifd_info.index, x, y, width, height, ifd_info.codec); + #endif + + try + { + // CRITICAL: Must use the same manager that created main_code_stream! + // Using a decoder from a different nvImageCodec instance causes segfaults. + auto& manager = NvImageCodecTiffParserManager::instance(); + if (!manager.is_available()) + { + #ifdef DEBUG + fmt::print("❌ nvImageCodec TIFF parser manager not initialized\n"); + #endif + return false; + } + + // Select decoder based on target device + // CPU-only backend can handle in-bounds ROI decoding for TIFF files + std::string device_str = std::string(out_device); + bool target_is_cpu = (device_str.find("cpu") != std::string::npos); + + // Check if ROI is out of bounds (extends beyond image boundaries) + // CPU decoder doesn't support out-of-bounds ROI decoding, must use hybrid decoder + bool roi_out_of_bounds = (x + width > ifd_info.width) || (y + height > ifd_info.height); + if (target_is_cpu && roi_out_of_bounds) + { + target_is_cpu = false; // Force hybrid decoder for out-of-bounds ROI + #ifdef DEBUG + fmt::print(" ⚠️ ROI out of bounds (region ends at [{},{}] but image is {}x{}), using hybrid decoder\n", + x + width, y + height, ifd_info.width, ifd_info.height); + #endif + } + + nvimgcodecDecoder_t decoder; + if (target_is_cpu && manager.has_cpu_decoder()) + { + decoder = manager.get_cpu_decoder(); + #ifdef DEBUG + fmt::print(" 💡 Using CPU-only decoder for ROI\n"); + #endif + } + else + { + decoder = manager.get_decoder(); + #ifdef DEBUG + fmt::print(" 💡 Using hybrid decoder for ROI\n"); + #endif + } + + // Step 1: Create view with ROI for this IFD + nvimgcodecRegion_t region{}; + region.struct_type = NVIMGCODEC_STRUCTURE_TYPE_REGION; + region.struct_size = sizeof(nvimgcodecRegion_t); + region.struct_next = nullptr; + region.ndim = 2; + region.start[0] = y; // row + region.start[1] = x; // col + region.end[0] = y + height; + region.end[1] = x + width; + + nvimgcodecCodeStreamView_t view{}; + view.struct_type = NVIMGCODEC_STRUCTURE_TYPE_CODE_STREAM_VIEW; + view.struct_size = sizeof(nvimgcodecCodeStreamView_t); + view.struct_next = nullptr; + view.image_idx = ifd_info.index; + view.region = region; + + // Get sub-code stream for this ROI (RAII managed) + nvimgcodecCodeStream_t roi_stream_raw = nullptr; + nvimgcodecStatus_t status = nvimgcodecCodeStreamGetSubCodeStream( + main_code_stream, + &roi_stream_raw, + &view + ); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + #ifdef DEBUG + fmt::print("❌ Failed to create ROI sub-stream (status: {})\n", + static_cast(status)); + #endif + return false; + } + // RAII wrapper - sub-stream will be properly destroyed when scope exits + UniqueCodeStream roi_stream(roi_stream_raw); + + // Step 2: Determine buffer kind based on target device and decoder + int device_count = 0; + cudaError_t cuda_err = cudaGetDeviceCount(&device_count); + bool gpu_available = (cuda_err == cudaSuccess && device_count > 0); + + nvimgcodecImageBufferKind_t buffer_kind; + if (target_is_cpu) + { + // CPU target: use host buffer directly + buffer_kind = NVIMGCODEC_IMAGE_BUFFER_KIND_STRIDED_HOST; + #ifdef DEBUG + fmt::print(" ℹ️ Using CPU buffer for ROI decoding\n"); + #endif + } + else if (gpu_available) + { + // GPU target with GPU available: use device buffer + buffer_kind = NVIMGCODEC_IMAGE_BUFFER_KIND_STRIDED_DEVICE; + } + else + { + // GPU target but no GPU available: fall back to host buffer + buffer_kind = NVIMGCODEC_IMAGE_BUFFER_KIND_STRIDED_HOST; + #ifdef DEBUG + fmt::print(" ⚠️ No GPU available, using CPU buffer\n"); + #endif + } + + // Step 3: Prepare output image info for the region + nvimgcodecImageInfo_t output_image_info{}; + output_image_info.struct_type = NVIMGCODEC_STRUCTURE_TYPE_IMAGE_INFO; + output_image_info.struct_size = sizeof(nvimgcodecImageInfo_t); + output_image_info.struct_next = nullptr; + + // Use interleaved RGB format + output_image_info.sample_format = NVIMGCODEC_SAMPLEFORMAT_I_RGB; + output_image_info.color_spec = NVIMGCODEC_COLORSPEC_SRGB; + output_image_info.chroma_subsampling = NVIMGCODEC_SAMPLING_NONE; + output_image_info.num_planes = 1; + output_image_info.buffer_kind = buffer_kind; + + // Calculate buffer requirements for the region + uint32_t num_channels = 3; // RGB + size_t row_stride = width * num_channels; + size_t buffer_size = row_stride * height; + + output_image_info.plane_info[0].height = height; + output_image_info.plane_info[0].width = width; + output_image_info.plane_info[0].num_channels = num_channels; + output_image_info.plane_info[0].row_stride = row_stride; + output_image_info.plane_info[0].sample_type = NVIMGCODEC_SAMPLE_DATA_TYPE_UINT8; + // Note: buffer_size removed in nvImageCodec v0.7.0 - size is inferred from plane_info + output_image_info.cuda_stream = 0; + + #ifdef DEBUG + fmt::print(" Buffer: {}x{} RGB, stride={}, size={} bytes\n", + width, height, row_stride, buffer_size); + #endif + + // Step 4: Allocate output buffer (RAII managed) + bool use_device_memory = (buffer_kind == NVIMGCODEC_IMAGE_BUFFER_KIND_STRIDED_DEVICE); + DecodeBuffer decode_buffer; + if (!decode_buffer.allocate(buffer_size, use_device_memory)) + { + #ifdef DEBUG + fmt::print("❌ Failed to allocate {} memory\n", use_device_memory ? "GPU" : "host"); + #endif + return false; + } + #ifdef DEBUG + fmt::print(" Allocated {} buffer\n", use_device_memory ? "GPU" : "CPU"); + #endif + + output_image_info.buffer = decode_buffer.get(); + + // Step 5: Create image object (RAII managed) + nvimgcodecImage_t image_raw = nullptr; + status = nvimgcodecImageCreate( + manager.get_instance(), + &image_raw, + &output_image_info + ); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + #ifdef DEBUG + fmt::print("❌ Failed to create image object (status: {})\n", + static_cast(status)); + #endif + return false; // RAII handles cleanup + } + UniqueImage image(image_raw); + + // Step 6: Prepare decode parameters + nvimgcodecDecodeParams_t decode_params{}; + decode_params.struct_type = NVIMGCODEC_STRUCTURE_TYPE_DECODE_PARAMS; + decode_params.struct_size = sizeof(nvimgcodecDecodeParams_t); + decode_params.struct_next = nullptr; + decode_params.apply_exif_orientation = 1; + + // Step 7: Schedule decoding (RAII managed) + nvimgcodecCodeStream_t roi_stream_ptr = roi_stream.get(); + nvimgcodecImage_t image_ptr = image.get(); + nvimgcodecFuture_t decode_future_raw = nullptr; + status = nvimgcodecDecoderDecode(decoder, + &roi_stream_ptr, + &image_ptr, + 1, + &decode_params, + &decode_future_raw); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + #ifdef DEBUG + fmt::print("❌ Failed to schedule decoding (status: {})\n", + static_cast(status)); + #endif + return false; // RAII handles cleanup + } + UniqueFuture decode_future(decode_future_raw); + + // Step 8: Wait for completion + nvimgcodecProcessingStatus_t decode_status = NVIMGCODEC_PROCESSING_STATUS_UNKNOWN; + size_t status_size = 1; + status = nvimgcodecFutureGetProcessingStatus(decode_future.get(), &decode_status, &status_size); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + #ifdef DEBUG + fmt::print("❌ Failed to get processing status (status: {})\n", static_cast(status)); + #endif + return false; // RAII handles cleanup + } + + if (use_device_memory) + { + cudaDeviceSynchronize(); + } + + // Step 9: Check decode status + if (decode_status != NVIMGCODEC_PROCESSING_STATUS_SUCCESS) + { + #ifdef DEBUG + fmt::print("❌ Decoding failed (status: {})\n", static_cast(decode_status)); + #endif + return false; // RAII handles cleanup + } + + #ifdef DEBUG + fmt::print("✅ Successfully decoded IFD[{}] region\n", ifd_info.index); + #endif + + // Success: release buffer ownership to caller (RAII cleanup skipped for buffer) + *output_buffer = reinterpret_cast(decode_buffer.release()); + #ifdef DEBUG + fmt::print("✅ nvImageCodec ROI decode successful: {}x{} at ({}, {})\n", + width, height, x, y); + #endif + return true; // roi_stream, image, decode_future all cleaned up by RAII + } + catch (const std::exception& e) + { + #ifdef DEBUG + fmt::print("❌ Exception in ROI decoding: {}\n", e.what()); + #endif + return false; + } +} + +#else // !CUCIM_HAS_NVIMGCODEC + +// Fallback stub when nvImageCodec is not available +// cuslide2 plugin requires nvImageCodec, so this should never be called +// Forward declaration for types +struct IfdInfo; +typedef void* nvimgcodecCodeStream_t; + +bool decode_ifd_region_nvimgcodec(const IfdInfo&, + nvimgcodecCodeStream_t, + uint32_t, uint32_t, + uint32_t, uint32_t, + uint8_t**, + const cucim::io::Device&) +{ + throw std::runtime_error("cuslide2 plugin requires nvImageCodec to be enabled at compile time"); +} + +#endif // CUCIM_HAS_NVIMGCODEC + +} // namespace cuslide2::nvimgcodec diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.h b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.h new file mode 100644 index 000000000..8a7771c8e --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.h @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef CUSLIDE2_NVIMGCODEC_DECODER_H +#define CUSLIDE2_NVIMGCODEC_DECODER_H + +#ifdef CUCIM_HAS_NVIMGCODEC +#include +#endif + +#include +#include + +namespace cuslide2::nvimgcodec +{ + +#ifdef CUCIM_HAS_NVIMGCODEC +// Forward declaration +struct IfdInfo; + +/** + * Decode a region of interest (ROI) from an IFD using nvImageCodec + * + * Uses nvImageCodec's CodeStreamView with region specification for + * memory-efficient decoding of specific image areas. + * + * @param ifd_info Parsed IFD information with sub_code_stream + * @param main_code_stream Main TIFF code stream (for creating ROI views) + * @param x Starting x coordinate (column) + * @param y Starting y coordinate (row) + * @param width Width of region in pixels + * @param height Height of region in pixels + * @param output_buffer Pointer to receive allocated buffer (caller must free) + * @param out_device Output device ("cpu" or "cuda") + * @return true if successful, false otherwise + */ +bool decode_ifd_region_nvimgcodec(const IfdInfo& ifd_info, + nvimgcodecCodeStream_t main_code_stream, + uint32_t x, uint32_t y, + uint32_t width, uint32_t height, + uint8_t** output_buffer, + const cucim::io::Device& out_device); +#endif // CUCIM_HAS_NVIMGCODEC + +} // namespace cuslide2::nvimgcodec + +#endif // CUSLIDE2_NVIMGCODEC_DECODER_H diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.cpp b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.cpp new file mode 100644 index 000000000..6c67c24b1 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.cpp @@ -0,0 +1,1245 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "nvimgcodec_tiff_parser.h" + +#include // for std::transform +#include // for strlen + +#ifdef CUCIM_HAS_NVIMGCODEC +#include +#include +#endif + +#include +#include +#include +#include +#include + +namespace cuslide2::nvimgcodec +{ + +#ifdef CUCIM_HAS_NVIMGCODEC + +// Helper function to convert TiffTagValue variant to string representation +static std::string tiff_tag_value_to_string(const TiffTagValue& value) +{ + return std::visit([](const auto& v) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + return ""; // Empty/unset + } + else if constexpr (std::is_same_v) + { + return v; + } + else if constexpr (std::is_same_v>) + { + return fmt::format("[{} bytes]", v.size()); + } + else if constexpr (std::is_same_v>) + { + std::string result; + for (size_t i = 0; i < v.size() && i < 10; ++i) + { + if (i > 0) result += ","; + result += std::to_string(v[i]); + } + if (v.size() > 10) result += ",..."; + return result; + } + else if constexpr (std::is_same_v>) + { + std::string result; + for (size_t i = 0; i < v.size() && i < 10; ++i) + { + if (i > 0) result += ","; + result += std::to_string(v[i]); + } + if (v.size() > 10) result += ",..."; + return result; + } + else if constexpr (std::is_same_v>) + { + std::string result; + for (size_t i = 0; i < v.size() && i < 10; ++i) + { + if (i > 0) result += ","; + result += std::to_string(v[i]); + } + if (v.size() > 10) result += ",..."; + return result; + } + else if constexpr (std::is_same_v>) + { + std::string result; + for (size_t i = 0; i < v.size() && i < 10; ++i) + { + if (i > 0) result += ","; + result += std::to_string(v[i]); + } + if (v.size() > 10) result += ",..."; + return result; + } + else if constexpr (std::is_same_v>) + { + std::string result; + for (size_t i = 0; i < v.size() && i < 10; ++i) + { + if (i > 0) result += ","; + result += std::to_string(v[i]); + } + if (v.size() > 10) result += ",..."; + return result; + } + else if constexpr (std::is_same_v || std::is_same_v) + { + return fmt::format("{}", v); + } + else + { + return std::to_string(v); + } + }, value); +} + +// Template helper to extract single scalar value from TIFF tag metadata +// Per nvImageCodec team: value_count check is sufficient, buffer_size check is redundant +template +static bool extract_single_value(const std::vector& buffer, + int value_count, + TiffTagValue& out_value) +{ + if (value_count == 1) + { + T val = *reinterpret_cast(buffer.data()); + out_value = val; + return true; + } + return false; +} + +// Template helper to extract array of values as vector +template +static bool extract_value_array(const std::vector& buffer, + int value_count, + TiffTagValue& out_value) +{ + if (value_count > 1) + { + const T* vals = reinterpret_cast(buffer.data()); + out_value = std::vector(vals, vals + value_count); + return true; + } + return false; +} + +// ============================================================================ +// NvImageCodecTiffParserManager Implementation +// ============================================================================ + +NvImageCodecTiffParserManager::NvImageCodecTiffParserManager() + : instance_(nullptr), decoder_(nullptr), cpu_decoder_(nullptr), initialized_(false) +{ + try + { + // Create nvImageCodec instance for TIFF parsing (separate from decoder instance) + nvimgcodecInstanceCreateInfo_t create_info{}; + create_info.struct_type = NVIMGCODEC_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + create_info.struct_size = sizeof(nvimgcodecInstanceCreateInfo_t); + create_info.struct_next = nullptr; + create_info.load_builtin_modules = 1; // Load JPEG, PNG, etc. + create_info.load_extension_modules = 1; // Load JPEG2K, TIFF, etc. + create_info.extension_modules_path = nullptr; + create_info.create_debug_messenger = 0; // Disable debug for TIFF parser + create_info.debug_messenger_desc = nullptr; + create_info.message_severity = 0; + create_info.message_category = 0; + + nvimgcodecStatus_t status = nvimgcodecInstanceCreate(&instance_, &create_info); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + status_message_ = fmt::format("Failed to create nvImageCodec instance for TIFF parsing (status: {})", + static_cast(status)); + #ifdef DEBUG + fmt::print("⚠️ {}\n", status_message_); + #endif // DEBUG + return; + } + + // Create decoder for metadata extraction (not for image decoding) + // This decoder is used exclusively for nvimgcodecDecoderGetMetadata() calls + nvimgcodecExecutionParams_t exec_params{}; + exec_params.struct_type = NVIMGCODEC_STRUCTURE_TYPE_EXECUTION_PARAMS; + exec_params.struct_size = sizeof(nvimgcodecExecutionParams_t); + exec_params.struct_next = nullptr; + exec_params.device_allocator = nullptr; + exec_params.pinned_allocator = nullptr; + exec_params.max_num_cpu_threads = 0; + exec_params.executor = nullptr; + exec_params.device_id = NVIMGCODEC_DEVICE_CPU_ONLY; // CPU-only for metadata extraction + exec_params.pre_init = 0; + exec_params.skip_pre_sync = 0; + exec_params.num_backends = 0; + exec_params.backends = nullptr; + + status = nvimgcodecDecoderCreate(instance_, &decoder_, &exec_params, nullptr); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + nvimgcodecInstanceDestroy(instance_); + instance_ = nullptr; + status_message_ = fmt::format("Failed to create decoder for metadata extraction (status: {})", + static_cast(status)); + #ifdef DEBUG + fmt::print("⚠️ {}\n", status_message_); + #endif // DEBUG + return; + } + + // Create CPU-only decoder for native CPU decoding + nvimgcodecBackendKind_t cpu_backend_kind = NVIMGCODEC_BACKEND_KIND_CPU_ONLY; + nvimgcodecBackendParams_t cpu_backend_params{}; + cpu_backend_params.struct_type = NVIMGCODEC_STRUCTURE_TYPE_BACKEND_PARAMS; + cpu_backend_params.struct_size = sizeof(nvimgcodecBackendParams_t); + cpu_backend_params.struct_next = nullptr; + + nvimgcodecBackend_t cpu_backend{}; + cpu_backend.struct_type = NVIMGCODEC_STRUCTURE_TYPE_BACKEND; + cpu_backend.struct_size = sizeof(nvimgcodecBackend_t); + cpu_backend.struct_next = nullptr; + cpu_backend.kind = cpu_backend_kind; + cpu_backend.params = cpu_backend_params; + + nvimgcodecExecutionParams_t cpu_exec_params = exec_params; + cpu_exec_params.num_backends = 1; + cpu_exec_params.backends = &cpu_backend; + + if (nvimgcodecDecoderCreate(instance_, &cpu_decoder_, &cpu_exec_params, nullptr) == NVIMGCODEC_STATUS_SUCCESS) + { + #ifdef DEBUG + fmt::print("✅ CPU-only decoder created successfully (TIFF parser)\n"); + #endif // DEBUG + } + else + { + #ifdef DEBUG + fmt::print("⚠️ Failed to create CPU-only decoder (CPU decoding will use fallback)\n"); + #endif // DEBUG + cpu_decoder_ = nullptr; + } + + initialized_ = true; + status_message_ = "nvImageCodec TIFF parser initialized successfully (with metadata extraction support)"; + #ifdef DEBUG + fmt::print("✅ {}\n", status_message_); + #endif // DEBUG + } + catch (const std::exception& e) + { + status_message_ = fmt::format("nvImageCodec TIFF parser initialization exception: {}", e.what()); + #ifdef DEBUG + fmt::print("❌ {}\n", status_message_); + #endif // DEBUG + initialized_ = false; + } +} + +NvImageCodecTiffParserManager::~NvImageCodecTiffParserManager() +{ + // Proper cleanup: destroy decoders first, then instance + // Per nvImageCodec team: all code streams should be destroyed before this point + // (handled by TiffFileParser destructors which are called before singleton destruction) + + if (cpu_decoder_) + { + nvimgcodecDecoderDestroy(cpu_decoder_); + cpu_decoder_ = nullptr; + } + + if (decoder_) + { + nvimgcodecDecoderDestroy(decoder_); + decoder_ = nullptr; + } + + if (instance_) + { + nvimgcodecInstanceDestroy(instance_); + instance_ = nullptr; + } +} + +// ============================================================================ +// TiffFileParser Implementation +// ============================================================================ + +TiffFileParser::TiffFileParser(const std::string& file_path) + : file_path_(file_path), initialized_(false), + main_code_stream_(nullptr) +{ + auto& manager = NvImageCodecTiffParserManager::instance(); + + if (!manager.is_available()) + { + throw std::runtime_error(fmt::format("nvImageCodec not available: {}", + manager.get_status())); + } + + try + { + // Step 1: Create code stream from TIFF file + nvimgcodecStatus_t status = nvimgcodecCodeStreamCreateFromFile( + manager.get_instance(), + &main_code_stream_, + file_path.c_str() + ); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + throw std::runtime_error(fmt::format("Failed to create code stream from file: {} (status: {})", + file_path, static_cast(status))); + } + + #ifdef DEBUG + fmt::print("✅ Opened TIFF file: {}\n", file_path); + #endif // DEBUG + + // Step 2: Parse TIFF structure (metadata only) + parse_tiff_structure(); + + initialized_ = true; + #ifdef DEBUG + fmt::print("✅ TIFF parser initialized with {} IFDs\n", ifd_infos_.size()); + #endif // DEBUG + } + catch (const std::exception& e) + { + // Don't explicitly destroy main_code_stream_ here - let instance cleanup handle it + // (See destructor comment for explanation of static destruction order issues) + main_code_stream_ = nullptr; + + throw; // Re-throw + } +} + +TiffFileParser::~TiffFileParser() +{ + // Per nvImageCodec team: each code stream (parent or sub) has its own state + // and MUST be explicitly destroyed. Sub-streams are NOT automatically cleaned + // up when the parent is destroyed. + + // Destroy sub-code streams first (IFD streams) + for (auto& ifd_info : ifd_infos_) + { + if (ifd_info.sub_code_stream) + { + nvimgcodecCodeStreamDestroy(ifd_info.sub_code_stream); + ifd_info.sub_code_stream = nullptr; + } + } + + // Then destroy main code stream + if (main_code_stream_) + { + nvimgcodecCodeStreamDestroy(main_code_stream_); + main_code_stream_ = nullptr; + } + + ifd_infos_.clear(); +} + +void TiffFileParser::parse_tiff_structure() +{ + // Get TIFF structure information + nvimgcodecCodeStreamInfo_t stream_info{}; + stream_info.struct_type = NVIMGCODEC_STRUCTURE_TYPE_CODE_STREAM_INFO; + stream_info.struct_size = sizeof(nvimgcodecCodeStreamInfo_t); + stream_info.struct_next = nullptr; + + nvimgcodecStatus_t status = nvimgcodecCodeStreamGetCodeStreamInfo( + main_code_stream_, &stream_info); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + throw std::runtime_error(fmt::format("Failed to get code stream info (status: {})", + static_cast(status))); + } + + uint32_t num_ifds = stream_info.num_images; + #ifdef DEBUG + fmt::print(" TIFF has {} IFDs (resolution levels)\n", num_ifds); + #endif // DEBUG + + if (stream_info.codec_name[0] != '\0') + { + #ifdef DEBUG + fmt::print(" Codec: {}\n", stream_info.codec_name); + #endif // DEBUG + } + + // Get information for each IFD + for (uint32_t i = 0; i < num_ifds; ++i) + { + IfdInfo ifd_info; + ifd_info.index = i; + + // Create view for this IFD + nvimgcodecCodeStreamView_t view{}; + view.struct_type = NVIMGCODEC_STRUCTURE_TYPE_CODE_STREAM_VIEW; + view.struct_size = sizeof(nvimgcodecCodeStreamView_t); + view.struct_next = nullptr; + view.image_idx = i; // Note: nvImageCodec uses 'image_idx' not 'image_index' + + // Get sub-code stream for this IFD + status = nvimgcodecCodeStreamGetSubCodeStream(main_code_stream_, + &ifd_info.sub_code_stream, + &view); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + #ifdef DEBUG + fmt::print("❌ Failed to get sub-code stream for IFD {} (status: {})\n", + i, static_cast(status)); + #endif // DEBUG + #ifdef DEBUG + fmt::print(" This IFD will be SKIPPED and cannot be decoded.\n"); + #endif // DEBUG + // Set sub_code_stream to nullptr explicitly to mark as invalid + ifd_info.sub_code_stream = nullptr; + continue; + } + + // Get image information for this IFD + nvimgcodecImageInfo_t image_info{}; + image_info.struct_type = NVIMGCODEC_STRUCTURE_TYPE_IMAGE_INFO; + image_info.struct_size = sizeof(nvimgcodecImageInfo_t); + image_info.struct_next = nullptr; + + status = nvimgcodecCodeStreamGetImageInfo(ifd_info.sub_code_stream, &image_info); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + #ifdef DEBUG + fmt::print("❌ Failed to get image info for IFD {} (status: {})\n", + i, static_cast(status)); + #endif // DEBUG + #ifdef DEBUG + fmt::print(" This IFD will be SKIPPED and cannot be decoded.\n"); + #endif // DEBUG + // NOTE: Do NOT destroy sub_code_stream here - it's a view into main_code_stream + // Main stream destruction will handle cleanup. Just mark as invalid. + ifd_info.sub_code_stream = nullptr; + continue; + } + + // Extract IFD metadata + ifd_info.width = image_info.plane_info[0].width; + ifd_info.height = image_info.plane_info[0].height; + ifd_info.num_channels = image_info.num_planes; + + // Extract bits per sample from sample type + // sample_type encoding: bytes_per_element = (type >> 11) & 0xFF + // Convert bytes to bits + auto sample_type = image_info.plane_info[0].sample_type; + int bytes_per_element = (static_cast(sample_type) >> 11) & 0xFF; + ifd_info.bits_per_sample = bytes_per_element * 8; // Convert bytes to bits + + // NOTE: image_info.codec_name typically contains "tiff" (the container format) + // We need to determine the actual compression codec (jpeg2000, jpeg, etc.) + if (image_info.codec_name[0] != '\0') + { + ifd_info.codec = image_info.codec_name; + } + + // Extract metadata for this IFD using nvimgcodecDecoderGetMetadata + // Extract vendor-specific metadata (Aperio, Philips, etc.) + extract_ifd_metadata(ifd_info); + + // Extract TIFF metadata using available methods + extract_tiff_tags(ifd_info); + + // TODO(nvImageCodec 0.7.0): Use direct TIFF tag queries when 0.7.0 is released + // Individual TIFF tag access (e.g., COMPRESSION tag 259) will be available in 0.7.0 + // Example: metadata = decoder.get_metadata(scs, name="Compression") + // + // Current limitation (0.6.0): + // - codec_name returns "tiff" (container format) not compression type + // - Individual TIFF tags not exposed through metadata API + // - Only vendor-specific metadata blobs available (MED_APERIO, MED_PHILIPS, etc.) + // + // Workaround: Infer compression from chroma_subsampling and file extension + // Reference: https://nvidia.slack.com/archives/C092X06LK9U (Oct 27, 2024) + if (ifd_info.codec == "tiff") + { + // Try to infer compression from TIFF metadata first + bool compression_inferred = false; + + // Check if we have TIFF Compression tag (stored as typed value) + auto compression_it = ifd_info.tiff_tags.find("COMPRESSION"); + if (compression_it != ifd_info.tiff_tags.end()) + { + // COMPRESSION tag is always SHORT (uint16_t) per TIFF spec + // Check type before extracting to avoid exceptions + if (std::holds_alternative(compression_it->second)) + { + uint16_t compression_value = std::get(compression_it->second); + + switch (compression_value) + { + case 1: // COMPRESSION_NONE + // Keep as "tiff" for uncompressed + #ifdef DEBUG + fmt::print(" ℹ️ Detected uncompressed TIFF\n"); + #endif // DEBUG + compression_inferred = true; + break; + case 5: // COMPRESSION_LZW + ifd_info.codec = "tiff"; // nvImageCodec handles as tiff + compression_inferred = true; + #ifdef DEBUG + fmt::print(" ℹ️ Detected LZW compression (TIFF codec)\n"); + #endif // DEBUG + break; + case 7: // COMPRESSION_JPEG + ifd_info.codec = "jpeg"; // Use JPEG decoder! + compression_inferred = true; + #ifdef DEBUG + fmt::print(" ℹ️ Detected JPEG compression → using JPEG codec\n"); + #endif // DEBUG + break; + case 8: // COMPRESSION_DEFLATE (Adobe-style) + case 32946: // COMPRESSION_DEFLATE (old-style) + ifd_info.codec = "tiff"; + compression_inferred = true; + #ifdef DEBUG + fmt::print(" ℹ️ Detected DEFLATE compression (TIFF codec)\n"); + #endif // DEBUG + break; + case 33003: // Aperio JPEG2000 YCbCr + case 33005: // Aperio JPEG2000 RGB + case 34712: // JPEG2000 + ifd_info.codec = "jpeg2000"; + compression_inferred = true; + #ifdef DEBUG + fmt::print(" ℹ️ Detected JPEG2000 compression\n"); + #endif // DEBUG + break; + default: + #ifdef DEBUG + fmt::print(" ⚠️ Unknown TIFF compression value: {}\n", compression_value); + #endif // DEBUG + break; + } + } + else + { + #ifdef DEBUG + fmt::print(" ⚠️ COMPRESSION tag is not uint16_t (unexpected type)\n"); + #endif // DEBUG + } + } + + // Fallback to filename-based heuristics if metadata didn't help + if (!compression_inferred) + { + // Aperio JPEG2000 files typically have "JP2K" in filename + if (file_path_.find("JP2K") != std::string::npos || + file_path_.find("jp2k") != std::string::npos) + { + ifd_info.codec = "jpeg2000"; + #ifdef DEBUG + fmt::print(" ℹ️ Inferred codec 'jpeg2000' from filename (JP2K pattern)\n"); + #endif // DEBUG + compression_inferred = true; + } + } + + // Warning if we still couldn't infer compression + if (!compression_inferred && ifd_info.tiff_tags.empty()) + { + #ifdef DEBUG + fmt::print(" ⚠️ Warning: codec is 'tiff' but could not infer compression.\n"); + fmt::print(" File: {}\n", file_path_); + fmt::print(" This may limit CPU decoder availability.\n"); + #endif + } + } + + ifd_infos_.push_back(std::move(ifd_info)); + } + + // Report parsing results + if (ifd_infos_.size() == num_ifds) + { + #ifdef DEBUG + fmt::print("✅ TIFF parser initialized with {} IFDs (all successful)\n", ifd_infos_.size()); + #endif // DEBUG + } + else + { + #ifdef DEBUG + fmt::print("⚠️ TIFF parser initialized with {} IFDs ({} out of {} total)\n", + ifd_infos_.size(), ifd_infos_.size(), num_ifds); + #endif // DEBUG + #ifdef DEBUG + fmt::print(" {} IFDs were skipped due to parsing errors\n", num_ifds - ifd_infos_.size()); + #endif // DEBUG + } +} + +void TiffFileParser::extract_ifd_metadata(IfdInfo& ifd_info) +{ + auto& manager = NvImageCodecTiffParserManager::instance(); + + #ifdef DEBUG + fmt::print("🔍 Extracting metadata for IFD[{}]...\n", ifd_info.index); + #endif + + if (!manager.get_decoder() || !ifd_info.sub_code_stream) + { + if (!manager.get_decoder()) + fmt::print(" ⚠️ Decoder not available\n"); + if (!ifd_info.sub_code_stream) + fmt::print(" ⚠️ No sub-code stream for this IFD\n"); + return; // No decoder or stream available + } + + // Step 1: Get metadata count (first call with nullptr) + int metadata_count = 0; + nvimgcodecStatus_t status = nvimgcodecDecoderGetMetadata( + manager.get_decoder(), + ifd_info.sub_code_stream, + nullptr, // First call: get count only + &metadata_count + ); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + #ifdef DEBUG + fmt::print(" ⚠️ Metadata query failed with status: {}\n", static_cast(status)); + #endif + return; + } + + if (metadata_count == 0) + { + #ifdef DEBUG + fmt::print(" ℹ️ No metadata entries found for this IFD\n"); + #endif + return; // No metadata + } + + #ifdef DEBUG + fmt::print(" ✅ Found {} metadata entries for IFD[{}]\n", metadata_count, ifd_info.index); + #endif + + // Step 2: Allocate metadata structures AND buffers + // nvImageCodec requires us to allocate buffers based on buffer_size from first call + std::vector metadata_structs(metadata_count); + std::vector metadata_ptrs(metadata_count); + std::vector> metadata_buffers(metadata_count); // Storage for actual data + + // First, query to get buffer sizes (metadata structs must be initialized) + for (int i = 0; i < metadata_count; i++) + { + metadata_structs[i].struct_type = NVIMGCODEC_STRUCTURE_TYPE_METADATA; + metadata_structs[i].struct_size = sizeof(nvimgcodecMetadata_t); + metadata_structs[i].struct_next = nullptr; + metadata_structs[i].buffer = nullptr; // Query mode: get sizes + metadata_structs[i].buffer_size = 0; + metadata_ptrs[i] = &metadata_structs[i]; + } + + // Query call to get buffer sizes + status = nvimgcodecDecoderGetMetadata( + manager.get_decoder(), + ifd_info.sub_code_stream, + metadata_ptrs.data(), + &metadata_count + ); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + #ifdef DEBUG + fmt::print(" ⚠️ Failed to query metadata sizes (status: {})\n", static_cast(status)); + #endif + return; + } + + // Now allocate buffers based on reported sizes + for (int i = 0; i < metadata_count; i++) + { + size_t required_size = metadata_structs[i].buffer_size; + if (required_size > 0) + { + metadata_buffers[i].resize(required_size); + metadata_structs[i].buffer = metadata_buffers[i].data(); + #ifdef DEBUG + fmt::print(" 📦 Allocated {} bytes for metadata[{}]\n", required_size, i); + #endif + } + } + + // Step 3: Get actual metadata content (buffers now allocated) + status = nvimgcodecDecoderGetMetadata( + manager.get_decoder(), + ifd_info.sub_code_stream, + metadata_ptrs.data(), + &metadata_count + ); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + #ifdef DEBUG + fmt::print(" ⚠️ Failed to retrieve metadata content (status: {})\n", static_cast(status)); + #endif + return; + } + + #ifdef DEBUG + fmt::print(" ✅ Successfully retrieved {} metadata entries with content\n", metadata_count); + #endif + + // Step 4: Process each metadata entry + for (int j = 0; j < metadata_count; ++j) + { + if (!metadata_ptrs[j]) + continue; + + nvimgcodecMetadata_t* metadata = metadata_ptrs[j]; + + // Extract metadata fields + int kind = metadata->kind; + int format = metadata->format; + size_t buffer_size = metadata->buffer_size; + const uint8_t* buffer = static_cast(metadata->buffer); + + #ifdef DEBUG + // Map kind to human-readable name for debugging + const char* kind_name = "UNKNOWN"; + switch (kind) { + case NVIMGCODEC_METADATA_KIND_UNKNOWN: kind_name = "UNKNOWN"; break; + case NVIMGCODEC_METADATA_KIND_TIFF_TAG: kind_name = "TIFF_TAG"; break; + case NVIMGCODEC_METADATA_KIND_ICC_PROFILE: kind_name = "ICC_PROFILE"; break; + case NVIMGCODEC_METADATA_KIND_EXIF: kind_name = "EXIF"; break; + case NVIMGCODEC_METADATA_KIND_GEO: kind_name = "GEO"; break; + case NVIMGCODEC_METADATA_KIND_MED_APERIO: kind_name = "MED_APERIO"; break; + case NVIMGCODEC_METADATA_KIND_MED_PHILIPS: kind_name = "MED_PHILIPS"; break; + case NVIMGCODEC_METADATA_KIND_MED_VENTANA: kind_name = "MED_VENTANA"; break; + case NVIMGCODEC_METADATA_KIND_MED_LEICA: kind_name = "MED_LEICA"; break; + case NVIMGCODEC_METADATA_KIND_MED_TRESTLE: kind_name = "MED_TRESTLE"; break; + } + fmt::print(" Metadata[{}]: kind={} ({}), format={}, size={}\n", + j, kind, kind_name, format, buffer_size); + #endif + + // Store in metadata_blobs map + if (buffer && buffer_size > 0) + { + IfdInfo::MetadataBlob blob; + blob.format = format; + blob.data.assign(buffer, buffer + buffer_size); + ifd_info.metadata_blobs[kind] = std::move(blob); + + // Note: ImageDescription is now extracted directly via TIFF tag 270 + // in extract_tiff_tags() using nvImageCodec 0.7.0's direct tag query API. + // The vendor metadata blobs (MED_APERIO, MED_PHILIPS, etc.) are stored + // above for format detection and vendor-specific parsing. + } + } +} + +const IfdInfo& TiffFileParser::get_ifd(uint32_t index) const +{ + if (index >= ifd_infos_.size()) + { + throw std::out_of_range(fmt::format("IFD index {} out of range (have {} IFDs)", + index, ifd_infos_.size())); + } + return ifd_infos_[index]; +} + +std::string TiffFileParser::get_tiff_tag(uint32_t ifd_index, const std::string& tag_name) const +{ + if (ifd_index >= ifd_infos_.size()) + return ""; + + auto it = ifd_infos_[ifd_index].tiff_tags.find(tag_name); + if (it != ifd_infos_[ifd_index].tiff_tags.end()) + return tiff_tag_value_to_string(it->second); + + return ""; +} + +void TiffFileParser::extract_tiff_tags(IfdInfo& ifd_info) +{ + auto& manager = NvImageCodecTiffParserManager::instance(); + + if (!manager.get_decoder()) + { + #ifdef DEBUG + fmt::print(" ⚠️ Cannot extract TIFF tags: decoder not available\n"); + #endif // DEBUG + return; + } + + if (!ifd_info.sub_code_stream) + { + #ifdef DEBUG + fmt::print(" ⚠️ Cannot extract TIFF tags: sub_code_stream is null\n"); + #endif // DEBUG + return; + } + + // ======================================================================== + // nvImageCodec 0.7.0: Direct TIFF Tag Retrieval by ID + // ======================================================================== + // Python API example: + // tag_value = decoder.get_metadata(scs, id=tag_id).value + // + // C API equivalent: + // 1. Set metadata.kind = NVIMGCODEC_METADATA_KIND_TIFF_TAG + // 2. Set metadata.id = (e.g., 270 for ImageDescription) + // 3. Call nvimgcodecDecoderGetMetadata() to retrieve the specific tag + + // Map of TIFF tag IDs to names for tags we want to extract + std::vector> tiff_tags_to_query = { + {254, "SUBFILETYPE"}, // Image type classification (0=full, 1=reduced, etc.) + {256, "IMAGEWIDTH"}, + {257, "IMAGELENGTH"}, + {258, "BITSPERSAMPLE"}, + {259, "COMPRESSION"}, // Critical for codec detection! + {262, "PHOTOMETRIC"}, + {270, "IMAGEDESCRIPTION"}, // Vendor metadata + {271, "MAKE"}, // Scanner manufacturer + {272, "MODEL"}, // Scanner model + {277, "SAMPLESPERPIXEL"}, + {305, "SOFTWARE"}, + {306, "DATETIME"}, + {322, "TILEWIDTH"}, + {323, "TILELENGTH"}, + {330, "SUBIFD"}, // SubIFD offsets (for OME-TIFF, etc.) + {339, "SAMPLEFORMAT"}, + {347, "JPEGTABLES"} // Shared JPEG tables + }; + + #ifdef DEBUG + fmt::print(" 📋 Extracting TIFF tags (nvImageCodec 0.7.0 - query by ID)...\n"); + #endif // DEBUG + + int extracted_count = 0; + + // Query each tag individually by ID (following Python API pattern) + for (const auto& [tag_id, tag_name] : tiff_tags_to_query) + { + // Set up metadata request for specific tag + nvimgcodecMetadata_t metadata{}; + metadata.struct_type = NVIMGCODEC_STRUCTURE_TYPE_METADATA; + metadata.struct_size = sizeof(nvimgcodecMetadata_t); + metadata.struct_next = nullptr; + metadata.kind = NVIMGCODEC_METADATA_KIND_TIFF_TAG; + metadata.id = tag_id; + metadata.buffer = nullptr; + metadata.buffer_size = 0; + + nvimgcodecMetadata_t* metadata_ptr = &metadata; + int metadata_count = 1; + + // First call: query buffer size + nvimgcodecStatus_t status = nvimgcodecDecoderGetMetadata( + manager.get_decoder(), + ifd_info.sub_code_stream, + &metadata_ptr, + &metadata_count + ); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + // API error - log warning for unexpected failures + // Note: Some status codes may indicate "tag not found" which is normal + #ifdef DEBUG + fmt::print(" ⚠️ TIFF tag {} query failed (status: {})\n", tag_id, static_cast(status)); + #endif + continue; + } + + if (metadata.buffer_size == 0) + { + // Tag not present in this IFD - this is normal, not all tags exist + continue; + } + + // Allocate buffer for tag value + std::vector buffer(metadata.buffer_size); + metadata.buffer = buffer.data(); + + // Second call: retrieve actual value + status = nvimgcodecDecoderGetMetadata( + manager.get_decoder(), + ifd_info.sub_code_stream, + &metadata_ptr, + &metadata_count + ); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + // Unexpected: first call succeeded but second failed + #ifdef DEBUG + fmt::print(" ⚠️ TIFF tag {} retrieval failed (status: {})\n", tag_id, static_cast(status)); + #endif + continue; + } + + if (metadata.buffer_size == 0) + { + // Unexpected: buffer was allocated but size is now 0 + continue; + } + + // Convert value based on type and store as typed variant + // The variant is initialized to std::monostate by default + TiffTagValue tag_value; + + switch (metadata.value_type) + { + case NVIMGCODEC_METADATA_VALUE_TYPE_ASCII: + { + // ASCII string + std::string str_val; + str_val.assign(reinterpret_cast(buffer.data()), metadata.buffer_size); + // Remove trailing null(s) if present + while (!str_val.empty() && str_val.back() == '\0') + str_val.pop_back(); + if (!str_val.empty()) + { + tag_value = std::move(str_val); + } + break; + } + + case NVIMGCODEC_METADATA_VALUE_TYPE_SHORT: + extract_single_value(buffer, metadata.value_count, tag_value) || + extract_value_array(buffer, metadata.value_count, tag_value); + break; + + case NVIMGCODEC_METADATA_VALUE_TYPE_LONG: + extract_single_value(buffer, metadata.value_count, tag_value) || + extract_value_array(buffer, metadata.value_count, tag_value); + break; + + case NVIMGCODEC_METADATA_VALUE_TYPE_BYTE: + if (metadata.value_count == 1) + { + tag_value = buffer[0]; + } + else + { + // Binary data - store as vector + std::vector vec(buffer.begin(), buffer.begin() + metadata.buffer_size); + tag_value = std::move(vec); + } + break; + + case NVIMGCODEC_METADATA_VALUE_TYPE_SBYTE: + if (metadata.value_count == 1) + { + tag_value = static_cast(buffer[0]); + } + else + { + // Signed byte array - store as vector (reinterpret as needed) + std::vector vec(buffer.begin(), buffer.begin() + metadata.buffer_size); + tag_value = std::move(vec); + } + break; + + case NVIMGCODEC_METADATA_VALUE_TYPE_UNDEFINED: + // UNDEFINED type - binary data, store as vector + { + std::vector vec(buffer.begin(), buffer.begin() + metadata.buffer_size); + tag_value = std::move(vec); + } + break; + + case NVIMGCODEC_METADATA_VALUE_TYPE_SSHORT: + extract_single_value(buffer, metadata.value_count, tag_value) || + extract_value_array(buffer, metadata.value_count, tag_value); + break; + + case NVIMGCODEC_METADATA_VALUE_TYPE_SLONG: + extract_single_value(buffer, metadata.value_count, tag_value) || + extract_value_array(buffer, metadata.value_count, tag_value); + break; + + case NVIMGCODEC_METADATA_VALUE_TYPE_LONG8: + case NVIMGCODEC_METADATA_VALUE_TYPE_IFD8: + extract_single_value(buffer, metadata.value_count, tag_value) || + extract_value_array(buffer, metadata.value_count, tag_value); + break; + + case NVIMGCODEC_METADATA_VALUE_TYPE_SLONG8: + extract_single_value(buffer, metadata.value_count, tag_value) || + extract_value_array(buffer, metadata.value_count, tag_value); + break; + + case NVIMGCODEC_METADATA_VALUE_TYPE_FLOAT: + extract_single_value(buffer, metadata.value_count, tag_value) || + extract_value_array(buffer, metadata.value_count, tag_value); + break; + + case NVIMGCODEC_METADATA_VALUE_TYPE_DOUBLE: + extract_single_value(buffer, metadata.value_count, tag_value) || + extract_value_array(buffer, metadata.value_count, tag_value); + break; + + case NVIMGCODEC_METADATA_VALUE_TYPE_RATIONAL: + if (metadata.value_count == 1 && metadata.buffer_size >= 8) + { + // Single Rational = two LONGs (numerator, denominator) - store as string + uint32_t num = *reinterpret_cast(buffer.data()); + uint32_t den = *reinterpret_cast(buffer.data() + 4); + if (den != 0) + tag_value = fmt::format("{}/{}", num, den); + else + tag_value = std::to_string(num); + } + else if (metadata.value_count > 1) + { + // Array of Rationals - store as comma-separated string + size_t rational_size = 8; // 2 × uint32_t + std::string result; + for (size_t i = 0; i < metadata.value_count; ++i) + { + const uint8_t* ptr = buffer.data() + i * rational_size; + uint32_t num = *reinterpret_cast(ptr); + uint32_t den = *reinterpret_cast(ptr + 4); + if (i > 0) result += ", "; + if (den != 0) + result += fmt::format("{}/{}", num, den); + else + result += std::to_string(num); + } + tag_value = std::move(result); + } + break; + + case NVIMGCODEC_METADATA_VALUE_TYPE_SRATIONAL: + if (metadata.value_count == 1 && metadata.buffer_size >= 8) + { + // Single Signed Rational = two SLONGs (numerator, denominator) - store as string + int32_t num = *reinterpret_cast(buffer.data()); + int32_t den = *reinterpret_cast(buffer.data() + 4); + if (den != 0) + tag_value = fmt::format("{}/{}", num, den); + else + tag_value = std::to_string(num); + } + else if (metadata.value_count > 1) + { + // Array of Signed Rationals - store as comma-separated string + size_t rational_size = 8; // 2 × int32_t + std::string result; + for (size_t i = 0; i < metadata.value_count; ++i) + { + const uint8_t* ptr = buffer.data() + i * rational_size; + int32_t num = *reinterpret_cast(ptr); + int32_t den = *reinterpret_cast(ptr + 4); + if (i > 0) result += ", "; + if (den != 0) + result += fmt::format("{}/{}", num, den); + else + result += std::to_string(num); + } + tag_value = std::move(result); + } + break; + + default: + // For unknown types, store as binary data or string + if (metadata.buffer_size > 0) + { + if (metadata.buffer_size <= 8 && metadata.value_count == 1) + { + // Small value - try to interpret as number and store as string + uint64_t val = 0; + std::memcpy(&val, buffer.data(), std::min(metadata.buffer_size, sizeof(val))); + tag_value = std::to_string(val); + } + else + { + // Store raw bytes - optionally limit size to prevent storing huge blobs + // Use configurable limit (0 = unlimited, default) + size_t store_size = metadata.buffer_size; + if (max_binary_tag_size_ > 0 && metadata.buffer_size > max_binary_tag_size_) + { + store_size = max_binary_tag_size_; + #ifdef DEBUG + fmt::print(" ℹ️ TIFF tag {} binary data truncated: {} -> {} bytes\n", + tag_id, metadata.buffer_size, store_size); + #endif + } + std::vector vec(buffer.begin(), buffer.begin() + store_size); + tag_value = std::move(vec); + } + } + break; + } + + // Check if a value was successfully stored (not monostate) + if (!std::holds_alternative(tag_value)) + { + ifd_info.tiff_tags[tag_name] = std::move(tag_value); + extracted_count++; + + #ifdef DEBUG + // Format value for debug output + std::string debug_str = std::visit([](const auto& v) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) + return ""; + else if constexpr (std::is_same_v) + return v.length() > 60 ? v.substr(0, 60) + "..." : v; + else if constexpr (std::is_same_v>) + return fmt::format("[{} bytes]", v.size()); + else if constexpr (std::is_same_v>) + return fmt::format("[{} uint16 values]", v.size()); + else if constexpr (std::is_same_v>) + return fmt::format("[{} uint32 values]", v.size()); + else if constexpr (std::is_same_v>) + return fmt::format("[{} uint64 values]", v.size()); + else if constexpr (std::is_same_v>) + return fmt::format("[{} float values]", v.size()); + else if constexpr (std::is_same_v>) + return fmt::format("[{} double values]", v.size()); + else if constexpr (std::is_same_v || std::is_same_v) + return fmt::format("{}", v); + else + return std::to_string(v); + }, ifd_info.tiff_tags[tag_name]); + fmt::print(" ✅ Tag {} ({}): {}\n", tag_id, tag_name, debug_str); + #endif // DEBUG + } + } + + if (extracted_count > 0) + { + #ifdef DEBUG + fmt::print(" ✅ Extracted {} TIFF tags using nvImageCodec 0.7.0 API\n", extracted_count); + #endif // DEBUG + + // Store ImageDescription if available from tags + auto desc_it = ifd_info.tiff_tags.find("IMAGEDESCRIPTION"); + if (desc_it != ifd_info.tiff_tags.end() && ifd_info.image_description.empty()) + { + ifd_info.image_description = tiff_tag_value_to_string(desc_it->second); + } + + return; // Success + } + + // Fallback: File extension heuristics for older nvImageCodec versions + #ifdef DEBUG + fmt::print(" ⚠️ Using file extension heuristics (no TIFF tags retrieved)\n"); + #endif // DEBUG + + std::string ext; + size_t dot_pos = file_path_.rfind('.'); + if (dot_pos != std::string::npos) + { + ext = file_path_.substr(dot_pos); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + } + + // Aperio SVS, Hamamatsu NDPI, Hamamatsu VMS/VMU typically use JPEG compression + if (ext == ".svs" || ext == ".ndpi" || ext == ".vms" || ext == ".vmu") + { + ifd_info.tiff_tags["COMPRESSION"] = static_cast(7); // TIFF_COMPRESSION_JPEG + #ifdef DEBUG + fmt::print(" ✅ Inferred JPEG compression (WSI format: {})\n", ext); + #endif // DEBUG + } +} + +int TiffFileParser::get_subfile_type(uint32_t ifd_index) const +{ + std::string subfile_str = get_tiff_tag(ifd_index, "SUBFILETYPE"); + if (subfile_str.empty()) + return -1; + + try { + return std::stoi(subfile_str); + } catch (...) { + return -1; + } +} + +std::vector TiffFileParser::query_metadata_kinds(uint32_t ifd_index) const +{ + std::vector kinds; + + if (ifd_index >= ifd_infos_.size()) + return kinds; + + // Return all metadata kinds found in this IFD + for (const auto& [kind, blob] : ifd_infos_[ifd_index].metadata_blobs) + { + kinds.push_back(kind); + } + + // Also add TIFF_TAG kind if any tags were extracted + // nvImageCodec 0.7.0: TIFF_TAG = 1 (not 0!) + if (!ifd_infos_[ifd_index].tiff_tags.empty()) + { + kinds.insert(kinds.begin(), NVIMGCODEC_METADATA_KIND_TIFF_TAG); + } + + return kinds; +} + +std::string TiffFileParser::get_detected_format() const +{ + if (ifd_infos_.empty()) + return "Unknown"; + + // Check first IFD for vendor-specific metadata + // nvImageCodec 0.7.0: Use proper enum values + const auto& kinds = query_metadata_kinds(0); + + for (int kind : kinds) + { + switch (kind) + { + case NVIMGCODEC_METADATA_KIND_MED_APERIO: + return "Aperio SVS"; + case NVIMGCODEC_METADATA_KIND_MED_PHILIPS: + return "Philips TIFF"; + case NVIMGCODEC_METADATA_KIND_MED_LEICA: + return "Leica SCN"; + case NVIMGCODEC_METADATA_KIND_MED_VENTANA: + return "Ventana"; + case NVIMGCODEC_METADATA_KIND_MED_TRESTLE: + return "Trestle"; + default: + break; + } + } + + // Fallback: Generic TIFF with detected codec + if (!ifd_infos_.empty() && !ifd_infos_[0].codec.empty()) + { + return fmt::format("Generic TIFF ({})", ifd_infos_[0].codec); + } + + return "Generic TIFF"; +} + +#endif // CUCIM_HAS_NVIMGCODEC + +} // namespace cuslide2::nvimgcodec + diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.h b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.h new file mode 100644 index 000000000..22e539018 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.h @@ -0,0 +1,498 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#ifdef CUCIM_HAS_NVIMGCODEC +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cuslide2::nvimgcodec +{ + +#ifdef CUCIM_HAS_NVIMGCODEC + +/** + * @brief Variant type for storing typed TIFF tag values + * + * Supports all TIFF tag value types as defined in nvimgcodecMetadataValueType_t: + * - std::monostate: Empty/unset state (tag not found or not extracted) + * - std::string: ASCII strings (ImageDescription, Software, DateTime, etc.) + * - int8_t, uint8_t: SBYTE/BYTE values + * - int16_t, uint16_t: SSHORT/SHORT values (Compression, Photometric, etc.) + * - int32_t, uint32_t: SLONG/LONG values (ImageWidth, ImageLength, SubfileType, etc.) + * - int64_t, uint64_t: SLONG8/LONG8/IFD8 values (BigTIFF support) + * - float, double: FLOAT/DOUBLE values + * - std::vector: Binary data (JPEGTables, UNDEFINED type, etc.) + * - std::vector: Arrays of SHORT values (BitsPerSample, etc.) + * - std::vector: Arrays of LONG values (SubIFD offsets, etc.) + * - std::vector: Arrays of LONG8 values (BigTIFF offsets, etc.) + */ +using TiffTagValue = std::variant< + std::monostate, // Empty/unset state + std::string, + int8_t, + uint8_t, + int16_t, + uint16_t, + int32_t, + uint32_t, + int64_t, + uint64_t, + float, + double, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, // Arrays of FLOAT values + std::vector // Arrays of DOUBLE values +>; + +/** + * @brief Image type classification for TIFF IFDs + * + * Used to categorize IFDs as resolution levels or associated images + * (particularly for formats like Aperio SVS that use SUBFILETYPE tags) + */ +/** + * @brief Information about a single IFD (Image File Directory) in a TIFF file + * + * Represents one resolution level in a multi-resolution TIFF pyramid. + */ +struct IfdInfo +{ + uint32_t index; // IFD index (0, 1, 2, ...) + uint32_t width; // Image width in pixels + uint32_t height; // Image height in pixels + uint32_t num_channels; // Number of channels (typically 3 for RGB) + uint32_t bits_per_sample; // Bits per channel (8, 16, etc.) + std::string codec; // Compression codec (jpeg, jpeg2k, deflate, etc.) - replace with int : 0,1,2, for each codec type + nvimgcodecCodeStream_t sub_code_stream; // nvImageCodec code stream for this IFD + + // Metadata fields (extracted from nvImageCodec metadata API) + std::string image_description; // ImageDescription TIFF tag (270) + + // Format-specific metadata: kind -> (format, buffer_data) + // kind: nvimgcodecMetadataKind_t (e.g., MED_APERIO=1, MED_PHILIPS=2, etc.) + // format: nvimgcodecMetadataFormat_t (e.g., RAW, XML, JSON) + // buffer_data: raw bytes from metadata buffer + struct MetadataBlob { + int format; // nvimgcodecMetadataFormat_t + std::vector data; + }; + std::map metadata_blobs; + + // nvImageCodec 0.7.0: Individual TIFF tag storage with typed values + // tag_name -> TiffTagValue (variant with typed storage) + std::unordered_map tiff_tags; + + IfdInfo() : index(0), width(0), height(0), num_channels(0), + bits_per_sample(0), sub_code_stream(nullptr) {} + + ~IfdInfo() + { + // NOTE: sub_code_stream is managed by TiffFileParser and should NOT be destroyed here + // The parent TiffFileParser destroys all sub-code streams when destroying main_code_stream + } + + // Disable copy, enable move + IfdInfo(const IfdInfo&) = delete; + IfdInfo& operator=(const IfdInfo&) = delete; + IfdInfo(IfdInfo&&) = default; + IfdInfo& operator=(IfdInfo&&) = default; +}; + +/** + * @brief TIFF file parser using nvImageCodec file-level API + * + * This class provides TIFF parsing capabilities using nvImageCodec's native + * TIFF support. It can query TIFF structure (IFD count, dimensions, codecs) + * and decode entire resolution levels. + * + * Note: This is an alternative to the libtiff-based approach. It provides + * simpler code but less metadata access and no tile-level granularity. + * + * Usage: + * auto tiff = std::make_unique("image.tif"); + * if (tiff->is_valid()) { + * uint32_t num_levels = tiff->get_ifd_count(); + * const auto& ifd = tiff->get_ifd(0); + * + * // Use IFD information for decoding via separate decoder + * // (decoding is handled by IFD::read() or similar) + * } + */ +class TiffFileParser +{ +public: + /** + * @brief Open and parse a TIFF file + * + * @param file_path Path to TIFF file + * @throws std::runtime_error if nvImageCodec is not available or file cannot be opened + */ + explicit TiffFileParser(const std::string& file_path); + + /** + * @brief Destructor - cleans up nvImageCodec resources + */ + ~TiffFileParser(); + + // Disable copy, enable move + TiffFileParser(const TiffFileParser&) = delete; + TiffFileParser& operator=(const TiffFileParser&) = delete; + TiffFileParser(TiffFileParser&&) = default; + TiffFileParser& operator=(TiffFileParser&&) = default; + + /** + * @brief Check if TIFF file was successfully opened and parsed + * + * @return true if file is valid and ready to use + */ + bool is_valid() const { return initialized_; } + + /** + * @brief Get the number of IFDs (resolution levels) in the TIFF file + * + * @return Number of IFDs + */ + uint32_t get_ifd_count() const { return static_cast(ifd_infos_.size()); } + + /** + * @brief Get information about a specific IFD + * + * @param index IFD index (0 = highest resolution) + * @return Reference to IFD information + * @throws std::out_of_range if index is invalid + */ + const IfdInfo& get_ifd(uint32_t index) const; + + /** + * @brief Get all metadata blobs for an IFD + * + * Returns all vendor-specific metadata extracted by nvImageCodec. + * The map key is nvimgcodecMetadataKind_t (e.g., MED_APERIO=1, MED_PHILIPS=2). + * + * @param ifd_index IFD index + * @return Map of metadata kind to blob (format + data), or empty if no metadata + */ + const std::map& get_metadata_blobs(uint32_t ifd_index) const + { + static const std::map empty_map; + if (ifd_index >= ifd_infos_.size()) + return empty_map; + return ifd_infos_[ifd_index].metadata_blobs; + } + + // ======================================================================== + // nvImageCodec 0.7.0 Features: Individual TIFF Tag Retrieval + // ======================================================================== + + /** + * @brief Get a specific TIFF tag value as string (nvImageCodec 0.7.0+) + * + * Uses NVIMGCODEC_METADATA_KIND_TIFF_TAG to retrieve individual TIFF tags + * by name (e.g., "SUBFILETYPE", "ImageDescription", "DateTime", etc.) + * + * @param ifd_index IFD index + * @param tag_name TIFF tag name (case-sensitive) + * @return Tag value as string, or empty if not found + */ + std::string get_tiff_tag(uint32_t ifd_index, const std::string& tag_name) const; + + /** + * @brief Get SUBFILETYPE tag for format classification (nvImageCodec 0.7.0+) + * + * Returns the SUBFILETYPE value used in formats like Aperio SVS: + * - 0 = full resolution image + * - 1 = reduced resolution image (thumbnail/label/macro) + * + * @param ifd_index IFD index + * @return SUBFILETYPE value, or -1 if not present + */ + int get_subfile_type(uint32_t ifd_index) const; + + /** + * @brief Query all available metadata kinds in file (nvImageCodec 0.7.0+) + * + * Returns a list of metadata kinds present in the file for discovery. + * Useful for detecting file format (Aperio, Philips, Generic TIFF, etc.) + * + * Example kinds: TIFF_TAG=0, MED_APERIO=1, MED_PHILIPS=2, etc. + * + * @param ifd_index IFD index (default 0 for file-level metadata) + * @return Vector of metadata kind values present in the IFD + */ + std::vector query_metadata_kinds(uint32_t ifd_index = 0) const; + + /** + * @brief Get detected file format based on metadata (nvImageCodec 0.7.0+) + * + * Automatically detects format by checking available metadata kinds. + * nvImageCodec 0.7.0 handles detection internally. + * + * @return Format name: "Aperio SVS", "Philips TIFF", "Leica SCN", "Generic TIFF", etc. + */ + std::string get_detected_format() const; + + /** + * @brief Get the main code stream for the TIFF file + * + * This is used by decoder functions (in nvimgcodec_decoder.cpp) to create + * ROI sub-streams for decoding. The parser provides the stream, but does + * NOT perform decoding itself (separation of concerns). + * + * @return nvImageCodec code stream handle + */ + nvimgcodecCodeStream_t get_main_code_stream() const { return main_code_stream_; } + + /** + * @brief Set maximum size for binary TIFF tag data storage + * + * Controls the maximum size of binary data (UNDEFINED type, JPEGTables, etc.) + * that will be stored in memory when parsing TIFF tags. This can prevent + * memory issues when encountering very large binary blobs. + * + * @param max_size Maximum size in bytes (0 = unlimited, default) + * + * Note: This limit only applies to binary/unknown data types. Known typed + * arrays (SHORT, LONG, FLOAT, etc.) are not limited for consistency. + */ + void set_max_binary_tag_size(size_t max_size) { max_binary_tag_size_ = max_size; } + + /** + * @brief Get the current maximum binary tag size limit + * + * @return Maximum binary tag size in bytes (0 = unlimited) + */ + size_t get_max_binary_tag_size() const { return max_binary_tag_size_; } + +private: + /** + * @brief Parse TIFF file structure using nvImageCodec + * + * Queries the number of IFDs and gets metadata for each one. + */ + void parse_tiff_structure(); + + /** + * @brief Extract metadata for a specific IFD using nvimgcodecDecoderGetMetadata + * + * Retrieves vendor-specific metadata (Aperio, Philips, etc.) for the given IFD. + * Populates ifd_info.metadata_blobs and ifd_info.image_description. + * + * @param ifd_info IFD to extract metadata for (must have valid sub_code_stream) + */ + void extract_ifd_metadata(IfdInfo& ifd_info); + + /** + * @brief Extract individual TIFF tags (nvImageCodec 0.7.0+) + * + * Uses NVIMGCODEC_METADATA_KIND_TIFF_TAG to query specific TIFF tags by name. + * Populates ifd_info.tiff_tags map. + * + * @param ifd_info IFD to extract TIFF tags for + */ + void extract_tiff_tags(IfdInfo& ifd_info); + + std::string file_path_; + bool initialized_; + nvimgcodecCodeStream_t main_code_stream_; + std::vector ifd_infos_; + + // Configuration: Maximum size for binary TIFF tag data (0 = unlimited) + size_t max_binary_tag_size_ = 0; +}; + +/** + * @brief Singleton manager for nvImageCodec TIFF parsing + * + * Manages the global nvImageCodec instance for TIFF parsing operations. + * This is separate from the tile decoder manager to avoid conflicts. + */ +class NvImageCodecTiffParserManager +{ +public: + /** + * @brief Get the singleton instance + * + * @return Reference to the global manager + */ + static NvImageCodecTiffParserManager& instance() + { + static NvImageCodecTiffParserManager manager; + return manager; + } + + /** + * @brief Get the nvImageCodec instance + * + * @return nvImageCodec instance handle + */ + nvimgcodecInstance_t get_instance() const { return instance_; } + + /** + * @brief Get the nvImageCodec decoder (for metadata extraction) + * + * @return nvImageCodec decoder handle + */ + nvimgcodecDecoder_t get_decoder() const { return decoder_; } + + /** + * @brief Get the CPU-only decoder (for native CPU decoding) + * + * @return nvImageCodec CPU decoder handle + */ + nvimgcodecDecoder_t get_cpu_decoder() const { return cpu_decoder_; } + + /** + * @brief Check if CPU-only decoder is available + * + * @return true if CPU decoder is available + */ + bool has_cpu_decoder() const { return cpu_decoder_ != nullptr; } + + /** + * @brief Get the mutex for thread-safe decoder operations + * + * @return Reference to the decoder mutex + */ + std::mutex& get_mutex() { return decoder_mutex_; } + + /** + * @brief Check if nvImageCodec is available and initialized + * + * @return true if available + */ + bool is_available() const { return initialized_; } + + /** + * @brief Get initialization status message + * + * @return Status message + */ + const std::string& get_status() const { return status_message_; } + +private: + NvImageCodecTiffParserManager(); + ~NvImageCodecTiffParserManager(); + + // Disable copy and move + NvImageCodecTiffParserManager(const NvImageCodecTiffParserManager&) = delete; + NvImageCodecTiffParserManager& operator=(const NvImageCodecTiffParserManager&) = delete; + NvImageCodecTiffParserManager(NvImageCodecTiffParserManager&&) = delete; + NvImageCodecTiffParserManager& operator=(NvImageCodecTiffParserManager&&) = delete; + + nvimgcodecInstance_t instance_; + nvimgcodecDecoder_t decoder_; + nvimgcodecDecoder_t cpu_decoder_; // CPU-only decoder (uses libjpeg-turbo, etc.) + bool initialized_; + std::string status_message_; + std::mutex decoder_mutex_; // Protect decoder operations from concurrent access +}; + +#else // !CUCIM_HAS_NVIMGCODEC + +// Stub implementations when nvImageCodec is not available + +// Stub TiffTagValue for API compatibility +using TiffTagValue = std::variant< + std::monostate, // Empty/unset state + std::string, + int8_t, + uint8_t, + int16_t, + uint16_t, + int32_t, + uint32_t, + int64_t, + uint64_t, + float, + double, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, // Arrays of FLOAT values + std::vector // Arrays of DOUBLE values +>; + +struct IfdInfo { + struct MetadataBlob { + int format; + std::vector data; + }; + std::unordered_map tiff_tags; +}; + +class TiffFileParser +{ +public: + explicit TiffFileParser(const std::string& file_path) { (void)file_path; } + bool is_valid() const { return false; } + uint32_t get_ifd_count() const { return 0; } + const IfdInfo& get_ifd(uint32_t index) const + { + (void)index; + throw std::runtime_error("nvImageCodec not available"); + } + + // Stub methods for API compatibility + const std::map& get_metadata_blobs(uint32_t ifd_index) const + { + (void)ifd_index; + static const std::map empty_map; + return empty_map; + } + + std::string get_detected_format() const + { + return "Unknown"; + } + + std::string get_tiff_tag(uint32_t ifd_index, const std::string& tag_name) const + { + (void)ifd_index; + (void)tag_name; + return ""; + } + + int get_subfile_type(uint32_t ifd_index) const + { + (void)ifd_index; + return -1; + } +}; + +class NvImageCodecTiffParserManager +{ +public: + static NvImageCodecTiffParserManager& instance() + { + static NvImageCodecTiffParserManager manager; + return manager; + } + bool is_available() const { return false; } + const std::string& get_status() const + { + static std::string msg = "nvImageCodec not available"; + return msg; + } +}; + +#endif // CUCIM_HAS_NVIMGCODEC + +} // namespace cuslide2::nvimgcodec diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.cpp b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.cpp new file mode 100644 index 000000000..50e7c4dc1 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.cpp @@ -0,0 +1,1378 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ifd.h" + +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +// nvImageCodec handles ALL decoding (JPEG, JPEG2000, deflate, LZW, raw) +#include "cuslide/nvimgcodec/nvimgcodec_decoder.h" +#include "cuslide/nvimgcodec/nvimgcodec_tiff_parser.h" +#include "tiff.h" +#include "tiff_constants.h" + + +namespace cuslide::tiff +{ + +// OLD CONSTRUCTOR: libtiff-based (DEPRECATED - use nvImageCodec constructor instead) +// This constructor is kept for API compatibility but is not functional in pure nvImageCodec build +IFD::IFD(TIFF* tiff, uint16_t index, ifd_offset_t offset) : tiff_(tiff), ifd_index_(index), ifd_offset_(offset) +{ +#ifdef CUCIM_HAS_NVIMGCODEC + // Pure nvImageCodec path: try to use IfdInfo instead + if (tiff->nvimgcodec_parser_ && tiff->nvimgcodec_parser_->is_valid()) + { + if (static_cast(index) < tiff->nvimgcodec_parser_->get_ifd_count()) + { + const auto& ifd_info = tiff->nvimgcodec_parser_->get_ifd(static_cast(index)); + + // Initialize from IfdInfo + width_ = ifd_info.width; + height_ = ifd_info.height; + samples_per_pixel_ = ifd_info.num_channels; + bits_per_sample_ = ifd_info.bits_per_sample; + + // Parse codec to compression + compression_ = parse_codec_to_compression(ifd_info.codec); + codec_name_ = ifd_info.codec; + + // Assume tiled if tile dimensions are provided in IfdInfo (check nvImageCodec metadata) + // For now, use a heuristic: most whole-slide images are tiled + tile_width_ = 256; // Default tile size (can be overridden from IfdInfo metadata) + tile_height_ = 256; + + // nvImageCodec members + nvimgcodec_sub_stream_ = ifd_info.sub_code_stream; + + // Calculate hash value + hash_value_ = tiff->file_handle_shared_.get()->hash_value ^ cucim::codec::splitmix64(index); + + #ifdef DEBUG + fmt::print(" IFD[{}]: Initialized from nvImageCodec ({}x{}, codec: {})\n", + index, width_, height_, codec_name_); + #endif + return; + } + } + + // Fallback: throw error if nvImageCodec parser not available + throw std::runtime_error(fmt::format( + "IFD constructor (offset-based) requires libtiff, which is not available in pure nvImageCodec build. " + "Use IFD(TIFF*, uint16_t, IfdInfo&) constructor instead.")); +#else + // If nvImageCodec not available, this should never be called + throw std::runtime_error("Pure nvImageCodec build requires CUCIM_HAS_NVIMGCODEC"); +#endif +} + +// ============================================================================ +// NEW PRIMARY CONSTRUCTOR: nvImageCodec-Only (No libtiff) +// ============================================================================ + +#ifdef CUCIM_HAS_NVIMGCODEC +IFD::IFD(TIFF* tiff, uint16_t index, const cuslide2::nvimgcodec::IfdInfo& ifd_info) + : tiff_(tiff), ifd_index_(index), ifd_offset_(index) +{ + PROF_SCOPED_RANGE(PROF_EVENT(ifd_ifd)); // Use standard ifd_ifd profiler event + + #ifdef DEBUG + fmt::print("🔧 Creating IFD[{}] from nvImageCodec metadata\n", index); + #endif + + // Extract basic image properties from IfdInfo + width_ = ifd_info.width; + height_ = ifd_info.height; + samples_per_pixel_ = ifd_info.num_channels; + bits_per_sample_ = ifd_info.bits_per_sample; + + #ifdef DEBUG + fmt::print(" Dimensions: {}x{}, {} channels, {} bits/sample\n", + width_, height_, samples_per_pixel_, bits_per_sample_); + #endif + + // Parse codec string to compression enum + codec_name_ = ifd_info.codec; + compression_ = parse_codec_to_compression(codec_name_); + #ifdef DEBUG + fmt::print(" Codec: {} (compression={})\n", codec_name_, compression_); + #endif + + // Get ImageDescription from nvImageCodec + image_description_ = ifd_info.image_description; + + // Extract TIFF tags from TiffFileParser + if (tiff->nvimgcodec_parser_) { + // Software and Model tags + software_ = tiff->nvimgcodec_parser_->get_tiff_tag(index, "Software"); + model_ = tiff->nvimgcodec_parser_->get_tiff_tag(index, "Model"); + + // SUBFILETYPE for IFD classification + int subfile_type = tiff->nvimgcodec_parser_->get_subfile_type(index); + if (subfile_type >= 0) { + subfile_type_ = static_cast(subfile_type); + #ifdef DEBUG + fmt::print(" SUBFILETYPE: {}\n", subfile_type_); + #endif + } + + // Check for JPEGTables (abbreviated JPEG indicator) + std::string jpeg_tables = tiff->nvimgcodec_parser_->get_tiff_tag(index, "JPEGTables"); + if (!jpeg_tables.empty()) { + #ifdef DEBUG + fmt::print(" ✅ JPEGTables detected (abbreviated JPEG)\n"); + #endif + } + + // Tile dimensions (if available from TIFF tags) + std::string tile_w_str = tiff->nvimgcodec_parser_->get_tiff_tag(index, "TileWidth"); + std::string tile_h_str = tiff->nvimgcodec_parser_->get_tiff_tag(index, "TileLength"); + + if (!tile_w_str.empty() && !tile_h_str.empty()) { + try { + tile_width_ = std::stoul(tile_w_str); + tile_height_ = std::stoul(tile_h_str); + #ifdef DEBUG + fmt::print(" Tiles: {}x{}\n", tile_width_, tile_height_); + #endif + } catch (...) { + #ifdef DEBUG + fmt::print(" ⚠️ Failed to parse tile dimensions\n"); + #endif + tile_width_ = 0; + tile_height_ = 0; + } + } else { + // Not tiled - treat as single strip + tile_width_ = 0; + tile_height_ = 0; + #ifdef DEBUG + fmt::print(" Not tiled (strip-based or whole image)\n"); + #endif + } + } + + // Set format defaults + planar_config_ = PLANARCONFIG_CONTIG; // nvImageCodec outputs interleaved + photometric_ = PHOTOMETRIC_RGB; + predictor_ = 1; // No predictor + + // Resolution info (defaults - may not be available from nvImageCodec) + resolution_unit_ = 1; // No absolute unit + x_resolution_ = 1.0f; + y_resolution_ = 1.0f; + + // Calculate hash for caching + hash_value_ = cucim::codec::splitmix64(index); + +#ifdef CUCIM_HAS_NVIMGCODEC + // Store reference to nvImageCodec sub-stream + nvimgcodec_sub_stream_ = ifd_info.sub_code_stream; + #ifdef DEBUG + fmt::print(" ✅ nvImageCodec sub-stream: {}\n", + static_cast(nvimgcodec_sub_stream_)); + #endif +#endif + + #ifdef DEBUG + fmt::print("✅ IFD[{}] initialization complete\n", index); + #endif +} +#endif // CUCIM_HAS_NVIMGCODEC + +IFD::~IFD() +{ +#ifdef CUCIM_HAS_NVIMGCODEC + // NOTE: nvimgcodec_sub_stream_ is NOT owned by IFD - it's a borrowed pointer + // from TiffFileParser's ifd_infos_ vector. TiffFileParser::~TiffFileParser() + // destroys all sub-code streams before IFD destructors run. + // DO NOT call nvimgcodecCodeStreamDestroy here - just clear the pointer. + nvimgcodec_sub_stream_ = nullptr; +#endif +} + +bool IFD::read([[maybe_unused]] const TIFF* tiff, + [[maybe_unused]] const cucim::io::format::ImageMetadataDesc* metadata, + [[maybe_unused]] const cucim::io::format::ImageReaderRegionRequestDesc* request, + [[maybe_unused]] cucim::io::format::ImageDataDesc* out_image_data) +{ + PROF_SCOPED_RANGE(PROF_EVENT(ifd_read)); + + #ifdef DEBUG + fmt::print("🎯 IFD::read() ENTRY: IFD[{}], location=({}, {}), size={}x{}, device={}\n", + ifd_index_, + request->location[0], request->location[1], + request->size[0], request->size[1], + request->device); + #endif + +#ifdef CUCIM_HAS_NVIMGCODEC + // Fast path: Use nvImageCodec ROI decoding when available + // ROI decoding is supported in nvImageCodec v0.6.0+ for JPEG2000 + // Falls back to tile-based decoding if ROI decode fails + if (nvimgcodec_sub_stream_ && tiff->nvimgcodec_parser_ && + request->location_len == 1 && request->batch_size == 1) + { + std::string device_name(request->device); + if (request->shm_name) + { + device_name = device_name + fmt::format("[{}]", request->shm_name); + } + cucim::io::Device out_device(device_name); + + int64_t sx = request->location[0]; + int64_t sy = request->location[1]; + int64_t w = request->size[0]; + int64_t h = request->size[1]; + + // Output buffer - decode function will allocate (uses pinned memory for CPU) + uint8_t* output_buffer = nullptr; + DLTensor* out_buf = request->buf; + bool is_buf_available = out_buf && out_buf->data; + + if (is_buf_available) + { + // User provided pre-allocated buffer + output_buffer = static_cast(out_buf->data); + } + // Note: decode_ifd_region_nvimgcodec will allocate buffer if output_buffer is nullptr + + // Get IFD info from TiffFileParser + const auto& ifd_info = tiff->nvimgcodec_parser_->get_ifd(static_cast(ifd_index_)); + + // Call nvImageCodec ROI decoder + bool success = cuslide2::nvimgcodec::decode_ifd_region_nvimgcodec( + ifd_info, + tiff->nvimgcodec_parser_->get_main_code_stream(), + sx, sy, w, h, + &output_buffer, + out_device); + + if (success) + { + #ifdef DEBUG + fmt::print("✅ nvImageCodec ROI decode successful: {}x{} at ({}, {})\n", w, h, sx, sy); + #endif + + // Set up output metadata + out_image_data->container.data = output_buffer; + out_image_data->container.device = DLDevice{ static_cast(out_device.type()), out_device.index() }; + out_image_data->container.dtype = DLDataType{ kDLUInt, 8, 1 }; + out_image_data->container.ndim = 3; + out_image_data->container.shape = static_cast(cucim_malloc(3 * sizeof(int64_t))); + out_image_data->container.shape[0] = h; + out_image_data->container.shape[1] = w; + out_image_data->container.shape[2] = samples_per_pixel_; + out_image_data->container.strides = nullptr; + out_image_data->container.byte_offset = 0; + + return true; + } + else + { + + #ifdef DEBUG + fmt::print("❌ nvImageCodec ROI decode failed for IFD[{}]\n", ifd_index_); + #endif + + // Free allocated buffer on failure + // Note: decode function uses cudaMallocHost for CPU (pinned memory) + if (!is_buf_available && output_buffer) + { + if (out_device.type() == cucim::io::DeviceType::kCUDA) + { + cudaFree(output_buffer); + } + else + { + cudaFreeHost(output_buffer); // Pinned memory + } + } + + throw std::runtime_error(fmt::format( + "Failed to decode IFD[{}] with nvImageCodec. ROI: ({},{}) {}x{}", + ifd_index_, sx, sy, w, h)); + } + } +#endif + + // If we reach here, nvImageCodec is not available or request doesn't match fast path + #ifdef DEBUG + fmt::print("❌ Cannot decode: nvImageCodec not available or unsupported request type\n"); +#ifdef CUCIM_HAS_NVIMGCODEC + fmt::print(" nvimgcodec_sub_stream_={}, location_len={}, batch_size={}\n", + static_cast(nvimgcodec_sub_stream_), request->location_len, request->batch_size); +#else + fmt::print(" location_len={}, batch_size={}\n", + request->location_len, request->batch_size); +#endif + #endif + throw std::runtime_error(fmt::format( + "IFD[{}]: This library requires nvImageCodec for image decoding. " + "Multi-location/batch requests not yet supported.", ifd_index_)); +} + +uint32_t IFD::index() const +{ + return ifd_index_; +} +ifd_offset_t IFD::offset() const +{ + return ifd_offset_; +} + +std::string& IFD::software() +{ + return software_; +} +std::string& IFD::model() +{ + return model_; +} +std::string& IFD::image_description() +{ + return image_description_; +} +uint16_t IFD::resolution_unit() const +{ + return resolution_unit_; +} +float IFD::x_resolution() const +{ + return x_resolution_; +} +float IFD::y_resolution() const +{ + return y_resolution_; +} +uint32_t IFD::width() const +{ + return width_; +} +uint32_t IFD::height() const +{ + return height_; +} +uint32_t IFD::tile_width() const +{ + return tile_width_; +} +uint32_t IFD::tile_height() const +{ + return tile_height_; +} +uint32_t IFD::rows_per_strip() const +{ + return rows_per_strip_; +} +uint32_t IFD::bits_per_sample() const +{ + return bits_per_sample_; +} +uint32_t IFD::samples_per_pixel() const +{ + return samples_per_pixel_; +} +uint64_t IFD::subfile_type() const +{ + return subfile_type_; +} +uint16_t IFD::planar_config() const +{ + return planar_config_; +} +uint16_t IFD::photometric() const +{ + return photometric_; +} +uint16_t IFD::compression() const +{ + return compression_; +} +uint16_t IFD::predictor() const +{ + return predictor_; +} + +uint16_t IFD::subifd_count() const +{ + return subifd_count_; +} +std::vector& IFD::subifd_offsets() +{ + return subifd_offsets_; +} +uint32_t IFD::image_piece_count() const +{ + return image_piece_count_; +} +const std::vector& IFD::image_piece_offsets() const +{ + return image_piece_offsets_; +} +const std::vector& IFD::image_piece_bytecounts() const +{ + return image_piece_bytecounts_; +} + +size_t IFD::pixel_size_nbytes() const +{ + // Calculate pixel size based on bits_per_sample and samples_per_pixel + // Most whole-slide images are 8-bit RGB (3 bytes per pixel) + const size_t bytes_per_sample = (bits_per_sample_ + 7) / 8; // Round up to nearest byte + const size_t nbytes = bytes_per_sample * samples_per_pixel_; + return nbytes; +} + +size_t IFD::tile_raster_size_nbytes() const +{ + const size_t nbytes = tile_width_ * tile_height_ * pixel_size_nbytes(); + return nbytes; +} + +// ============================================================================ +// Helper: Parse nvImageCodec Codec String to TIFF Compression Enum +// ============================================================================ + +#ifdef CUCIM_HAS_NVIMGCODEC +uint16_t IFD::parse_codec_to_compression(const std::string& codec) +{ + // Map nvImageCodec codec strings to TIFF compression constants + if (codec == "jpeg") { + return COMPRESSION_JPEG; // 7 + } + if (codec == "jpeg2000" || codec == "jpeg2k" || codec == "j2k") { + // Default to YCbCr JPEG2000 (most common in whole-slide imaging) + return COMPRESSION_APERIO_JP2K_YCBCR; // 33003 + } + if (codec == "lzw") { + return COMPRESSION_LZW; // 5 + } + if (codec == "deflate" || codec == "zip") { + return COMPRESSION_DEFLATE; // 8 + } + if (codec == "adobe-deflate") { + return COMPRESSION_ADOBE_DEFLATE; // 32946 + } + if (codec == "none" || codec == "uncompressed" || codec.empty()) { + return COMPRESSION_NONE; // 1 + } + + // Handle generic 'tiff' codec from nvImageCodec 0.6.0 + // This is a known limitation where nvImageCodec doesn't expose the actual compression + // For now, default to JPEG which is most common in whole-slide imaging + if (codec == "tiff") { + #ifdef DEBUG + fmt::print("ℹ️ nvImageCodec returned generic 'tiff' codec, assuming JPEG compression\n"); + #endif + return COMPRESSION_JPEG; // 7 - Most common for WSI (Aperio, Philips, etc.) + } + + // Unknown codec - log warning and default to JPEG (safer than NONE for WSI) + #ifdef DEBUG + fmt::print("⚠️ Unknown codec '{}', defaulting to COMPRESSION_JPEG\n", codec); + #endif + return COMPRESSION_JPEG; // 7 - WSI files rarely use uncompressed +} +#endif // CUCIM_HAS_NVIMGCODEC + +bool IFD::is_compression_supported() const +{ + switch (compression_) + { + case COMPRESSION_NONE: + case COMPRESSION_JPEG: + case COMPRESSION_ADOBE_DEFLATE: + case COMPRESSION_DEFLATE: + case COMPRESSION_APERIO_JP2K_YCBCR: // 33003: Jpeg 2000 with YCbCr format + case COMPRESSION_APERIO_JP2K_RGB: // 33005: Jpeg 2000 with RGB + case COMPRESSION_LZW: + return true; + default: + return false; + } +} + +bool IFD::is_read_optimizable() const +{ + return is_compression_supported() && bits_per_sample_ == 8 && samples_per_pixel_ == 3 && + (tile_width_ != 0 && tile_height_ != 0) && planar_config_ == PLANARCONFIG_CONTIG && + (photometric_ == PHOTOMETRIC_RGB || photometric_ == PHOTOMETRIC_YCBCR) && + !tiff_->is_in_read_config(TIFF::kUseLibTiff); +} + +bool IFD::is_format_supported() const +{ + return is_compression_supported(); +} + +bool IFD::read_region_tiles(const TIFF* tiff, + const IFD* ifd, + const int64_t* location, + const int64_t location_index, + const int64_t w, + const int64_t h, + void* raster, + const cucim::io::Device& out_device, + cucim::loader::ThreadBatchDataLoader* loader) +{ + #ifdef DEBUG + fmt::print("🔍 read_region_tiles: ENTRY - location_index={}, w={}, h={}, loader={}\n", + location_index, w, h, static_cast(loader)); + #endif + PROF_SCOPED_RANGE(PROF_EVENT(ifd_read_region_tiles)); + // Reference code: https://github.com/libjpeg-turbo/libjpeg-turbo/blob/master/tjexample.c + + int64_t sx = location[location_index * 2]; + int64_t sy = location[location_index * 2 + 1]; + int64_t ex = sx + w - 1; + int64_t ey = sy + h - 1; + #ifdef DEBUG + fmt::print("🔍 read_region_tiles: Region bounds - sx={}, sy={}, ex={}, ey={}\n", sx, sy, ex, ey); + #endif + + uint32_t width = ifd->width_; + uint32_t height = ifd->height_; + + // Handle out-of-boundary case + if (sx < 0 || sy < 0 || sx >= width || sy >= height || ex < 0 || ey < 0 || ex >= width || ey >= height) + { + return read_region_tiles_boundary(tiff, ifd, location, location_index, w, h, raster, out_device, loader); + } + cucim::cache::ImageCache& image_cache = cucim::CuImage::cache_manager().cache(); + cucim::cache::CacheType cache_type = image_cache.type(); + + uint8_t background_value = tiff->background_value_; + uint16_t compression_method = ifd->compression_; + int jpeg_color_space = ifd->jpeg_color_space_; + int predictor = ifd->predictor_; + + // TODO: revert this once we can get RGB data instead of RGBA + uint32_t samples_per_pixel = 3; // ifd->samples_per_pixel(); + + const void* jpegtable_data = ifd->jpegtable_.data(); + uint32_t jpegtable_count = ifd->jpegtable_.size(); + + uint32_t tw = ifd->tile_width_; + uint32_t th = ifd->tile_height_; + + uint32_t offset_sx = static_cast(sx / tw); // x-axis start offset for the requested region in the ifd tile + // array as grid + uint32_t offset_ex = static_cast(ex / tw); // x-axis end offset for the requested region in the ifd tile + // array as grid + uint32_t offset_sy = static_cast(sy / th); // y-axis start offset for the requested region in the ifd tile + // array as grid + uint32_t offset_ey = static_cast(ey / th); // y-axis end offset for the requested region in the ifd tile + // array as grid + + uint32_t pixel_offset_sx = static_cast(sx % tw); + uint32_t pixel_offset_ex = static_cast(ex % tw); + uint32_t pixel_offset_sy = static_cast(sy % th); + uint32_t pixel_offset_ey = static_cast(ey % th); + + uint32_t stride_y = width / tw + !!(width % tw); // # of tiles in a row(y) in the ifd tile array as grid + + uint32_t start_index_y = offset_sy * stride_y; + uint32_t end_index_y = offset_ey * stride_y; + + const size_t tile_raster_nbytes = ifd->tile_raster_size_nbytes(); + + int tiff_file = tiff->file_handle_shared_.get()->fd; + uint64_t ifd_hash_value = ifd->hash_value_; + uint32_t dest_pixel_step_y = w * samples_per_pixel; + + uint32_t nbytes_tw = tw * samples_per_pixel; + auto dest_start_ptr = static_cast(raster); + + // TODO: Current implementation doesn't consider endianness so need to consider later + // TODO: Consider tile's depth tag. + for (uint32_t index_y = start_index_y; index_y <= end_index_y; index_y += stride_y) + { + uint32_t tile_pixel_offset_sy = (index_y == start_index_y) ? pixel_offset_sy : 0; + uint32_t tile_pixel_offset_ey = (index_y == end_index_y) ? pixel_offset_ey : (th - 1); + uint32_t dest_pixel_offset_len_y = tile_pixel_offset_ey - tile_pixel_offset_sy + 1; + + uint32_t dest_pixel_index_x = 0; + + uint32_t index = index_y + offset_sx; + for (uint32_t offset_x = offset_sx; offset_x <= offset_ex; ++offset_x, ++index) + { + #ifdef DEBUG + fmt::print("🔍 read_region_tiles: Processing tile index={}, offset_x={}\n", index, offset_x); + #endif + PROF_SCOPED_RANGE(PROF_EVENT_P(ifd_read_region_tiles_iter, index)); + auto tiledata_offset = static_cast(ifd->image_piece_offsets_[index]); + auto tiledata_size = static_cast(ifd->image_piece_bytecounts_[index]); + #ifdef DEBUG + fmt::print("🔍 read_region_tiles: tile_offset={}, tile_size={}\n", tiledata_offset, tiledata_size); + #endif + + // Calculate a simple hash value for the tile index + uint64_t index_hash = ifd_hash_value ^ (static_cast(index) | (static_cast(index) << 32)); + + uint32_t tile_pixel_offset_x = (offset_x == offset_sx) ? pixel_offset_sx : 0; + uint32_t nbytes_tile_pixel_size_x = (offset_x == offset_ex) ? + (pixel_offset_ex - tile_pixel_offset_x + 1) * samples_per_pixel : + (tw - tile_pixel_offset_x) * samples_per_pixel; + #ifdef DEBUG + fmt::print("🔍 read_region_tiles: About to create decode_func lambda\n"); + fflush(stdout); + #endif + + // Capture device type as integer to avoid copying Device object + auto device_type_int = static_cast(out_device.type()); + auto device_index = out_device.index(); + + // Create a struct to hold all data - avoids large lambda captures + struct TileDecodeData { + uint32_t index; + uint64_t index_hash; + uint16_t compression_method; + uint64_t tiledata_offset; + uint64_t tiledata_size; + uint32_t tile_pixel_offset_sy, tile_pixel_offset_ey, tile_pixel_offset_x; + uint32_t tw, th, samples_per_pixel, nbytes_tw, nbytes_tile_pixel_size_x; + uint32_t dest_pixel_index_x; + uint8_t* dest_start_ptr; + uint32_t dest_pixel_step_y; + int tiff_file; + uint64_t ifd_hash_value; + size_t tile_raster_nbytes; + cucim::cache::CacheType cache_type; + const void* jpegtable_data; + uint32_t jpegtable_count; + int jpeg_color_space; + uint8_t background_value; + int predictor; + int device_type_int; + int16_t device_index; + cucim::loader::ThreadBatchDataLoader* loader; + }; + + auto data = std::make_shared(); + data->index = index; + data->index_hash = index_hash; + data->compression_method = compression_method; + data->tiledata_offset = tiledata_offset; + data->tiledata_size = tiledata_size; + data->tile_pixel_offset_sy = tile_pixel_offset_sy; + data->tile_pixel_offset_ey = tile_pixel_offset_ey; + data->tile_pixel_offset_x = tile_pixel_offset_x; + data->tw = tw; + data->th = th; + data->samples_per_pixel = samples_per_pixel; + data->nbytes_tw = nbytes_tw; + data->nbytes_tile_pixel_size_x = nbytes_tile_pixel_size_x; + data->dest_pixel_index_x = dest_pixel_index_x; + data->dest_start_ptr = dest_start_ptr; + data->dest_pixel_step_y = dest_pixel_step_y; + data->tiff_file = tiff_file; + data->ifd_hash_value = ifd_hash_value; + data->tile_raster_nbytes = tile_raster_nbytes; + data->cache_type = cache_type; + data->jpegtable_data = jpegtable_data; + data->jpegtable_count = jpegtable_count; + data->jpeg_color_space = jpeg_color_space; + data->background_value = background_value; + data->predictor = predictor; + data->device_type_int = device_type_int; + data->device_index = device_index; + data->loader = loader; + + // Small lambda that only captures shared_ptr - cheap to copy! + auto decode_func = [data]() { + // FIRST THING - print before ANY other code + #ifdef DEBUG + fmt::print("🔍🔍🔍 decode_func: LAMBDA INVOKED! index={}\n", data->index); + fflush(stdout); + #endif + + // Extract all data to local variables to avoid repeated data-> access + auto index = data->index; + auto index_hash = data->index_hash; + auto compression_method = data->compression_method; + auto tiledata_offset = data->tiledata_offset; + auto tiledata_size = data->tiledata_size; + auto tile_pixel_offset_sy = data->tile_pixel_offset_sy; + auto tile_pixel_offset_ey = data->tile_pixel_offset_ey; + auto tile_pixel_offset_x = data->tile_pixel_offset_x; + auto tw = data->tw; + [[maybe_unused]] auto th = data->th; + auto samples_per_pixel = data->samples_per_pixel; + auto nbytes_tw = data->nbytes_tw; + auto nbytes_tile_pixel_size_x = data->nbytes_tile_pixel_size_x; + auto dest_pixel_index_x = data->dest_pixel_index_x; + auto dest_start_ptr = data->dest_start_ptr; + auto dest_pixel_step_y = data->dest_pixel_step_y; + // REMOVED: Legacy CPU decoder variables (unused after removing CPU decoder code) + // auto tiff_file = data->tiff_file; + auto ifd_hash_value = data->ifd_hash_value; + auto tile_raster_nbytes = data->tile_raster_nbytes; + auto cache_type = data->cache_type; + // REMOVED: Legacy CPU decoder variables (unused after removing CPU decoder code) + // auto jpegtable_data = data->jpegtable_data; + // auto jpegtable_count = data->jpegtable_count; + // auto jpeg_color_space = data->jpeg_color_space; + // auto predictor = data->predictor; + auto background_value = data->background_value; + auto loader = data->loader; + + // Reconstruct Device object inside lambda to avoid copying issues + cucim::io::Device out_device(static_cast(data->device_type_int), data->device_index); + try { + #ifdef DEBUG + fmt::print("🔍 decode_func: START - index={}, compression={}, tiledata_offset={}, tiledata_size={}\n", + index, compression_method, tiledata_offset, tiledata_size); + fflush(stdout); + #endif + PROF_SCOPED_RANGE(PROF_EVENT_P(ifd_read_region_tiles_task, index_hash)); + + // Get image cache directly instead of capturing by reference + #ifdef DEBUG + fmt::print("🔍 decode_func: Getting image cache...\n"); + fflush(stdout); + #endif + cucim::cache::ImageCache& image_cache = cucim::CuImage::cache_manager().cache(); + #ifdef DEBUG + fmt::print("🔍 decode_func: Got image cache\n"); + fflush(stdout); + #endif + + uint32_t nbytes_tile_index = (tile_pixel_offset_sy * tw + tile_pixel_offset_x) * samples_per_pixel; + uint32_t dest_pixel_index = dest_pixel_index_x; + uint8_t* tile_data = nullptr; + if (tiledata_size > 0) + { + #ifdef DEBUG + fmt::print("🔍 decode_func: tiledata_size > 0, entering decode path\n"); + #endif + std::unique_ptr tile_raster = + std::unique_ptr(nullptr, cucim_free); + + if (loader && loader->batch_data_processor()) + { + switch (compression_method) + { + case COMPRESSION_JPEG: + case COMPRESSION_APERIO_JP2K_YCBCR: // 33003 + case COMPRESSION_APERIO_JP2K_RGB: // 33005 + break; + default: + throw std::runtime_error("Unsupported compression method"); + } + auto value = loader->wait_for_processing(index); + if (!value) // if shutdown + { + return; + } + tile_data = static_cast(value->data); + + cudaError_t cuda_status; + CUDA_ERROR(cudaMemcpy2D(dest_start_ptr + dest_pixel_index, dest_pixel_step_y, + tile_data + nbytes_tile_index, nbytes_tw, nbytes_tile_pixel_size_x, + tile_pixel_offset_ey - tile_pixel_offset_sy + 1, + cudaMemcpyDeviceToDevice)); + } + else + { + auto key = image_cache.create_key(ifd_hash_value, index); + image_cache.lock(index_hash); + auto value = image_cache.find(key); + if (value) + { + image_cache.unlock(index_hash); + tile_data = static_cast(value->data); + } + else + { + // Lifetime of tile_data is same with `value` + // : do not access this data when `value` is not accessible. + if (cache_type != cucim::cache::CacheType::kNoCache) + { + tile_data = static_cast(image_cache.allocate(tile_raster_nbytes)); + } + else + { + // Allocate temporary buffer for tile data + tile_raster = std::unique_ptr( + reinterpret_cast(cucim_malloc(tile_raster_nbytes)), cucim_free); + tile_data = tile_raster.get(); + } + { + // REMOVED: Legacy CPU decoder fallback code + // This code path should NOT be reached in a pure nvImageCodec build. + // All decoding should go through the nvImageCodec ROI path (lines 219-276). + // If you see this error, investigate why ROI decode failed. + throw std::runtime_error(fmt::format( + "INTERNAL ERROR: Tile-based CPU decoder fallback reached. " + "This should not happen in nvImageCodec build. " + "Compression method: {}, tile offset: {}, size: {}", + compression_method, tiledata_offset, tiledata_size)); + } + + value = image_cache.create_value(tile_data, tile_raster_nbytes); + image_cache.insert(key, value); + image_cache.unlock(index_hash); + } + + for (uint32_t ty = tile_pixel_offset_sy; ty <= tile_pixel_offset_ey; + ++ty, dest_pixel_index += dest_pixel_step_y, nbytes_tile_index += nbytes_tw) + { + memcpy(dest_start_ptr + dest_pixel_index, tile_data + nbytes_tile_index, + nbytes_tile_pixel_size_x); + } + } + } + else + { + if (out_device.type() == cucim::io::DeviceType::kCPU) + { + for (uint32_t ty = tile_pixel_offset_sy; ty <= tile_pixel_offset_ey; + ++ty, dest_pixel_index += dest_pixel_step_y, nbytes_tile_index += nbytes_tw) + { + // Set background value such as (255,255,255) + memset(dest_start_ptr + dest_pixel_index, background_value, nbytes_tile_pixel_size_x); + } + } + else + { + cudaError_t cuda_status; + CUDA_ERROR(cudaMemset2D(dest_start_ptr + dest_pixel_index, dest_pixel_step_y, background_value, + nbytes_tile_pixel_size_x, + tile_pixel_offset_ey - tile_pixel_offset_sy + 1)); + } + } + } catch (const std::exception& e) { + #ifdef DEBUG + fmt::print("❌ decode_func: Exception caught: {}\n", e.what()); + #endif + throw; + } catch (...) { + #ifdef DEBUG + fmt::print("❌ decode_func: Unknown exception caught\n"); + #endif + throw; + } + }; + + #ifdef DEBUG + fmt::print("🔍 read_region_tiles: decode_func lambda created\n"); + #endif + + // TEMPORARY: Force single-threaded execution to test if decode works + bool force_single_threaded = true; + + if (force_single_threaded || !loader || !(*loader)) + { + #ifdef DEBUG + fmt::print("🔍 read_region_tiles: Executing decode_func directly (FORCED SINGLE-THREADED TEST)\n"); + fflush(stdout); + #endif + decode_func(); + #ifdef DEBUG + fmt::print("🔍 read_region_tiles: decode_func completed successfully!\n"); + fflush(stdout); + #endif + } + else + { + #ifdef DEBUG + fmt::print("🔍 read_region_tiles: Enqueueing task for tile index={}\n", index); + #endif + loader->enqueue(std::move(decode_func), + cucim::loader::TileInfo{ location_index, index, tiledata_offset, tiledata_size }); + #ifdef DEBUG + fmt::print("🔍 read_region_tiles: Task enqueued\n"); + #endif + } + + dest_pixel_index_x += nbytes_tile_pixel_size_x; + } + dest_start_ptr += dest_pixel_step_y * dest_pixel_offset_len_y; + } + + return true; +} + +bool IFD::read_region_tiles_boundary(const TIFF* tiff, + const IFD* ifd, + const int64_t* location, + const int64_t location_index, + const int64_t w, + const int64_t h, + void* raster, + const cucim::io::Device& out_device, + cucim::loader::ThreadBatchDataLoader* loader) +{ + PROF_SCOPED_RANGE(PROF_EVENT(ifd_read_region_tiles_boundary)); + (void)out_device; + // Reference code: https://github.com/libjpeg-turbo/libjpeg-turbo/blob/master/tjexample.c + int64_t sx = location[location_index * 2]; + int64_t sy = location[location_index * 2 + 1]; + + uint8_t background_value = tiff->background_value_; + uint16_t compression_method = ifd->compression_; + int jpeg_color_space = ifd->jpeg_color_space_; + int predictor = ifd->predictor_; + + int64_t ex = sx + w - 1; + int64_t ey = sy + h - 1; + + uint32_t width = ifd->width_; + uint32_t height = ifd->height_; + + // Memory for tile_raster would be manually allocated here, instead of using decode_libjpeg(). + // Need to free the manually. Usually it is set to nullptr and memory is created by decode_libjpeg() by using + // tjAlloc() (Also need to free with tjFree() after use. See the documentation of tjAlloc() for the detail.) + const int pixel_size_nbytes = ifd->pixel_size_nbytes(); + auto dest_start_ptr = static_cast(raster); + + bool is_out_of_image = (ex < 0 || width <= sx || ey < 0 || height <= sy); + if (is_out_of_image) + { + // Fill background color(255,255,255) and return + memset(dest_start_ptr, background_value, w * h * pixel_size_nbytes); + return true; + } + cucim::cache::ImageCache& image_cache = cucim::CuImage::cache_manager().cache(); + cucim::cache::CacheType cache_type = image_cache.type(); + + uint32_t tw = ifd->tile_width_; + uint32_t th = ifd->tile_height_; + + const size_t tile_raster_nbytes = tw * th * pixel_size_nbytes; + + // TODO: revert this once we can get RGB data instead of RGBA + uint32_t samples_per_pixel = 3; // ifd->samples_per_pixel(); + + const void* jpegtable_data = ifd->jpegtable_.data(); + uint32_t jpegtable_count = ifd->jpegtable_.size(); + + bool sx_in_range = (sx >= 0 && sx < width); + bool ex_in_range = (ex >= 0 && ex < width); + bool sy_in_range = (sy >= 0 && sy < height); + bool ey_in_range = (ey >= 0 && ey < height); + + int64_t offset_boundary_x = (static_cast(width) - 1) / tw; + int64_t offset_boundary_y = (static_cast(height) - 1) / th; + + int64_t offset_sx = sx / tw; // x-axis start offset for the requested region in the + // ifd tile array as grid + + int64_t offset_ex = ex / tw; // x-axis end offset for the requested region in the + // ifd tile array as grid + + int64_t offset_sy = sy / th; // y-axis start offset for the requested region in the + // ifd tile array as grid + int64_t offset_ey = ey / th; // y-axis end offset for the requested region in the + // ifd tile array as grid + int64_t pixel_offset_sx = (sx % tw); + int64_t pixel_offset_ex = (ex % tw); + int64_t pixel_offset_sy = (sy % th); + int64_t pixel_offset_ey = (ey % th); + int64_t pixel_offset_boundary_x = ((width - 1) % tw); + int64_t pixel_offset_boundary_y = ((height - 1) % th); + + // Make sure that division and modulo has same value with Python's one (e.g., making -1 / 3 == -1 instead of 0) + if (pixel_offset_sx < 0) + { + pixel_offset_sx += tw; + --offset_sx; + } + if (pixel_offset_ex < 0) + { + pixel_offset_ex += tw; + --offset_ex; + } + if (pixel_offset_sy < 0) + { + pixel_offset_sy += th; + --offset_sy; + } + if (pixel_offset_ey < 0) + { + pixel_offset_ey += th; + --offset_ey; + } + int64_t offset_min_x = sx_in_range ? offset_sx : 0; + int64_t offset_max_x = ex_in_range ? offset_ex : offset_boundary_x; + int64_t offset_min_y = sy_in_range ? offset_sy : 0; + int64_t offset_max_y = ey_in_range ? offset_ey : offset_boundary_y; + + uint32_t stride_y = width / tw + !!(width % tw); // # of tiles in a row(y) in the ifd tile array as grid + + int64_t start_index_y = offset_sy * stride_y; + int64_t start_index_min_y = offset_min_y * stride_y; + int64_t end_index_y = offset_ey * stride_y; + int64_t end_index_max_y = offset_max_y * stride_y; + int64_t boundary_index_y = offset_boundary_y * stride_y; + + + int tiff_file = tiff->file_handle_shared_.get()->fd; + uint64_t ifd_hash_value = ifd->hash_value_; + + uint32_t dest_pixel_step_y = w * samples_per_pixel; + uint32_t nbytes_tw = tw * samples_per_pixel; + + + // TODO: Current implementation doesn't consider endianness so need to consider later + // TODO: Consider tile's depth tag. + // TODO: update the type of variables (index, index_y) : other function uses uint32_t + for (int64_t index_y = start_index_y; index_y <= end_index_y; index_y += stride_y) + { + uint32_t tile_pixel_offset_sy = (index_y == start_index_y) ? pixel_offset_sy : 0; + uint32_t tile_pixel_offset_ey = (index_y == end_index_y) ? pixel_offset_ey : (th - 1); + uint32_t dest_pixel_offset_len_y = tile_pixel_offset_ey - tile_pixel_offset_sy + 1; + + uint32_t dest_pixel_index_x = 0; + + int64_t index = index_y + offset_sx; + for (int64_t offset_x = offset_sx; offset_x <= offset_ex; ++offset_x, ++index) + { + PROF_SCOPED_RANGE(PROF_EVENT_P(ifd_read_region_tiles_boundary_iter, index)); + uint64_t tiledata_offset = 0; + uint64_t tiledata_size = 0; + + // Calculate a simple hash value for the tile index + uint64_t index_hash = ifd_hash_value ^ (static_cast(index) | (static_cast(index) << 32)); + + if (offset_x >= offset_min_x && offset_x <= offset_max_x && index_y >= start_index_min_y && + index_y <= end_index_max_y) + { + tiledata_offset = static_cast(ifd->image_piece_offsets_[index]); + tiledata_size = static_cast(ifd->image_piece_bytecounts_[index]); + } + + uint32_t tile_pixel_offset_x = (offset_x == offset_sx) ? pixel_offset_sx : 0; + uint32_t nbytes_tile_pixel_size_x = (offset_x == offset_ex) ? + (pixel_offset_ex - tile_pixel_offset_x + 1) * samples_per_pixel : + (tw - tile_pixel_offset_x) * samples_per_pixel; + + uint32_t nbytes_tile_index_orig = (tile_pixel_offset_sy * tw + tile_pixel_offset_x) * samples_per_pixel; + uint32_t dest_pixel_index_orig = dest_pixel_index_x; + + // Capture device type as integer to avoid copying Device object + auto device_type_int = static_cast(out_device.type()); + auto device_index = out_device.index(); + + // Explicitly capture only what's needed to avoid issues with [=] + auto decode_func = [ + // Tile identification + index, index_hash, + // Compression and decoding params + compression_method, tiledata_offset, tiledata_size, + // Tile geometry + tile_pixel_offset_sy, tile_pixel_offset_ey, tile_pixel_offset_x, + tw, th, samples_per_pixel, nbytes_tw, nbytes_tile_pixel_size_x, pixel_offset_ey, + // Destination params - using _orig versions + nbytes_tile_index_orig, dest_pixel_index_orig, dest_start_ptr, dest_pixel_step_y, + // File and cache params + tiff_file, ifd_hash_value, tile_raster_nbytes, cache_type, + // JPEG params + jpegtable_data, jpegtable_count, jpeg_color_space, + // Other params + background_value, predictor, device_type_int, device_index, + // Boundary-specific params + offset_x, offset_ex, offset_boundary_x, pixel_offset_boundary_x, pixel_offset_ex, + offset_boundary_y, pixel_offset_boundary_y, dest_pixel_offset_len_y, + // Loop/boundary indices + index_y, boundary_index_y, end_index_y, + // Loader pointer + loader + ]() { + #ifdef DEBUG + fmt::print("🔍🔍🔍 decode_func_boundary: LAMBDA INVOKED! index={}\n", index); + fflush(stdout); + #endif + + // Reconstruct Device object inside lambda + cucim::io::Device out_device(static_cast(device_type_int), device_index); + + PROF_SCOPED_RANGE(PROF_EVENT_P(ifd_read_region_tiles_boundary_task, index_hash)); + + // Get image cache directly instead of capturing by reference + cucim::cache::ImageCache& image_cache = cucim::CuImage::cache_manager().cache(); + + uint32_t nbytes_tile_index = nbytes_tile_index_orig; + uint32_t dest_pixel_index = dest_pixel_index_orig; + + if (tiledata_size > 0) + { + bool copy_partial = false; + uint32_t fixed_nbytes_tile_pixel_size_x = nbytes_tile_pixel_size_x; + uint32_t fixed_tile_pixel_offset_ey = tile_pixel_offset_ey; + + if (offset_x == offset_boundary_x) + { + copy_partial = true; + if (offset_x != offset_ex) + { + fixed_nbytes_tile_pixel_size_x = + (pixel_offset_boundary_x - tile_pixel_offset_x + 1) * samples_per_pixel; + } + else + { + fixed_nbytes_tile_pixel_size_x = + (std::min(pixel_offset_boundary_x, pixel_offset_ex) - tile_pixel_offset_x + 1) * + samples_per_pixel; + } + } + if (index_y == boundary_index_y) + { + copy_partial = true; + if (index_y != end_index_y) + { + fixed_tile_pixel_offset_ey = pixel_offset_boundary_y; + } + else + { + fixed_tile_pixel_offset_ey = std::min(pixel_offset_boundary_y, pixel_offset_ey); + } + } + + uint8_t* tile_data = nullptr; + std::unique_ptr tile_raster = + std::unique_ptr(nullptr, cucim_free); + + if (loader && loader->batch_data_processor()) + { + switch (compression_method) + { + case COMPRESSION_JPEG: + case COMPRESSION_APERIO_JP2K_YCBCR: // 33003 + case COMPRESSION_APERIO_JP2K_RGB: // 33005 + break; + default: + throw std::runtime_error("Unsupported compression method"); + } + auto value = loader->wait_for_processing(index); + if (!value) // if shutdown + { + return; + } + + tile_data = static_cast(value->data); + + cudaError_t cuda_status; + if (copy_partial) + { + uint32_t fill_gap_x = nbytes_tile_pixel_size_x - fixed_nbytes_tile_pixel_size_x; + // Fill original, then fill white for remaining + if (fill_gap_x > 0) + { + CUDA_ERROR(cudaMemcpy2D( + dest_start_ptr + dest_pixel_index, dest_pixel_step_y, tile_data + nbytes_tile_index, + nbytes_tw, fixed_nbytes_tile_pixel_size_x, + fixed_tile_pixel_offset_ey - tile_pixel_offset_sy + 1, cudaMemcpyDeviceToDevice)); + CUDA_ERROR(cudaMemset2D(dest_start_ptr + dest_pixel_index + fixed_nbytes_tile_pixel_size_x, + dest_pixel_step_y, background_value, fill_gap_x, + fixed_tile_pixel_offset_ey - tile_pixel_offset_sy + 1)); + dest_pixel_index += + dest_pixel_step_y * (fixed_tile_pixel_offset_ey - tile_pixel_offset_sy + 1); + } + else + { + CUDA_ERROR(cudaMemcpy2D( + dest_start_ptr + dest_pixel_index, dest_pixel_step_y, tile_data + nbytes_tile_index, + nbytes_tw, fixed_nbytes_tile_pixel_size_x, + fixed_tile_pixel_offset_ey - tile_pixel_offset_sy + 1, cudaMemcpyDeviceToDevice)); + dest_pixel_index += + dest_pixel_step_y * (fixed_tile_pixel_offset_ey - tile_pixel_offset_sy + 1); + } + + CUDA_ERROR(cudaMemset2D(dest_start_ptr + dest_pixel_index, dest_pixel_step_y, + background_value, nbytes_tile_pixel_size_x, + tile_pixel_offset_ey - (fixed_tile_pixel_offset_ey + 1) + 1)); + } + else + { + CUDA_ERROR(cudaMemcpy2D(dest_start_ptr + dest_pixel_index, dest_pixel_step_y, + tile_data + nbytes_tile_index, nbytes_tw, nbytes_tile_pixel_size_x, + tile_pixel_offset_ey - tile_pixel_offset_sy + 1, + cudaMemcpyDeviceToDevice)); + } + } + else + { + auto key = image_cache.create_key(ifd_hash_value, index); + image_cache.lock(index_hash); + auto value = image_cache.find(key); + if (value) + { + image_cache.unlock(index_hash); + tile_data = static_cast(value->data); + } + else + { + // Lifetime of tile_data is same with `value` + // : do not access this data when `value` is not accessible. + if (cache_type != cucim::cache::CacheType::kNoCache) + { + tile_data = static_cast(image_cache.allocate(tile_raster_nbytes)); + } + else + { + // Allocate temporary buffer for tile data + tile_raster = std::unique_ptr( + reinterpret_cast(cucim_malloc(tile_raster_nbytes)), cucim_free); + tile_data = tile_raster.get(); + } + { + // REMOVED: Legacy CPU decoder fallback code (duplicate) + // This code path should NOT be reached in a pure nvImageCodec build. + // All decoding should go through the nvImageCodec ROI path (lines 219-276). + // If you see this error, investigate why ROI decode failed. + throw std::runtime_error(fmt::format( + "INTERNAL ERROR: Tile-based CPU decoder fallback reached. " + "This should not happen in nvImageCodec build. " + "Compression method: {}, tile offset: {}, size: {}", + compression_method, tiledata_offset, tiledata_size)); + } + value = image_cache.create_value(tile_data, tile_raster_nbytes); + image_cache.insert(key, value); + image_cache.unlock(index_hash); + } + if (copy_partial) + { + uint32_t fill_gap_x = nbytes_tile_pixel_size_x - fixed_nbytes_tile_pixel_size_x; + // Fill original, then fill white for remaining + if (fill_gap_x > 0) + { + for (uint32_t ty = tile_pixel_offset_sy; ty <= fixed_tile_pixel_offset_ey; + ++ty, dest_pixel_index += dest_pixel_step_y, nbytes_tile_index += nbytes_tw) + { + memcpy(dest_start_ptr + dest_pixel_index, tile_data + nbytes_tile_index, + fixed_nbytes_tile_pixel_size_x); + memset(dest_start_ptr + dest_pixel_index + fixed_nbytes_tile_pixel_size_x, + background_value, fill_gap_x); + } + } + else + { + for (uint32_t ty = tile_pixel_offset_sy; ty <= fixed_tile_pixel_offset_ey; + ++ty, dest_pixel_index += dest_pixel_step_y, nbytes_tile_index += nbytes_tw) + { + memcpy(dest_start_ptr + dest_pixel_index, tile_data + nbytes_tile_index, + fixed_nbytes_tile_pixel_size_x); + } + } + + for (uint32_t ty = fixed_tile_pixel_offset_ey + 1; ty <= tile_pixel_offset_ey; + ++ty, dest_pixel_index += dest_pixel_step_y) + { + memset(dest_start_ptr + dest_pixel_index, background_value, nbytes_tile_pixel_size_x); + } + } + else + { + #ifdef DEBUG + fmt::print("🔍 MEMCPY_DETAILED: tile_pixel_offset_sy={}, tile_pixel_offset_ey={}\n", + tile_pixel_offset_sy, tile_pixel_offset_ey); + fmt::print("🔍 MEMCPY_DETAILED: dest_start_ptr={}, dest_pixel_step_y={}\n", + static_cast(dest_start_ptr), dest_pixel_step_y); + fmt::print("🔍 MEMCPY_DETAILED: initial dest_pixel_index={}, initial nbytes_tile_index={}\n", + dest_pixel_index, nbytes_tile_index); + fmt::print("🔍 MEMCPY_DETAILED: nbytes_tile_pixel_size_x={}, nbytes_tw={}\n", + nbytes_tile_pixel_size_x, nbytes_tw); + fmt::print("🔍 MEMCPY_DETAILED: tile_data={}\n", static_cast(tile_data)); + #endif + + // Calculate total buffer size needed + uint32_t num_rows = tile_pixel_offset_ey - tile_pixel_offset_sy + 1; + [[maybe_unused]] size_t total_dest_size_needed = dest_pixel_index + (num_rows - 1) * dest_pixel_step_y + nbytes_tile_pixel_size_x; + #ifdef DEBUG + fmt::print("🔍 MEMCPY_DETAILED: num_rows={}, total_dest_size_needed={}\n", + num_rows, total_dest_size_needed); + #endif + + for (uint32_t ty = tile_pixel_offset_sy; ty <= tile_pixel_offset_ey; + ++ty, dest_pixel_index += dest_pixel_step_y, nbytes_tile_index += nbytes_tw) + { + #ifdef DEBUG + fmt::print("🔍 MEMCPY_ROW ty={}: dest_pixel_index={}, nbytes_tile_index={}, copy_size={}\n", + ty, dest_pixel_index, nbytes_tile_index, nbytes_tile_pixel_size_x); + fmt::print("🔍 MEMCPY_ROW: dest_ptr={}, src_ptr={}\n", + static_cast(dest_start_ptr + dest_pixel_index), + static_cast(tile_data + nbytes_tile_index)); + fflush(stdout); + #endif + + memcpy(dest_start_ptr + dest_pixel_index, tile_data + nbytes_tile_index, + nbytes_tile_pixel_size_x); + + #ifdef DEBUG + fmt::print("🔍 MEMCPY_ROW ty={}: SUCCESS\n", ty); + fflush(stdout); + #endif + } + } + } + } + else + { + + if (out_device.type() == cucim::io::DeviceType::kCPU) + { + for (uint32_t ty = tile_pixel_offset_sy; ty <= tile_pixel_offset_ey; + ++ty, dest_pixel_index += dest_pixel_step_y, nbytes_tile_index += nbytes_tw) + { + // Set (255,255,255) + memset(dest_start_ptr + dest_pixel_index, background_value, nbytes_tile_pixel_size_x); + } + } + else + { + cudaError_t cuda_status; + CUDA_ERROR(cudaMemset2D(dest_start_ptr + dest_pixel_index, dest_pixel_step_y, background_value, + nbytes_tile_pixel_size_x, tile_pixel_offset_ey - tile_pixel_offset_sy)); + } + } + }; + + if (loader && *loader) + { + loader->enqueue(std::move(decode_func), + cucim::loader::TileInfo{ location_index, index, tiledata_offset, tiledata_size }); + } + else + { + decode_func(); + } + + dest_pixel_index_x += nbytes_tile_pixel_size_x; + } + dest_start_ptr += dest_pixel_step_y * dest_pixel_offset_len_y; + } + return true; +} + +} // namespace cuslide::tiff + + +// Hidden methods for benchmarking. + +#include +#include +#include +#include + +namespace cuslide::tiff +{ +} // namespace cuslide::tiff diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.h b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.h new file mode 100644 index 000000000..d2d3b99a7 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.h @@ -0,0 +1,174 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef CUSLIDE_IFD_H +#define CUSLIDE_IFD_H + +#include "types.h" +#include "tiff_constants.h" + +#include +#include +#include + +#include +#include +#include +#include + +#ifdef CUCIM_HAS_NVIMGCODEC +#include +#include "cuslide/nvimgcodec/nvimgcodec_tiff_parser.h" +#endif + +namespace cuslide::tiff +{ + +// Forward declaration. +class TIFF; + +class EXPORT_VISIBLE IFD : public std::enable_shared_from_this +{ +public: + IFD(TIFF* tiff, uint16_t index, ifd_offset_t offset); +#ifdef CUCIM_HAS_NVIMGCODEC + IFD(TIFF* tiff, uint16_t index, const cuslide2::nvimgcodec::IfdInfo& ifd_info); +#endif + ~IFD(); + + static bool read_region_tiles(const TIFF* tiff, + const IFD* ifd, + const int64_t* location, + const int64_t location_index, + const int64_t w, + const int64_t h, + void* raster, + const cucim::io::Device& out_device, + cucim::loader::ThreadBatchDataLoader* loader); + + static bool read_region_tiles_boundary(const TIFF* tiff, + const IFD* ifd, + const int64_t* location, + const int64_t location_index, + const int64_t w, + const int64_t h, + void* raster, + const cucim::io::Device& out_device, + cucim::loader::ThreadBatchDataLoader* loader); + + bool read(const TIFF* tiff, + const cucim::io::format::ImageMetadataDesc* metadata, + const cucim::io::format::ImageReaderRegionRequestDesc* request, + cucim::io::format::ImageDataDesc* out_image_data); + + + uint32_t index() const; + ifd_offset_t offset() const; + + std::string& software(); + std::string& model(); + std::string& image_description(); + uint16_t resolution_unit() const; + float x_resolution() const; + float y_resolution() const; + uint32_t width() const; + uint32_t height() const; + uint32_t tile_width() const; + uint32_t tile_height() const; + uint32_t rows_per_strip() const; + uint32_t bits_per_sample() const; + uint32_t samples_per_pixel() const; + uint64_t subfile_type() const; + uint16_t planar_config() const; + uint16_t photometric() const; + uint16_t compression() const; + uint16_t predictor() const; + + uint16_t subifd_count() const; + std::vector& subifd_offsets(); + + uint32_t image_piece_count() const; + const std::vector& image_piece_offsets() const; + const std::vector& image_piece_bytecounts() const; + + size_t pixel_size_nbytes() const; + size_t tile_raster_size_nbytes() const; + + // Make TIFF available to access private members of IFD + friend class TIFF; + +private: + TIFF* tiff_; // cannot use shared_ptr as IFD is created during the construction of TIFF using 'new' + uint32_t ifd_index_ = 0; + ifd_offset_t ifd_offset_ = 0; + + std::string software_; + std::string model_; + std::string image_description_; + uint16_t resolution_unit_ = 1; // 1 = No absolute unit of measurement, 2 = Inch, 3 = Centimeter + float x_resolution_ = 1.0f; + float y_resolution_ = 1.0f; + + uint32_t flags_ = 0; + uint32_t width_ = 0; + uint32_t height_ = 0; + uint32_t tile_width_ = 0; + uint32_t tile_height_ = 0; + uint32_t rows_per_strip_ = 0; + uint32_t bits_per_sample_ = 0; + uint32_t samples_per_pixel_ = 0; + uint64_t subfile_type_ = 0; + uint16_t planar_config_ = 0; + uint16_t photometric_ = 0; + uint16_t compression_ = 0; + uint16_t predictor_ = 1; // 1: none, 2: horizontal differencing, 3: floating point predictor + + uint16_t subifd_count_ = 0; + std::vector subifd_offsets_; + + std::vector jpegtable_; + int32_t jpeg_color_space_ = 0; /// 0: JCS_UNKNOWN, 2: JCS_RGB, 3: JCS_YCbCr + + uint32_t image_piece_count_ = 0; + std::vector image_piece_offsets_; + std::vector image_piece_bytecounts_; + + uint64_t hash_value_ = 0; /// file hash including ifd index. + +#ifdef CUCIM_HAS_NVIMGCODEC + // nvImageCodec-specific members + nvimgcodecCodeStream_t nvimgcodec_sub_stream_ = nullptr; + std::string codec_name_; // codec name from nvImageCodec (jpeg, jpeg2k, deflate, etc.) +#endif + + /** + * @brief Check if the current compression method is supported or not. + */ + bool is_compression_supported() const; + + /** + * + * Note: This method is called by the constructor of IFD and read() method so it is possible that the output of + * 'is_read_optimizable()' could be changed during read() method if user set read configuration + * after opening TIFF file. + * @return + */ + bool is_read_optimizable() const; + + /** + * @brief Check if the specified image format is supported or not. + */ + bool is_format_supported() const; + +#ifdef CUCIM_HAS_NVIMGCODEC + /** + * @brief Parse codec string to TIFF compression code + */ + static uint16_t parse_codec_to_compression(const std::string& codec); +#endif +}; +} // namespace cuslide::tiff + +#endif // CUSLIDE_IFD_H diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/tiff.cpp b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/tiff.cpp new file mode 100644 index 000000000..2a2c3a31a --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/tiff.cpp @@ -0,0 +1,1270 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "tiff.h" + +#include + +#include +#include +#include + +#include +#include +#include +#include "tiff_constants.h" + +#include +#include +#include +#include + +#include "ifd.h" + +static constexpr int DEFAULT_IFD_SIZE = 32; + +using json = nlohmann::json; + +namespace cuslide::tiff +{ + +// djb2 algorithm from http://www.cse.yorku.ca/~oz/hash.html +constexpr uint32_t hash_str(const char* str) +{ + uint32_t hash = 5381; + uint32_t c = 0; + while ((c = *str++)) + hash = ((hash << 5) + hash) + c; // hash * 33 + c + return hash; +} + +enum class PhilipsMetadataStage : uint8_t +{ + ROOT = 0, + SCANNED_IMAGE, + PIXEL_DATA_PRESENTATION, + ELEMENT, + ARRAY_ELEMENT +}; +enum class PhilipsMetadataType : uint8_t +{ + IString = 0, + IDouble, + IUInt16, + IUInt32, + IUInt64 +}; +static void parse_string_array(const char* values, json& arr, PhilipsMetadataType type) +{ + std::string_view text(values); + std::string_view::size_type pos = 0; + while ((pos = text.find('"', pos)) != std::string_view::npos) + { + auto next_pos = text.find('"', pos + 1); + if (next_pos != std::string_view::npos) + { + if (text[next_pos - 1] != '\\') + { + switch (type) + { + case PhilipsMetadataType::IString: + arr.emplace_back(std::string(text.substr(pos + 1, next_pos - pos - 1))); + break; + case PhilipsMetadataType::IDouble: + arr.emplace_back(std::stod(std::string(text.substr(pos + 1, next_pos - pos - 1)))); + break; + case PhilipsMetadataType::IUInt16: + case PhilipsMetadataType::IUInt32: + case PhilipsMetadataType::IUInt64: + arr.emplace_back(std::stoul(std::string(text.substr(pos + 1, next_pos - pos - 1)))); + break; + } + pos = next_pos + 1; + } + } + } +} +static void parse_philips_tiff_metadata(const pugi::xml_node& node, + json& metadata, + const char* name, + PhilipsMetadataStage stage) +{ + switch (stage) + { + case PhilipsMetadataStage::ROOT: + case PhilipsMetadataStage::SCANNED_IMAGE: + case PhilipsMetadataStage::PIXEL_DATA_PRESENTATION: + for (pugi::xml_node attr = node.child("Attribute"); attr; attr = attr.next_sibling("Attribute")) + { + const pugi::xml_attribute& attr_attribute = attr.attribute("Name"); + if (attr_attribute) + { + parse_philips_tiff_metadata(attr, metadata, attr_attribute.value(), PhilipsMetadataStage::ELEMENT); + } + } + break; + case PhilipsMetadataStage::ARRAY_ELEMENT: + break; + case PhilipsMetadataStage::ELEMENT: + const pugi::xml_attribute& attr_attribute = node.attribute("PMSVR"); + auto p_attr_name = attr_attribute.as_string(); + if (p_attr_name != nullptr && *p_attr_name != '\0') + { + if (name) + { + switch (hash_str(p_attr_name)) + { + case hash_str("IString"): + metadata.emplace(name, node.text().as_string()); + break; + case hash_str("IDouble"): + metadata.emplace(name, node.text().as_double()); + break; + case hash_str("IUInt16"): + metadata.emplace(name, node.text().as_uint()); + break; + case hash_str("IUInt32"): + metadata.emplace(name, node.text().as_uint()); + break; + case hash_str("IUint64"): + metadata.emplace(name, node.text().as_ullong()); + break; + case hash_str("IStringArray"): { // Process text such as `"a" "b" "c"` + auto item_iter = metadata.emplace(name, json::array()); + parse_string_array(node.child_value(), *(item_iter.first), PhilipsMetadataType::IString); + break; + } + case hash_str("IDoubleArray"): { // Process text such as `"0.0" "0.1" "0.2"` + auto item_iter = metadata.emplace(name, json::array()); + parse_string_array(node.child_value(), *(item_iter.first), PhilipsMetadataType::IDouble); + break; + } + case hash_str("IUInt16Array"): { // Process text such as `"1" "2" "3"` + auto item_iter = metadata.emplace(name, json::array()); + parse_string_array(node.child_value(), *(item_iter.first), PhilipsMetadataType::IUInt16); + break; + } + case hash_str("IUInt32Array"): { // Process text such as `"1" "2" "3"` + auto item_iter = metadata.emplace(name, json::array()); + parse_string_array(node.child_value(), *(item_iter.first), PhilipsMetadataType::IUInt32); + break; + } + case hash_str("IUInt64Array"): { // Process text such as `"1" "2" "3"` + auto item_iter = metadata.emplace(name, json::array()); + parse_string_array(node.child_value(), *(item_iter.first), PhilipsMetadataType::IUInt64); + break; + } + case hash_str("IDataObjectArray"): + if (strcmp(name, "PIIM_PIXEL_DATA_REPRESENTATION_SEQUENCE") == 0) + { + const auto& item_array_iter = + metadata.emplace(std::string("PIIM_PIXEL_DATA_REPRESENTATION_SEQUENCE"), json::array()); + for (pugi::xml_node data_node = node.child("Array").child("DataObject"); data_node; + data_node = data_node.next_sibling("DataObject")) + { + auto& item_iter = item_array_iter.first->emplace_back(json{}); + parse_philips_tiff_metadata( + data_node, item_iter, nullptr, PhilipsMetadataStage::PIXEL_DATA_PRESENTATION); + } + } + break; + } + } + } + break; + } +} + +static std::vector split_string(std::string_view s, std::string_view delim, size_t capacity = 0) +{ + size_t pos_start = 0; + size_t pos_end = -1; + size_t delim_len = delim.length(); + + std::vector result; + std::string_view item; + + if (capacity != 0) + { + result.reserve(capacity); + } + + while ((pos_end = s.find(delim, pos_start)) != std::string_view::npos) + { + item = s.substr(pos_start, pos_end - pos_start); + pos_start = pos_end + delim_len; + result.emplace_back(item); + } + + result.emplace_back(s.substr(pos_start)); + return result; +} + +static std::string strip_string(const std::string& str) +{ + static const char* white_spaces = " \r\n\t"; + std::string::size_type start_pos = str.find_first_not_of(white_spaces); + std::string::size_type end_pos = str.find_last_not_of(white_spaces); + + if (start_pos != std::string::npos) + { + return str.substr(start_pos, end_pos - start_pos + 1); + } + else + { + return std::string(); + } +} + +static void parse_aperio_svs_metadata(std::shared_ptr& first_ifd, json& metadata) +{ + (void)metadata; + std::string& desc = first_ifd->image_description(); + + // Assumes that metadata's image description starts with 'Aperio '. + // It is handled by 'resolve_vendor_format()' + std::vector items = split_string(desc, "|"); + if (items.size() < 1) + { + return; + } + // Store the first item of the image description as 'Header' + metadata.emplace("Header", items[0]); + for (size_t i = 1; i < items.size(); ++i) + { + std::vector key_value = split_string(items[i], " = "); + if (key_value.size() == 2) + { + metadata.emplace(std::move(strip_string(key_value[0])), std::move(strip_string(key_value[1]))); + } + } +} + +TIFF::~TIFF() +{ + PROF_SCOPED_RANGE(PROF_EVENT(tiff__tiff)); + close(); +} + +// NEW CONSTRUCTOR: nvImageCodec-only (no libtiff) +TIFF::TIFF(const cucim::filesystem::Path& file_path) : file_path_(file_path) +{ + PROF_SCOPED_RANGE(PROF_EVENT_P(tiff_tiff, 1)); + + #ifdef DEBUG + fmt::print("📂 Opening TIFF file with nvImageCodec: {}\n", file_path); + #endif // DEBUG + + // Step 1: Open file descriptor (needed for CuCIMFileHandle) + // Copy file path (will be freed by CuCIMFileHandle destructor) + char* file_path_cstr = static_cast(cucim_malloc(file_path.size() + 1)); + memcpy(file_path_cstr, file_path.c_str(), file_path.size()); + file_path_cstr[file_path.size()] = '\0'; + + int fd = ::open(file_path_cstr, O_RDONLY, 0666); + if (fd == -1) + { + cucim_free(file_path_cstr); + throw std::invalid_argument(fmt::format("Cannot open {}!", file_path)); + } + + // Step 2: Create CuCIMFileHandle with 'this' as client_data + // CRITICAL: The 5th parameter (client_data) must be 'this' so parser_parse() + // can retrieve the TIFF object later via handle->client_data + file_handle_shared_ = std::make_shared( + fd, nullptr, FileHandleType::kPosix, file_path_cstr, this); + + // Step 3: Set up deleter to clean up TIFF object when handle is destroyed + // This is CRITICAL to prevent memory leaks and double-frees + file_handle_shared_->set_deleter([](CuCIMFileHandle_ptr handle_ptr) -> bool { + auto* handle = reinterpret_cast(handle_ptr); + if (handle && handle->client_data) + { + auto* tiff_obj = static_cast(handle->client_data); + delete tiff_obj; + handle->client_data = nullptr; + } + return true; + }); + + // Step 4: Initialize nvImageCodec TiffFileParser (MANDATORY) + try { + nvimgcodec_parser_ = std::make_unique( + file_path.c_str()); + + if (!nvimgcodec_parser_->is_valid()) { + throw std::runtime_error("TiffFileParser initialization failed"); + } + + #ifdef DEBUG + fmt::print("✅ nvImageCodec TiffFileParser initialized successfully\n"); + #endif // DEBUG + + // Extract basic file properties from TiffFileParser + std::string detected_format = nvimgcodec_parser_->get_detected_format(); + #ifdef DEBUG + fmt::print(" Detected format: {}\n", detected_format); + #endif // DEBUG + + #ifdef DEBUG + uint32_t ifd_count = nvimgcodec_parser_->get_ifd_count(); + fmt::print(" IFD count: {}\n", ifd_count); + #endif // DEBUG + + // Set default values (nvImageCodec handles endianness internally) + is_big_endian_ = false; + + } catch (const std::exception& e) { + #ifdef DEBUG + fmt::print("❌ FATAL: Failed to initialize nvImageCodec TiffFileParser: {}\n", e.what()); + #endif // DEBUG + #ifdef DEBUG + fmt::print(" This library requires nvImageCodec for TIFF support.\n"); + #endif // DEBUG + #ifdef DEBUG + fmt::print(" Please ensure nvImageCodec is properly installed.\n"); + #endif // DEBUG + // Cleanup file handle before re-throwing + file_handle_shared_.reset(); + throw std::runtime_error(fmt::format( + "Cannot open TIFF file '{}' without nvImageCodec: {}", + file_path, e.what())); + } + + // Initialize metadata container + metadata_ = new json{}; +} + +TIFF::TIFF(const cucim::filesystem::Path& file_path, uint64_t read_config) : TIFF(file_path) +{ + PROF_SCOPED_RANGE(PROF_EVENT_P(tiff_tiff, 2)); + read_config_ = read_config; +} + +// Legacy libtiff-style constructors (for backward compatibility with tests) +TIFF::TIFF(const cucim::filesystem::Path& file_path, int mode) : TIFF(file_path) +{ + // mode parameter is ignored in pure nvImageCodec build + (void)mode; +} + +TIFF::TIFF(const cucim::filesystem::Path& file_path, int mode, uint64_t config) : TIFF(file_path, config) +{ + // mode parameter is ignored in pure nvImageCodec build + (void)mode; +} + +std::shared_ptr TIFF::open(const cucim::filesystem::Path& file_path) +{ + auto tif = std::make_shared(file_path); + tif->construct_ifds(); + return tif; +} + +std::shared_ptr TIFF::open(const cucim::filesystem::Path& file_path, uint64_t config) +{ + auto tif = std::make_shared(file_path, config); + tif->construct_ifds(); + return tif; +} + +// Legacy libtiff-style open methods (for backward compatibility with tests) +std::shared_ptr TIFF::open(const cucim::filesystem::Path& file_path, int mode) +{ + auto tif = std::make_shared(file_path, mode); + tif->construct_ifds(); + return tif; +} + +std::shared_ptr TIFF::open(const cucim::filesystem::Path& file_path, int mode, uint64_t config) +{ + auto tif = std::make_shared(file_path, mode, config); + tif->construct_ifds(); + return tif; +} + +void TIFF::close() +{ + // REMOVED: libtiff cleanup - no longer using tiff_client_ + + // Clean up metadata + if (metadata_) + { + delete reinterpret_cast(metadata_); + metadata_ = nullptr; + } + + // nvimgcodec_parser_ is automatically cleaned up by unique_ptr destructor +} + +void TIFF::construct_ifds() +{ + PROF_SCOPED_RANGE(PROF_EVENT(tiff_construct_ifds)); + + if (!nvimgcodec_parser_ || !nvimgcodec_parser_->is_valid()) { + throw std::runtime_error("Cannot construct IFDs: nvImageCodec parser not available"); + } + + ifd_offsets_.clear(); + ifds_.clear(); + + uint32_t ifd_count = nvimgcodec_parser_->get_ifd_count(); + #ifdef DEBUG + fmt::print("📋 Constructing {} IFDs from nvImageCodec metadata\n", ifd_count); + #endif // DEBUG + + ifd_offsets_.reserve(ifd_count); + ifds_.reserve(ifd_count); + + for (uint32_t ifd_index = 0; ifd_index < ifd_count; ++ifd_index) { + try { +#ifdef CUCIM_HAS_NVIMGCODEC + // Get IFD metadata from TiffFileParser + const auto& ifd_info = nvimgcodec_parser_->get_ifd(ifd_index); + + // Use IFD index as pseudo-offset (actual file offset not needed) + ifd_offsets_.push_back(ifd_index); + + // Create IFD from nvImageCodec metadata using NEW constructor + auto ifd = std::make_shared(this, ifd_index, ifd_info); + ifds_.emplace_back(std::move(ifd)); + + #ifdef DEBUG + fmt::print(" ✅ IFD[{}]: {}x{}, {} channels, codec: {}\n", + ifd_index, ifd_info.width, ifd_info.height, + ifd_info.num_channels, ifd_info.codec); + #endif // DEBUG +#else + (void)ifd_index; + throw std::runtime_error("cuslide2 requires nvImageCodec for IFD parsing"); +#endif // CUCIM_HAS_NVIMGCODEC + + } catch (const std::exception& e) { + #ifdef DEBUG + fmt::print(" ⚠️ Failed to create IFD[{}]: {}\n", ifd_index, e.what()); + #endif // DEBUG + // Continue with other IFDs - some may be corrupted + } + } + + if (ifds_.empty()) { + throw std::runtime_error("No valid IFDs found in TIFF file"); + } + + #ifdef DEBUG + fmt::print("✅ Successfully created {} out of {} IFDs\n", ifds_.size(), ifd_count); + #endif // DEBUG + + // Initialize level-to-IFD mapping (will be updated by resolve_vendor_format) + level_to_ifd_idx_.clear(); + level_to_ifd_idx_.reserve(ifds_.size()); + for (size_t index = 0; index < ifds_.size(); ++index) { + level_to_ifd_idx_.emplace_back(index); + } + + // Detect vendor format and classify IFDs + resolve_vendor_format(); + + // Sort resolution levels by size (largest first) + std::sort(level_to_ifd_idx_.begin(), level_to_ifd_idx_.end(), + [this](const size_t& a, const size_t& b) { + uint32_t width_a = this->ifds_[a]->width(); + uint32_t width_b = this->ifds_[b]->width(); + if (width_a != width_b) { + return width_a > width_b; + } + return this->ifds_[a]->height() > this->ifds_[b]->height(); + }); + + #ifdef DEBUG + fmt::print("✅ TIFF initialization complete: {} levels, {} associated images\n", + level_to_ifd_idx_.size(), associated_images_.size()); + #endif // DEBUG +} +void TIFF::resolve_vendor_format() +{ + PROF_SCOPED_RANGE(PROF_EVENT(tiff_resolve_vendor_format)); + uint16_t ifd_count = ifds_.size(); + if (ifd_count == 0) + { + return; + } + json* json_metadata = reinterpret_cast(metadata_); + + auto& first_ifd = ifds_[0]; + std::string& model = first_ifd->model(); + std::string& software = first_ifd->software(); + const uint16_t resolution_unit = first_ifd->resolution_unit(); + const float x_resolution = first_ifd->x_resolution(); + const float y_resolution = first_ifd->y_resolution(); + + // Detect Aperio SVS format + { + bool is_aperio = false; + + // Method 1: Check ImageDescription starts with "Aperio " + auto& image_desc = first_ifd->image_description(); + std::string_view prefix("Aperio "); + auto res = std::mismatch(prefix.begin(), prefix.end(), image_desc.begin()); + if (res.first == prefix.end()) + { + is_aperio = true; + } + + // Method 2: Check metadata_blobs for Aperio (kind=1) + // This includes the workaround for nvImageCodec 0.6.0 misclassifying Aperio as Leica + if (!is_aperio && nvimgcodec_parser_) + { + const auto& metadata_blobs = nvimgcodec_parser_->get_metadata_blobs(0); + if (metadata_blobs.find(1) != metadata_blobs.end()) // MED_APERIO = 1 + { + is_aperio = true; + #ifdef DEBUG + fmt::print("✅ Aperio detected via metadata_blobs workaround\n"); + #endif + } + } + + if (is_aperio) + { + _populate_aperio_svs_metadata(ifd_count, json_metadata, first_ifd); + } + } + + // Detect Philips TIFF + // NOTE: nvImageCodec 0.6.0 doesn't expose individual TIFF tags (like SOFTWARE) + // Workaround: Check for Philips XML in ImageDescription or use nvImageCodec metadata kind + { + bool is_philips = false; + + // Method 1: Check SOFTWARE tag (available in nvImageCodec 0.7.0+) + std::string_view prefix("Philips"); + auto res = std::mismatch(prefix.begin(), prefix.end(), software.begin()); + if (res.first == prefix.end()) + { + is_philips = true; + } + + // Method 2: Check for Philips XML structure in ImageDescription + // (Workaround for nvImageCodec 0.6.0 where SOFTWARE tag is not available) + if (!is_philips) + { + auto& image_desc = first_ifd->image_description(); + if (image_desc.find("get_metadata_blobs(0); + if (metadata_blobs.find(2) != metadata_blobs.end()) // MED_PHILIPS = 2 + { + is_philips = true; + #ifdef DEBUG + fmt::print("✅ Philips detected via metadata_blobs workaround\n"); + #endif + } + } + + // Method 4: Check if nvImageCodec detected it as Philips (format string) + if (!is_philips && nvimgcodec_parser_) + { + std::string detected_format = nvimgcodec_parser_->get_detected_format(); + if (detected_format.find("Philips") != std::string::npos) + { + is_philips = true; + } + } + + if (is_philips) + { + _populate_philips_tiff_metadata(ifd_count, json_metadata, first_ifd); + } + } + + // Append TIFF metadata + if (json_metadata) + { + json tiff_metadata; + + tiff_metadata.emplace("model", model); + tiff_metadata.emplace("software", software); + switch (resolution_unit) + { + case 2: + tiff_metadata.emplace("resolution_unit", "inch"); + break; + case 3: + tiff_metadata.emplace("resolution_unit", "centimeter"); + break; + default: + tiff_metadata.emplace("resolution_unit", ""); + break; + } + tiff_metadata.emplace("x_resolution", x_resolution); + tiff_metadata.emplace("y_resolution", y_resolution); + + (*json_metadata).emplace("tiff", std::move(tiff_metadata)); + } +} + +void TIFF::_populate_philips_tiff_metadata(uint16_t ifd_count, void* metadata, std::shared_ptr& first_ifd) +{ + json* json_metadata = reinterpret_cast(metadata); + std::string_view macro_prefix("Macro"); + std::string_view label_prefix("Label"); + + pugi::xml_document doc; + const char* image_desc_cstr = first_ifd->image_description().c_str(); + pugi::xml_parse_result result = doc.load_string(image_desc_cstr); + if (result) + { + const auto& data_object = doc.child("DataObject"); + if (std::string_view(data_object.attribute("ObjectType").as_string("")) != "DPUfsImport") + { + #ifdef DEBUG + fmt::print( + stderr, + "[Warning] Failed to read as Philips TIFF. It looks like Philips TIFF but the image description of the first IFD doesn't have '' node!\n"); + #endif // DEBUG + return; + } + + pugi::xpath_query PIM_DP_IMAGE_TYPE( + "Attribute[@Name='PIM_DP_SCANNED_IMAGES']/Array/DataObject[Attribute/@Name='PIM_DP_IMAGE_TYPE' and Attribute/text()='WSI']"); + pugi::xpath_node_set wsi_nodes = PIM_DP_IMAGE_TYPE.evaluate_node_set(data_object); + if (wsi_nodes.size() != 1) + { + #ifdef DEBUG + fmt::print( + stderr, + "[Warning] Failed to read as Philips TIFF. Expected only one 'DPScannedImage' node with PIM_DP_IMAGE_TYPE='WSI'.\n"); + #endif // DEBUG + return; + } + + pugi::xpath_query DICOM_PIXEL_SPACING( + "Attribute[@Name='PIIM_PIXEL_DATA_REPRESENTATION_SEQUENCE']/Array/DataObject/Attribute[@Name='DICOM_PIXEL_SPACING']"); + pugi::xpath_node_set pixel_spacing_nodes = DICOM_PIXEL_SPACING.evaluate_node_set(wsi_nodes[0]); + + std::vector> pixel_spacings; + pixel_spacings.reserve(pixel_spacings.size()); + + for (const pugi::xpath_node& pixel_spacing : pixel_spacing_nodes) + { + std::string values = pixel_spacing.node().text().as_string(); + + // Assume that 'values' has a '"" ""' form. + double spacing_x = 0.0; + double spacing_y = 0.0; + + std::string::size_type offset = values.find("\""); + if (offset != std::string::npos) + { + spacing_y = std::atof(&values.c_str()[offset + 1]); + offset = values.find(" \"", offset); + if (offset != std::string::npos) + { + spacing_x = std::atof(&values.c_str()[offset + 2]); + } + } + if (spacing_x == 0.0 || spacing_y == 0.0) + { + #ifdef DEBUG + fmt::print(stderr, "[Warning] Failed to read DICOM_PIXEL_SPACING: {}\n", values); + #endif // DEBUG + return; + } + pixel_spacings.emplace_back(std::pair{ spacing_x, spacing_y }); + } + + double spacing_x_l0 = pixel_spacings[0].first; + double spacing_y_l0 = pixel_spacings[0].second; + + uint32_t width_l0 = first_ifd->width(); + uint32_t height_l0 = first_ifd->height(); + + uint16_t spacing_index = 1; + for (int index = 1, level_index = 1; index < ifd_count; ++index, ++level_index) + { + auto& ifd = ifds_[index]; + + // Check if this IFD is an associated image (macro/label) based on ImageDescription + // NOTE: In Philips TIFF, pyramid levels can be strip-based (tile_width==0) + // So we can't use tile_width to identify associated images + auto& image_desc = ifd->image_description(); + bool is_macro = (std::mismatch(macro_prefix.begin(), macro_prefix.end(), image_desc.begin()).first == macro_prefix.end()); + bool is_label = (std::mismatch(label_prefix.begin(), label_prefix.end(), image_desc.begin()).first == label_prefix.end()); + + if (is_macro || is_label) + { + // This is an associated image - add to associated_images_ map + AssociatedImageBufferDesc buf_desc{}; + buf_desc.type = AssociatedImageBufferType::IFD; + buf_desc.compression = static_cast(ifd->compression()); + buf_desc.ifd_index = index; + + if (is_macro) + { + associated_images_.emplace("macro", buf_desc); + } + else if (is_label) + { + associated_images_.emplace("label", buf_desc); + } + + // Remove item at index `ifd_index` from `level_to_ifd_idx_` + level_to_ifd_idx_.erase(level_to_ifd_idx_.begin() + level_index); + --level_index; + continue; + } + + // This is a pyramid level - calculate downsample and fix dimensions + if (spacing_index < pixel_spacings.size()) + { + double downsample = std::round((pixel_spacings[spacing_index].first / spacing_x_l0 + + pixel_spacings[spacing_index].second / spacing_y_l0) / + 2); + // Fix width and height of IFD + ifd->width_ = width_l0 / downsample; + ifd->height_ = height_l0 / downsample; + ++spacing_index; + } + else + { + // No pixel spacing metadata for this level - calculate from actual dimensions + #ifdef DEBUG + fmt::print(" ℹ️ No DICOM_PIXEL_SPACING for IFD[{}], using actual dimensions\n", index); + #endif + // Keep the actual dimensions from nvImageCodec + } + } + + constexpr int associated_image_type_count = 2; + pugi::xpath_query ASSOCIATED_IMAGES[associated_image_type_count] = { + pugi::xpath_query( + "Attribute[@Name='PIM_DP_SCANNED_IMAGES']/Array/DataObject[Attribute/@Name='PIM_DP_IMAGE_TYPE' and Attribute/text()='MACROIMAGE'][1]/Attribute[@Name='PIM_DP_IMAGE_DATA']"), + pugi::xpath_query( + "Attribute[@Name='PIM_DP_SCANNED_IMAGES']/Array/DataObject[Attribute/@Name='PIM_DP_IMAGE_TYPE' and Attribute/text()='LABELIMAGE'][1]/Attribute[@Name='PIM_DP_IMAGE_DATA']") + }; + constexpr const char* associated_image_names[associated_image_type_count] = { "macro", "label" }; + + // Add associated image from XML if available (macro and label images) + // : Refer to PIM_DP_IMAGE_TYPE in + // https://www.openpathology.philips.com/wp-content/uploads/isyntax/4522%20207%2043941_2020_04_24%20Pathology%20iSyntax%20image%20format.pdf + + for (int associated_image_type_idx = 0; associated_image_type_idx < associated_image_type_count; + ++associated_image_type_idx) + { + pugi::xpath_node associated_node = ASSOCIATED_IMAGES[associated_image_type_idx].evaluate_node(data_object); + const char* associated_image_name = associated_image_names[associated_image_type_idx]; + + // If the associated image doesn't exist + if (associated_images_.find(associated_image_name) == associated_images_.end()) + { + if (associated_node) + { + auto node_offset = associated_node.node().offset_debug(); + + if (node_offset >= 0) + { + // `image_desc_cstr[node_offset]` would point to the following text: + // Attribute Element="0x1004" Group="0x301D" Name="PIM_DP_IMAGE_DATA" PMSVR="IString"> + // (base64-encoded JPEG image) + // + // + + // 34 is from `Attribute Name="PIM_DP_IMAGE_DATA"` + char* data_ptr = const_cast(image_desc_cstr) + node_offset + 34; + uint32_t data_len = 0; + while (*data_ptr != '>' && *data_ptr != '\0') + { + ++data_ptr; + } + if (*data_ptr != '\0') + { + ++data_ptr; // start of base64-encoded data + char* data_end_ptr = data_ptr; + // Seek until it finds '<' for '' + while (*data_end_ptr != '<' && *data_end_ptr != '\0') + { + ++data_end_ptr; + } + data_len = data_end_ptr - data_ptr; + } + + if (data_len > 0) + { + AssociatedImageBufferDesc buf_desc{}; + buf_desc.type = AssociatedImageBufferType::IFD_IMAGE_DESC; + buf_desc.compression = cucim::codec::CompressionMethod::JPEG; + buf_desc.desc_ifd_index = 0; + buf_desc.desc_offset = data_ptr - image_desc_cstr; + buf_desc.desc_size = data_len; + + associated_images_.emplace(associated_image_name, buf_desc); + } + } + } + } + } + + // Set TIFF type + tiff_type_ = TiffType::Philips; + + // Set background color + background_value_ = 0xFF; + + // Get metadata + if (json_metadata) + { + json philips_metadata; + parse_philips_tiff_metadata(data_object, philips_metadata, nullptr, PhilipsMetadataStage::ROOT); + parse_philips_tiff_metadata( + wsi_nodes[0].node(), philips_metadata, nullptr, PhilipsMetadataStage::SCANNED_IMAGE); + (*json_metadata).emplace("philips", std::move(philips_metadata)); + } + } +} + +void TIFF::_populate_aperio_svs_metadata(uint16_t ifd_count, void* metadata, std::shared_ptr& first_ifd) +{ + (void)ifd_count; + (void)metadata; + (void)first_ifd; + json* json_metadata = reinterpret_cast(metadata); + (void)json_metadata; + + int32_t non_tile_image_count = 0; + + // Append associated images + // NOTE: For Aperio SVS, associated images are identified by SubfileType: + // - SubfileType=0 at index 1: thumbnail (reduced resolution copy) + // - SubfileType=1: label image + // - SubfileType=9: macro image + // Pyramid levels typically have SubfileType=0 and are tiled, but may be strip-based + for (int index = 1, level_index = 1; index < ifd_count; ++index, ++level_index) + { + auto& ifd = ifds_[index]; + uint64_t subfile_type = ifd->subfile_type(); + + // Check if this is an associated image based on SubfileType + bool is_associated = false; + std::string associated_name; + + if (index == 1 && subfile_type == 0 && ifd->tile_width() == 0) + { + // First non-main IFD with SubfileType=0 and strip-based: likely thumbnail + is_associated = true; + associated_name = "thumbnail"; + } + else if (subfile_type == 1) + { + // SubfileType=1: label image + is_associated = true; + associated_name = "label"; + } + else if (subfile_type == 9) + { + // SubfileType=9: macro image + is_associated = true; + associated_name = "macro"; + } + + if (is_associated) + { + ++non_tile_image_count; + AssociatedImageBufferDesc buf_desc{}; + buf_desc.type = AssociatedImageBufferType::IFD; + buf_desc.compression = static_cast(ifd->compression()); + buf_desc.ifd_index = index; + associated_images_.emplace(associated_name, buf_desc); + + // Remove from pyramid levels + level_to_ifd_idx_.erase(level_to_ifd_idx_.begin() + level_index); + --level_index; + continue; + } + // If not associated, keep as pyramid level (even if strip-based) + } + + // Set TIFF type + tiff_type_ = TiffType::Aperio; + + // Set background color + background_value_ = 0xFF; + + // Get metadata + if (json_metadata) + { + json aperio_metadata; + parse_aperio_svs_metadata(first_ifd, aperio_metadata); + (*json_metadata).emplace("aperio", std::move(aperio_metadata)); + } +} + +bool TIFF::read(const cucim::io::format::ImageMetadataDesc* metadata, + const cucim::io::format::ImageReaderRegionRequestDesc* request, + cucim::io::format::ImageDataDesc* out_image_data, + cucim::io::format::ImageMetadataDesc* out_metadata) +{ + PROF_SCOPED_RANGE(PROF_EVENT(tiff_read)); + if (request->associated_image_name) + { + // 'out_metadata' is only needed for reading associated image + return read_associated_image(metadata, request, out_image_data, out_metadata); + } + + const int32_t ndim = request->size_ndim; + const uint64_t location_len = request->location_len; + + if (request->level >= level_to_ifd_idx_.size()) + { + throw std::invalid_argument(fmt::format( + "Invalid level ({}) in the request! (Should be < {})", request->level, level_to_ifd_idx_.size())); + } + auto main_ifd = ifds_[level_to_ifd_idx_[0]]; + auto ifd = ifds_[level_to_ifd_idx_[request->level]]; + auto original_img_width = main_ifd->width(); + auto original_img_height = main_ifd->height(); + + for (int32_t i = 0; i < ndim; ++i) + { + if (request->size[i] <= 0) + { + throw std::invalid_argument( + fmt::format("Invalid size ({}) in the request! (Should be > 0)", request->size[i])); + } + } + if (request->size[0] > original_img_width) + { + throw std::invalid_argument( + fmt::format("Invalid size (it exceeds the original image width {})", original_img_width)); + } + if (request->size[1] > original_img_height) + { + throw std::invalid_argument( + fmt::format("Invalid size (it exceeds the original image height {})", original_img_height)); + } + + float downsample_factor = metadata->resolution_info.level_downsamples[request->level]; + + // Change request based on downsample factor. (normalized value at level-0 -> real location at the requested level) + for (int64_t i = ndim * location_len - 1; i >= 0; --i) + { + request->location[i] /= downsample_factor; + } + return ifd->read(this, metadata, request, out_image_data); +} + +bool TIFF::read_associated_image(const cucim::io::format::ImageMetadataDesc* metadata, + const cucim::io::format::ImageReaderRegionRequestDesc* request, + cucim::io::format::ImageDataDesc* out_image_data, + cucim::io::format::ImageMetadataDesc* out_metadata_desc) +{ + PROF_SCOPED_RANGE(PROF_EVENT(tiff_read_associated_image)); + // TODO: implement + (void)metadata; + + std::string device_name(request->device); + if (request->shm_name) + { + device_name = device_name + fmt::format("[{}]", request->shm_name); // TODO: check performance + } + cucim::io::Device out_device(device_name); + + uint8_t* raster = nullptr; + size_t raster_size = 0; + uint32_t width = 0; + uint32_t height = 0; + uint32_t samples_per_pixel = 0; + + // Raw metadata for the associated image + const char* raw_data_ptr = nullptr; + size_t raw_data_len = 0; + // Json metadata for the associated image + char* json_data_ptr = nullptr; + + auto associated_image = associated_images_.find(request->associated_image_name); + if (associated_image != associated_images_.end()) + { + auto& buf_desc = associated_image->second; + + switch (buf_desc.type) + { + case AssociatedImageBufferType::IFD: { + const auto& image_ifd = ifd(buf_desc.ifd_index); + + auto& image_description = image_ifd->image_description(); + auto image_description_size = image_description.size(); + + // Assign image description into raw_data_ptr + raw_data_ptr = image_description.c_str(); + raw_data_len = image_description_size; + + width = image_ifd->width_; + height = image_ifd->height_; + samples_per_pixel = image_ifd->samples_per_pixel_; + raster_size = width * height * samples_per_pixel; + + uint16_t compression_method = image_ifd->compression_; + + if (compression_method != COMPRESSION_JPEG && compression_method != COMPRESSION_LZW) + { + #ifdef DEBUG + fmt::print(stderr, + "[Error] Unsupported compression method in read_associated_image()! (compression: {})\n", + compression_method); + #endif // DEBUG + return false; + } + + // REMOVED: Legacy CPU decoder code for strips + // In a pure nvImageCodec build, associated images should use nvImageCodec decoding + // This legacy strip-based decoding path should not be used + throw std::runtime_error(fmt::format( + "INTERNAL ERROR: Legacy strip-based CPU decoder path reached. " + "This should not happen in nvImageCodec build. " + "Compression method: {}, IFD index: {}. " + "Associated images should be decoded via nvImageCodec.", + compression_method, buf_desc.desc_ifd_index)); + break; + } + case AssociatedImageBufferType::IFD_IMAGE_DESC: { + // REMOVED: Legacy CPU decoder code for base64-encoded JPEG in ImageDescription + // In a pure nvImageCodec build, this path should not be used + // Base64-encoded images in metadata should be decoded via nvImageCodec + throw std::runtime_error( + "INTERNAL ERROR: Legacy IFD_IMAGE_DESC CPU decoder path reached. " + "This should not happen in nvImageCodec build. " + "Base64-encoded associated images should be decoded via nvImageCodec."); + } + case AssociatedImageBufferType::FILE_OFFSET: + // TODO: implement + break; + case AssociatedImageBufferType::BUFFER_POINTER: + // TODO: implement + break; + case AssociatedImageBufferType::OWNED_BUFFER_POINTER: + // TODO: implement + break; + } + } + + // Populate image data + const uint16_t ndim = 3; + + int64_t* container_shape = static_cast(cucim_malloc(sizeof(int64_t) * ndim)); + container_shape[0] = height; + container_shape[1] = width; + container_shape[2] = 3; // TODO: hard-coded for 'C' + + // Copy the raster memory and free it if needed. + cucim::memory::move_raster_from_host((void**)&raster, raster_size, out_device); + + auto& out_image_container = out_image_data->container; + out_image_container.data = raster; + out_image_container.device = DLDevice{ static_cast(out_device.type()), out_device.index() }; + out_image_container.ndim = ndim; + out_image_container.dtype = { kDLUInt, 8, 1 }; + out_image_container.shape = container_shape; + out_image_container.strides = nullptr; // Tensor is compact and row-majored + out_image_container.byte_offset = 0; + + auto& shm_name = out_device.shm_name(); + size_t shm_name_len = shm_name.size(); + if (shm_name_len != 0) + { + out_image_data->shm_name = static_cast(cucim_malloc(shm_name_len + 1)); + memcpy(out_image_data->shm_name, shm_name.c_str(), shm_name_len + 1); + } + else + { + out_image_data->shm_name = nullptr; + } + + // Populate metadata + if (out_metadata_desc && out_metadata_desc->handle) + { + cucim::io::format::ImageMetadata& out_metadata = + *reinterpret_cast(out_metadata_desc->handle); + auto& resource = out_metadata.get_resource(); + + std::string_view dims{ "YXC" }; + + std::pmr::vector shape(&resource); + shape.reserve(ndim); + shape.insert(shape.end(), &container_shape[0], &container_shape[ndim]); + + DLDataType dtype{ kDLUInt, 8, 1 }; + + // TODO: Do not assume channel names as 'RGB' + std::pmr::vector channel_names( + { std::string_view{ "R" }, std::string_view{ "G" }, std::string_view{ "B" } }, &resource); + + + // We don't know physical pixel size for associated image so fill it with default value 1 + std::pmr::vector spacing(&resource); + spacing.reserve(ndim); + spacing.insert(spacing.end(), ndim, 1.0); + + std::pmr::vector spacing_units(&resource); + spacing_units.reserve(ndim); + spacing_units.emplace_back(std::string_view{ "micrometer" }); + spacing_units.emplace_back(std::string_view{ "micrometer" }); + spacing_units.emplace_back(std::string_view{ "color" }); + + std::pmr::vector origin({ 0.0, 0.0, 0.0 }, &resource); + + // Direction cosines (size is always 3x3) + // clang-format off + std::pmr::vector direction({ 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0}, &resource); + // clang-format on + + // The coordinate frame in which the direction cosines are measured (either 'LPS'(ITK/DICOM) or 'RAS'(NIfTI/3D + // Slicer)) + std::string_view coord_sys{ "LPS" }; + + // Manually set resolution dimensions to 2 + const uint16_t level_ndim = 2; + std::pmr::vector level_dimensions(&resource); + level_dimensions.reserve(level_ndim * 1); // it has only one size + level_dimensions.emplace_back(shape[1]); // width + level_dimensions.emplace_back(shape[0]); // height + + std::pmr::vector level_downsamples(&resource); + level_downsamples.reserve(1); + level_downsamples.emplace_back(1.0); + + std::pmr::vector level_tile_sizes(&resource); + level_tile_sizes.reserve(level_ndim * 1); // it has only one size + level_tile_sizes.emplace_back(shape[1]); // tile_width + level_tile_sizes.emplace_back(shape[0]); // tile_height + + // Empty associated images + const size_t associated_image_count = 0; + std::pmr::vector associated_image_names(&resource); + + std::string_view raw_data{ raw_data_ptr ? raw_data_ptr : "", raw_data_len }; + std::string_view json_data{ json_data_ptr ? json_data_ptr : "" }; + + out_metadata.ndim(ndim); + out_metadata.dims(std::move(dims)); + out_metadata.shape(std::move(shape)); + out_metadata.dtype(dtype); + out_metadata.channel_names(std::move(channel_names)); + out_metadata.spacing(std::move(spacing)); + out_metadata.spacing_units(std::move(spacing_units)); + out_metadata.origin(std::move(origin)); + out_metadata.direction(std::move(direction)); + out_metadata.coord_sys(std::move(coord_sys)); + out_metadata.level_count(1); + out_metadata.level_ndim(2); + out_metadata.level_dimensions(std::move(level_dimensions)); + out_metadata.level_downsamples(std::move(level_downsamples)); + out_metadata.level_tile_sizes(std::move(level_tile_sizes)); + out_metadata.image_count(associated_image_count); + out_metadata.image_names(std::move(associated_image_names)); + out_metadata.raw_data(raw_data); + out_metadata.json_data(json_data); + } + + return true; +} + +cucim::filesystem::Path TIFF::file_path() const +{ + return file_path_; +} + +std::shared_ptr& TIFF::file_handle() +{ + return file_handle_shared_; +} + +const std::vector& TIFF::ifd_offsets() const +{ + return ifd_offsets_; +} +std::shared_ptr TIFF::ifd(size_t index) const +{ + return ifds_.at(index); +} +std::shared_ptr TIFF::level_ifd(size_t level_index) const +{ + return ifds_.at(level_to_ifd_idx_.at(level_index)); +} +size_t TIFF::ifd_count() const +{ + return ifd_offsets_.size(); +} +size_t TIFF::level_count() const +{ + return level_to_ifd_idx_.size(); +} +const std::map& TIFF::associated_images() const +{ + return associated_images_; +} +size_t TIFF::associated_image_count() const +{ + return associated_images_.size(); +} +bool TIFF::is_big_endian() const +{ + return is_big_endian_; +} + +uint64_t TIFF::read_config() const +{ + return read_config_; +} + +bool TIFF::is_in_read_config(uint64_t configs) const +{ + return (read_config_ & configs) == configs; +} + +void TIFF::add_read_config(uint64_t configs) +{ + read_config_ |= configs; +} + +TiffType TIFF::tiff_type() +{ + return tiff_type_; +} + +std::string TIFF::metadata() +{ + json* metadata = reinterpret_cast(metadata_); + + if (metadata) + { + return metadata->dump(); + } + else + { + return std::string{}; + } +} + +void* TIFF::operator new(std::size_t sz) +{ + return cucim_malloc(sz); +} + +void TIFF::operator delete(void* ptr) +{ + cucim_free(ptr); +} +} // namespace cuslide::tiff diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/tiff.h b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/tiff.h new file mode 100644 index 000000000..459576d49 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/tiff.h @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef CUSLIDE_TIFF_H +#define CUSLIDE_TIFF_H + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "ifd.h" +#include "types.h" +#include "cuslide/nvimgcodec/nvimgcodec_tiff_parser.h" + +namespace cuslide::tiff +{ + +/** + * TIFF file handler class. + * + * This class doesn't use PImpl idiom for performance reasons and is not + * intended to be used for subclassing. + */ +class EXPORT_VISIBLE TIFF : public std::enable_shared_from_this +{ +public: + // nvImageCodec constructors (primary - no libtiff mode parameter) + TIFF(const cucim::filesystem::Path& file_path); + TIFF(const cucim::filesystem::Path& file_path, uint64_t read_config); + static std::shared_ptr open(const cucim::filesystem::Path& file_path); + static std::shared_ptr open(const cucim::filesystem::Path& file_path, uint64_t config); + + // Legacy libtiff-style constructors (for compatibility if needed) + TIFF(const cucim::filesystem::Path& file_path, int mode); + TIFF(const cucim::filesystem::Path& file_path, int mode, uint64_t config); + static std::shared_ptr open(const cucim::filesystem::Path& file_path, int mode); + static std::shared_ptr open(const cucim::filesystem::Path& file_path, int mode, uint64_t config); + + void close(); + void construct_ifds(); + + /** + * Resolve vendor format and fix values for `associated_image_descs_` and `level_to_ifd_idx_. + */ + void resolve_vendor_format(); + bool read(const cucim::io::format::ImageMetadataDesc* metadata, + const cucim::io::format::ImageReaderRegionRequestDesc* request, + cucim::io::format::ImageDataDesc* out_image_data, + cucim::io::format::ImageMetadataDesc* out_metadata = nullptr); + + bool read_associated_image(const cucim::io::format::ImageMetadataDesc* metadata, + const cucim::io::format::ImageReaderRegionRequestDesc* request, + cucim::io::format::ImageDataDesc* out_image_data, + cucim::io::format::ImageMetadataDesc* out_metadata); + + cucim::filesystem::Path file_path() const; + std::shared_ptr& file_handle(); + const std::vector& ifd_offsets() const; + std::shared_ptr ifd(size_t index) const; + std::shared_ptr level_ifd(size_t level_index) const; + size_t ifd_count() const; + size_t level_count() const; + const std::map& associated_images() const; + size_t associated_image_count() const; + bool is_big_endian() const; + uint64_t read_config() const; + bool is_in_read_config(uint64_t configs) const; + void add_read_config(uint64_t configs); + TiffType tiff_type(); + std::string metadata(); + + ~TIFF(); + + static void* operator new(std::size_t sz); + static void operator delete(void* ptr); + // static void* operator new[](std::size_t sz); + // static void operator delete(void* ptr, std::size_t sz); + // static void operator delete[](void* ptr, std::size_t sz); + + // const values for read_configs_ + static constexpr uint64_t kUseLibTiff = 1 << 1; + + // Make IFD available to access private members of TIFF + friend class IFD; + +private: + // UPDATED: These now use nvImageCodec TiffFileParser instead of libtiff + void _populate_philips_tiff_metadata(uint16_t ifd_count, void* metadata, std::shared_ptr& first_ifd); + void _populate_aperio_svs_metadata(uint16_t ifd_count, void* metadata, std::shared_ptr& first_ifd); + + cucim::filesystem::Path file_path_; + std::shared_ptr file_handle_shared_; + std::vector ifd_offsets_; /// IFD offset for an index (IFD index) + std::vector> ifds_; /// IFD object for an index (IFD index) + /// nvImageCodec TIFF parser - MUST be destroyed BEFORE ifds_ to avoid double-free of sub-code streams + /// Placed AFTER ifds_ so it's destroyed FIRST (reverse declaration order) + std::unique_ptr nvimgcodec_parser_; + std::vector level_to_ifd_idx_; + // note: we use std::map instead of std::unordered_map as # of associated_image would be usually less than 10. + std::map associated_images_; + bool is_big_endian_ = false; /// if big endian + uint8_t background_value_ = 0x00; /// background_value + uint64_t read_config_ = 0; + TiffType tiff_type_ = TiffType::Generic; + void* metadata_ = nullptr; + + mutable std::once_flag slow_path_warning_flag_; +}; +} // namespace cuslide::tiff + +#endif // CUSLIDE_TIFF_H diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/tiff_constants.h b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/tiff_constants.h new file mode 100644 index 000000000..45bed33de --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/tiff_constants.h @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef CUSLIDE_TIFF_CONSTANTS_H +#define CUSLIDE_TIFF_CONSTANTS_H + +#include + +/** + * TIFF constants extracted from libtiff headers. + * These are standard TIFF specification values that don't change. + * We define them here so we don't need libtiff headers. + */ + +namespace cuslide::tiff { + +// TIFF Tags +constexpr uint32_t TIFFTAG_SOFTWARE = 305; +constexpr uint32_t TIFFTAG_MODEL = 272; +constexpr uint32_t TIFFTAG_IMAGEDESCRIPTION = 270; +constexpr uint32_t TIFFTAG_RESOLUTIONUNIT = 296; +constexpr uint32_t TIFFTAG_XRESOLUTION = 282; +constexpr uint32_t TIFFTAG_YRESOLUTION = 283; +constexpr uint32_t TIFFTAG_PREDICTOR = 317; +constexpr uint32_t TIFFTAG_JPEGTABLES = 347; + +// TIFF Compression Types +constexpr uint16_t COMPRESSION_NONE = 1; +constexpr uint16_t COMPRESSION_LZW = 5; +constexpr uint16_t COMPRESSION_JPEG = 7; +constexpr uint16_t COMPRESSION_DEFLATE = 8; +constexpr uint16_t COMPRESSION_ADOBE_DEFLATE = 32946; + +// Aperio JPEG2000 compression (vendor-specific) +constexpr uint16_t COMPRESSION_APERIO_JP2K_YCBCR = 33003; +constexpr uint16_t COMPRESSION_APERIO_JP2K_RGB = 33005; + +// TIFF Photometric Interpretation +constexpr uint16_t PHOTOMETRIC_RGB = 2; +constexpr uint16_t PHOTOMETRIC_YCBCR = 6; + +// TIFF Planar Configuration +constexpr uint16_t PLANARCONFIG_CONTIG = 1; +constexpr uint16_t PLANARCONFIG_SEPARATE = 2; + +// TIFF Flags +constexpr uint32_t TIFF_ISTILED = 0x00000004; + +} // namespace cuslide::tiff + +#endif // CUSLIDE_TIFF_CONSTANTS_H diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/types.h b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/types.h new file mode 100644 index 000000000..af8e58588 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/types.h @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2020-2021, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef CUSLIDE_TYPES_H +#define CUSLIDE_TYPES_H + +#include + +#include + +namespace cuslide::tiff +{ + +using ifd_offset_t = uint64_t; + +enum class TiffType : uint32_t +{ + Generic = 0, + Philips = 1, + Aperio = 2, +}; + +enum class AssociatedImageBufferType : uint8_t +{ + IFD = 0, + IFD_IMAGE_DESC = 1, + FILE_OFFSET = 2, + BUFFER_POINTER = 3, + OWNED_BUFFER_POINTER = 4, +}; + +struct AssociatedImageBufferDesc +{ + AssociatedImageBufferType type; /// 0: IFD index, 1: IFD index + image description offset&size (base64-encoded text) + /// 2: file offset + size, 3: buffer pointer (owned by others) + size + /// 4: allocated (owned) buffer pointer (so need to free after use) + size + cucim::codec::CompressionMethod compression; + union + { + ifd_offset_t ifd_index; + struct + { + ifd_offset_t desc_ifd_index; + uint64_t desc_offset; + uint64_t desc_size; + }; + struct + { + uint64_t file_offset; + uint64_t file_size; + }; + struct + { + void* buf_ptr; + uint64_t buf_size; + }; + struct + { + void* owned_ptr; + uint64_t owned_size; + }; + }; +}; + + +} // namespace cuslide::tiff + +#endif // CUSLIDE_TYPES_H diff --git a/cpp/plugins/cucim.kit.cuslide2/test_data b/cpp/plugins/cucim.kit.cuslide2/test_data new file mode 120000 index 000000000..6c6c77553 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/test_data @@ -0,0 +1 @@ +../../../test_data \ No newline at end of file diff --git a/cpp/plugins/cucim.kit.cuslide2/tests/CMakeLists.txt b/cpp/plugins/cucim.kit.cuslide2/tests/CMakeLists.txt new file mode 100644 index 000000000..542cc9a81 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/tests/CMakeLists.txt @@ -0,0 +1,59 @@ +# +# cmake-format: off +# SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. +# SPDX-License-Identifier: Apache-2.0 +# cmake-format: on +# + +include(CTest) +enable_testing() + +################################################################################ +# Add executable: cuslide_tests +################################################################################ +add_executable(cuslide_tests + config.h + main.cpp + test_read_region.cpp + # test_read_rawtiff.cpp # Disabled: requires libtiff-specific write_offsets_() method not available in nvImageCodec + test_philips_tiff.cpp + ) +set_target_properties(cuslide_tests + PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO +) +target_compile_features(cuslide_tests PRIVATE ${CUCIM_REQUIRED_FEATURES}) +# Use generator expression to avoid `nvcc fatal : Value '-std=c++17' is not defined for option 'Werror'` +target_compile_options(cuslide_tests PRIVATE $<$:-Werror -Wall -Wextra>) +target_compile_definitions(cuslide_tests + PUBLIC + CUSLIDE_VERSION=${PROJECT_VERSION} + CUSLIDE_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} + CUSLIDE_VERSION_MINOR=${PROJECT_VERSION_MINOR} + CUSLIDE_VERSION_PATCH=${PROJECT_VERSION_PATCH} + CUSLIDE_VERSION_BUILD=${PROJECT_VERSION_BUILD} +) +target_link_libraries(cuslide_tests + PRIVATE + CUDA::cudart + cucim::cucim + ${CUCIM_PLUGIN_NAME} + deps::catch2 + deps::openslide + deps::cli11 + deps::fmt + ) + +# Add headers in src +target_include_directories(cuslide_tests + PUBLIC + $ + ) + +include(Catch) +# See https://github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md#catchcmake-and-catchaddtestscmake for other options +# Do not use catch_discover_tests() since it causes a test to be run at build time +# and somehow it causes a deadlock during the build. +# catch_discover_tests(cuslide_tests) diff --git a/cpp/plugins/cucim.kit.cuslide2/tests/config.h b/cpp/plugins/cucim.kit.cuslide2/tests/config.h new file mode 100644 index 000000000..6767d9183 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/tests/config.h @@ -0,0 +1,52 @@ +/* + * Apache License, Version 2.0 + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef CUSLIDE_TESTS_CONFIG_H +#define CUSLIDE_TESTS_CONFIG_H + +#include +#include + +struct AppConfig +{ + std::string test_folder; + std::string test_file; + std::string temp_folder = "/tmp"; + std::string get_input_path(const std::string default_value = "generated/tiff_stripe_4096x4096_256.tif") const + { + // If `test_file` is absolute path + if (!test_folder.empty() && test_file.substr(0, 1) == "/") + { + return test_file; + } + else + { + std::string test_data_folder = test_folder; + if (test_data_folder.empty()) + { + if (const char* env_p = std::getenv("CUCIM_TESTDATA_FOLDER")) + { + test_data_folder = env_p; + } + else + { + test_data_folder = "test_data"; + } + } + if (test_file.empty()) + { + return test_data_folder + "/" + default_value; + } + else + { + return test_data_folder + "/" + test_file; + } + } + } +}; + +extern AppConfig g_config; + +#endif // CUSLIDE_TESTS_CONFIG_H diff --git a/cpp/plugins/cucim.kit.cuslide2/tests/main.cpp b/cpp/plugins/cucim.kit.cuslide2/tests/main.cpp new file mode 100644 index 000000000..1a24e09b5 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/tests/main.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2021, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// #define CATCH_CONFIG_MAIN +// #include + +// Implement main explicitly to handle additional parameters. +#define CATCH_CONFIG_RUNNER +#include "config.h" +#include "cucim/core/framework.h" + +#include +#include +#include +#include + +CUCIM_FRAMEWORK_GLOBALS("sample.app") + +// Global config object +AppConfig g_config; + +/** + * Extract `--[option]` or `--[option]=` string from command and set the value to g_config object. + * + * @param argc number of arguments used for command + * @param argv arguments for command + * @param obj object reference to modify + * @param argument name of argument(option) + * @return true if it extracted the value for the option + */ +static bool extract_test_file_option(int* argc, char** argv, std::string& obj, const char* argument) +{ + std::string arg_str = fmt::format("--{}=", argument); // test_file => --test_file= + std::string arg_str2 = fmt::format("--{}", argument); // test_file => --test_file + + char* value_ptr = nullptr; + for (int i = 1; argc && i < *argc; ++i) + { + if (strncmp(argv[i], arg_str.c_str(), arg_str.size()) == 0) + { + value_ptr = &argv[i][arg_str.size()]; + for (int j = i + 1; argc && j < *argc; ++j) + { + argv[j - 1] = argv[j]; + } + --(*argc); + argv[*argc] = nullptr; + break; + } + if (strncmp(argv[i], arg_str2.c_str(), arg_str2.size()) == 0 && i + 1 < *argc) + { + value_ptr = argv[i + 1]; + for (int j = i + 2; argc && j < *argc; ++j) + { + argv[j - 2] = argv[j]; + } + *argc -= 2; + argv[*argc] = nullptr; + argv[*argc + 1] = nullptr; + break; + } + } + + if (value_ptr) { + obj = value_ptr; + return true; + } + else { + return false; + } +} + +int main (int argc, char** argv) { + extract_test_file_option(&argc, argv, g_config.test_folder, "test_folder"); + extract_test_file_option(&argc, argv, g_config.test_file, "test_file"); + extract_test_file_option(&argc, argv, g_config.temp_folder, "temp_folder"); + printf("Target test folder: %s (use --test_folder option to change this)\n", g_config.test_folder.c_str()); + printf("Target test file : %s (use --test_file option to change this)\n", g_config.test_file.c_str()); + printf("Temp folder : %s (use --temp_folder option to change this)\n", g_config.temp_folder.c_str()); + int result = Catch::Session().run(argc, argv); + return result; +} diff --git a/cpp/plugins/cucim.kit.cuslide2/tests/test_philips_tiff.cpp b/cpp/plugins/cucim.kit.cuslide2/tests/test_philips_tiff.cpp new file mode 100644 index 000000000..82fb5ddb7 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/tests/test_philips_tiff.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "cuslide/tiff/tiff.h" +#include "config.h" + +#include +#include + +TEST_CASE("Verify philips tiff file", "[test_philips_tiff.cpp]") +{ + + auto tif = std::make_shared(g_config.get_input_path("private/philips_tiff_000.tif").c_str(), + O_RDONLY); // , cuslide::tiff::TIFF::kUseLibTiff + tif->construct_ifds(); + + int64_t test_sx = 0; + int64_t test_sy = 0; + + int64_t test_width = 500; + int64_t test_height = 500; + + cucim::io::format::ImageMetadata metadata{}; + cucim::io::format::ImageReaderRegionRequestDesc request{}; + cucim::io::format::ImageDataDesc image_data{}; + + metadata.level_count(1).level_downsamples({ 1.0 }).level_ndim(3); + + int64_t request_location[2] = { test_sx, test_sy }; + request.location = request_location; + request.level = 0; + int64_t request_size[2] = { test_width, test_height }; + request.size = request_size; + request.device = const_cast("cpu"); + + tif->read(&metadata.desc(), &request, &image_data); + + request.associated_image_name = const_cast("label"); + tif->read(&metadata.desc(), &request, &image_data, nullptr /*out_metadata*/); + + tif->close(); + + REQUIRE(1 == 1); +} diff --git a/cpp/plugins/cucim.kit.cuslide2/tests/test_read_rawtiff.cpp b/cpp/plugins/cucim.kit.cuslide2/tests/test_read_rawtiff.cpp new file mode 100644 index 000000000..819e801c5 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/tests/test_read_rawtiff.cpp @@ -0,0 +1,385 @@ +/* + * Copyright (c) 2020, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "cuslide/tiff/tiff.h" +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ALIGN_UP(x, align_to) (((uint64_t)(x) + ((uint64_t)(align_to)-1)) & ~((uint64_t)(align_to)-1)) +#define ALIGN_DOWN(x, align_to) ((uint64_t)(x) & ~((uint64_t)(align_to)-1)) + +#define CUDA_ERROR(stmt) \ + { \ + cuda_status = stmt; \ + if (cudaSuccess != cuda_status) \ + { \ + INFO(fmt::format("Error message: {}", cudaGetErrorString(cuda_status))); \ + REQUIRE(cudaSuccess == cuda_status); \ + } \ + } + +#define POSIX_ERROR(stmt) \ + { \ + err = stmt; \ + if (err < 0) \ + { \ + INFO(fmt::format("Error message: {}", std::strerror(errno))); \ + REQUIRE(err >= 0); \ + } \ + } + +static void shuffle_offsets(uint32_t count, uint64_t* offsets, uint64_t* bytecounts) +{ + // Fisher-Yates shuffle + for (uint32_t i = 0; i < count; ++i) + { + int j = (std::rand() % (count - i)) + i; + std::swap(offsets[i], offsets[j]); + std::swap(bytecounts[i], bytecounts[j]); + } +} + +TEST_CASE("Verify raw tiff read", "[test_read_rawtiff.cpp]") +{ +// cudaError_t cuda_status; +// int err; + constexpr int BLOCK_SECTOR_SIZE = 4096; + constexpr bool SHUFFLE_LIST = true; + // constexpr int iter_max = 32; + // constexpr int skip_count = 2; + constexpr int iter_max = 1; + constexpr int skip_count = 0; + + std::srand(std::time(nullptr)); + + auto input_file = g_config.get_input_path(); + + struct stat sb; + auto fd_temp = ::open(input_file.c_str(), O_RDONLY); + fstat(fd_temp, &sb); + uint64_t test_file_size = sb.st_size; + ::close(fd_temp); + + auto tif = std::make_shared(input_file, + O_RDONLY); // , cuslide::tiff::TIFF::kUseLibTiff + tif->construct_ifds(); + tif->ifd(0)->write_offsets_(input_file.c_str()); + + + std::ifstream offsets(fmt::format("{}.offsets", input_file), std::ios::in | std::ios::binary); + std::ifstream bytecounts(fmt::format("{}.bytecounts", input_file), std::ios::in | std::ios::binary); + + // Read image piece count + uint32_t image_piece_count_ = 0; + offsets.read(reinterpret_cast(&image_piece_count_), sizeof(image_piece_count_)); + bytecounts.read(reinterpret_cast(&image_piece_count_), sizeof(image_piece_count_)); + + uint64_t image_piece_offsets_[image_piece_count_]; + uint64_t image_piece_bytecounts_[image_piece_count_]; + uint64_t min_bytecount = 9999999999; + uint64_t max_bytecount = 0; + uint64_t sum_bytecount = 0; + + uint64_t min_offset = 9999999999; + uint64_t max_offset = 0; + for (uint32_t i = 0; i < image_piece_count_; i++) + { + offsets.read((char*)&image_piece_offsets_[i], sizeof(image_piece_offsets_[i])); + bytecounts.read((char*)&image_piece_bytecounts_[i], sizeof(image_piece_bytecounts_[i])); + + min_bytecount = std::min(min_bytecount, image_piece_bytecounts_[i]); + max_bytecount = std::max(max_bytecount, image_piece_bytecounts_[i]); + sum_bytecount += image_piece_bytecounts_[i]; + + min_offset = std::min(min_offset, image_piece_offsets_[i]); + max_offset = std::max(max_offset, image_piece_offsets_[i] + image_piece_bytecounts_[i]); + } + bytecounts.close(); + offsets.close(); + + fmt::print("file_size : {}\n", test_file_size); + fmt::print("min_bytecount: {}\n", min_bytecount); + fmt::print("max_bytecount: {}\n", max_bytecount); + fmt::print("avg_bytecount: {}\n", static_cast(sum_bytecount) / image_piece_count_); + fmt::print("min_offset : {}\n", min_offset); + fmt::print("max_offset : {}\n", max_offset); + + // Shuffle offsets + if (SHUFFLE_LIST) + { + shuffle_offsets(image_piece_count_, image_piece_offsets_, image_piece_bytecounts_); + } + + // Allocate memory + uint8_t* unaligned_host = static_cast(malloc(test_file_size + BLOCK_SECTOR_SIZE * 2)); + uint8_t* buffer_host = static_cast(malloc(test_file_size + BLOCK_SECTOR_SIZE * 2)); + uint8_t* aligned_host = reinterpret_cast(ALIGN_UP(unaligned_host, BLOCK_SECTOR_SIZE)); + + // uint8_t* unaligned_device; + // CUDA_ERROR(cudaMalloc(&unaligned_device, test_file_size + BLOCK_SECTOR_SIZE)); + // uint8_t* aligned_device = reinterpret_cast(ALIGN_UP(unaligned_device, BLOCK_SECTOR_SIZE)); + // + // uint8_t* unaligned_device_host; + // CUDA_ERROR(cudaMallocHost(&unaligned_device_host, test_file_size + BLOCK_SECTOR_SIZE)); + // uint8_t* aligned_device_host = reinterpret_cast(ALIGN_UP(unaligned_device_host, BLOCK_SECTOR_SIZE)); + // + // uint8_t* unaligned_device_managed; + // CUDA_ERROR(cudaMallocManaged(&unaligned_device_managed, test_file_size + BLOCK_SECTOR_SIZE)); + // uint8_t* aligned_device_managed = reinterpret_cast(ALIGN_UP(unaligned_device_managed, + // BLOCK_SECTOR_SIZE)); + + cucim::filesystem::discard_page_cache(input_file.c_str()); + + fmt::print("count:{} \n", image_piece_count_); + + SECTION("Regular POSIX") + { + fmt::print("Regular POSIX\n"); + + double total_elapsed_time = 0; + for (int iter = 0; iter < iter_max; ++iter) + { + cucim::filesystem::discard_page_cache(input_file.c_str()); + auto fd = cucim::filesystem::open(input_file.c_str(), "rpn"); + { + cucim::logger::Timer timer("- read whole : {:.7f}\n", true, false); + + fd->pread(aligned_host, test_file_size, 0); + + double elapsed_time = timer.stop(); + if (iter >= skip_count) + { + total_elapsed_time += elapsed_time; + } + timer.print(); + } + } + fmt::print("- Read whole average: {}\n", total_elapsed_time / (iter_max - skip_count)); + + total_elapsed_time = 0; + for (int iter = 0; iter < iter_max; ++iter) + { + cucim::filesystem::discard_page_cache(input_file.c_str()); + auto fd = cucim::filesystem::open(input_file.c_str(), "rpn"); + { + cucim::logger::Timer timer("- read tiles : {:.7f}\n", true, false); + + for (uint32_t i = 0; i < image_piece_count_; ++i) + { + fd->pread(aligned_host, image_piece_bytecounts_[i], image_piece_offsets_[i]); + } + + double elapsed_time = timer.stop(); + if (iter >= skip_count) + { + total_elapsed_time += elapsed_time; + } + timer.print(); + } + } + fmt::print("- Read tiles average: {}\n", total_elapsed_time / (iter_max - skip_count)); + } + + SECTION("O_DIRECT") + { + fmt::print("O_DIRECT\n"); + + double total_elapsed_time = 0; + for (int iter = 0; iter < iter_max; ++iter) + { + cucim::filesystem::discard_page_cache(input_file.c_str()); + auto fd = cucim::filesystem::open(input_file.c_str(), "rp"); + { + cucim::logger::Timer timer("- read whole : {:.7f}\n", true, false); + + fd->pread(aligned_host, test_file_size, 0); + + double elapsed_time = timer.stop(); + if (iter >= skip_count) + { + total_elapsed_time += elapsed_time; + } + timer.print(); + } + } + fmt::print("- Read whole average: {}\n", total_elapsed_time / (iter_max - skip_count)); + + total_elapsed_time = 0; + for (int iter = 0; iter < iter_max; ++iter) + { + cucim::filesystem::discard_page_cache(input_file.c_str()); + auto fd = cucim::filesystem::open(input_file.c_str(), "rp"); + { + cucim::logger::Timer timer("- read tiles : {:.7f}\n", true, false); + + for (uint32_t i = 0; i < image_piece_count_; ++i) + { + fd->pread(buffer_host, image_piece_bytecounts_[i], image_piece_offsets_[i]); + } + + double elapsed_time = timer.stop(); + if (iter >= skip_count) + { + total_elapsed_time += elapsed_time; + } + timer.print(); + } + } + fmt::print("- Read tiles average: {}\n", total_elapsed_time / (iter_max - skip_count)); + } + + SECTION("O_DIRECT pre-load") + { + fmt::print("O_DIRECT pre-load\n"); + + size_t file_start_offset = ALIGN_DOWN(min_offset, BLOCK_SECTOR_SIZE); + size_t end_boundary_offset = ALIGN_UP(max_offset + max_bytecount, BLOCK_SECTOR_SIZE); + size_t large_block_size = end_boundary_offset - file_start_offset; + + fmt::print("- size:{}\n", end_boundary_offset - file_start_offset); + + double total_elapsed_time = 0; + for (int iter = 0; iter < iter_max; ++iter) + { + cucim::filesystem::discard_page_cache(input_file.c_str()); + auto fd = cucim::filesystem::open(input_file.c_str(), "rp"); + { + cucim::logger::Timer timer("- preload : {:.7f}\n", true, false); + + fd->pread(aligned_host, large_block_size, file_start_offset); + + double elapsed_time = timer.stop(); + if (iter >= skip_count) + { + total_elapsed_time += elapsed_time; + } + timer.print(); + } + } + fmt::print("- Preload average: {}\n", total_elapsed_time / (iter_max - skip_count)); + + total_elapsed_time = 0; + for (int iter = 0; iter < iter_max; ++iter) + { + cucim::filesystem::discard_page_cache(input_file.c_str()); + auto fd = cucim::filesystem::open(input_file.c_str(), "rp"); + { + cucim::logger::Timer timer("- read tiles : {:.7f}\n", true, false); + + for (uint32_t i = 0; i < image_piece_count_; ++i) + { + memcpy(buffer_host, aligned_host + image_piece_offsets_[i] - file_start_offset, + image_piece_bytecounts_[i]); + } + + double elapsed_time = timer.stop(); + if (iter >= skip_count) + { + total_elapsed_time += elapsed_time; + } + timer.print(); + } + } + fmt::print("- Read tiles average: {}\n", total_elapsed_time / (iter_max - skip_count)); + } + + SECTION("mmap") + { + fmt::print("mmap\n"); + + double total_elapsed_time = 0; + for (int iter = 0; iter < iter_max; ++iter) + { + cucim::filesystem::discard_page_cache(input_file.c_str()); + auto fd_mmap = open(input_file.c_str(), O_RDONLY); + { + cucim::logger::Timer timer("- open/close : {:.7f}\n", true, false); + + void* mmap_host = mmap((void*)0, test_file_size, PROT_READ, MAP_SHARED, fd_mmap, 0); + + REQUIRE(mmap_host != MAP_FAILED); + + if (mmap_host != MAP_FAILED) + { + REQUIRE(munmap(mmap_host, test_file_size) != -1); + close(fd_mmap); + } + + double elapsed_time = timer.stop(); + if (iter >= skip_count) + { + total_elapsed_time += elapsed_time; + } + timer.print(); + } + } + fmt::print("- mmap/munmap average: {}\n", total_elapsed_time / (iter_max - skip_count)); + + + total_elapsed_time = 0; + for (int iter = 0; iter < iter_max; ++iter) + { + cucim::filesystem::discard_page_cache(input_file.c_str()); + // auto fd_mmap = open(input_file, O_RDONLY); + // void* mmap_host = mmap((void*)0, test_file_size, PROT_READ, MAP_SHARED, fd_mmap, 0); + // REQUIRE(mmap_host != MAP_FAILED); + auto fd = cucim::filesystem::open(input_file.c_str(), "rm"); + { + cucim::logger::Timer timer("- read tiles : {:.7f}\n", true, false); + + for (uint32_t i = 0; i < image_piece_count_; ++i) + { + // 3.441 => 3.489 + fd->pread(buffer_host, image_piece_bytecounts_[i], image_piece_offsets_[i]); + // memcpy(buffer_host, static_cast(mmap_host) + + // image_piece_offsets_[i], image_piece_bytecounts_[i]); + } + + double elapsed_time = timer.stop(); + if (iter >= skip_count) + { + total_elapsed_time += elapsed_time; + } + timer.print(); + } + + // if (mmap_host != MAP_FAILED) + // { + // REQUIRE(munmap(mmap_host, test_file_size) != -1); + // } + // close(fd_mmap); + } + fmt::print("- Read tiles average: {}\n", total_elapsed_time / (iter_max - skip_count)); + } + + free(unaligned_host); + free(buffer_host); +} diff --git a/cpp/plugins/cucim.kit.cuslide2/tests/test_read_region.cpp b/cpp/plugins/cucim.kit.cuslide2/tests/test_read_region.cpp new file mode 100644 index 000000000..af808fd12 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/tests/test_read_region.cpp @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2020-2021, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +#include + +#include "config.h" +#include "cuslide/tiff/tiff.h" + + +TEST_CASE("Verify read_region()", "[test_read_region.cpp]") +{ + SECTION("Test with different parameters") + { + auto test_sx = GENERATE(as{}, 1, 255, 256, 511, 512); + auto test_sy = GENERATE(as{}, 1, 255, 256, 511, 512); + auto test_width = GENERATE(as{}, 1, 255, 256, 511, 512); + auto test_height = GENERATE(as{}, 1, 255, 256, 511, 512); + + INFO("Execute with [sx:" << test_sx << ", sy:" << test_sy << ", width:" << test_width + << ", height:" << test_height << "]"); + + int openslide_count = 0; + int cucim_count = 0; + + printf("[sx:%ld, sy:%ld, width:%ld, height:%ld]\n", test_sx, test_sy, test_width, test_height); + { + auto start = std::chrono::high_resolution_clock::now(); + + openslide_t* slide = openslide_open(g_config.get_input_path().c_str()); + REQUIRE(slide != nullptr); + + auto buf = static_cast(cucim_malloc(test_width * test_height * 4)); + openslide_read_region(slide, buf, test_sx, test_sy, 0, test_width, test_height); + + openslide_close(slide); + + auto end = std::chrono::high_resolution_clock::now(); + auto elapsed_seconds = std::chrono::duration_cast>(end - start); + printf("openslide: %f\n", elapsed_seconds.count()); + + auto out_image = reinterpret_cast(buf); + for (int i = 0; i < test_width * test_height * 4; i += 4) + { + openslide_count += out_image[i] + out_image[i + 1] + out_image[i + 2]; + } + INFO("openslide value count: " << openslide_count); + + cucim_free(buf); + } + + { + auto start = std::chrono::high_resolution_clock::now(); + + auto tif = std::make_shared(g_config.get_input_path().c_str(), + O_RDONLY); // , cuslide::tiff::TIFF::kUseLibTiff + tif->construct_ifds(); + + cucim::io::format::ImageMetadata metadata{}; + cucim::io::format::ImageReaderRegionRequestDesc request{}; + cucim::io::format::ImageDataDesc image_data{}; + + metadata.level_count(1).level_downsamples({ 1.0 }).level_ndim(3); + + int64_t request_location[2] = { test_sx, test_sy }; + request.location = request_location; + request.level = 0; + int64_t request_size[2] = { test_width, test_height }; + request.size = request_size; + request.device = const_cast("cpu"); + + tif->read(&metadata.desc(), &request, &image_data); + + tif->close(); + + auto end = std::chrono::high_resolution_clock::now(); + auto elapsed_seconds = std::chrono::duration_cast>(end - start); + + printf("cucim: %f\n", elapsed_seconds.count()); + auto out_image = reinterpret_cast(image_data.container.data); + for (int i = 0; i < test_width * test_height * 3; i += 3) + { + cucim_count += out_image[i] + out_image[i + 1] + out_image[i + 2]; + } + INFO("cucim value count: " << cucim_count); + + cucim_free(image_data.container.data); + printf("\n"); + } + + REQUIRE(openslide_count == cucim_count); + + /** + * Note: Experiment with OpenSlide with various level values (2020-09-28) + * + * When other level (1~) is used (for example, sx=4, sy=4, level=2, assuming that down factor is 4 for + * level 2), openslide's output is same with the values of cuCIM on the start position (sx/4, sy/4). If sx and + * sy is not multiple of 4, openslide's output was not trivial and performance was low. + */ + } +} diff --git a/cpp/src/cuimage.cpp b/cpp/src/cuimage.cpp index 08cfe772e..cbedeaa67 100644 --- a/cpp/src/cuimage.cpp +++ b/cpp/src/cuimage.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2020-2022, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. * SPDX-License-Identifier: Apache-2.0 */ @@ -276,14 +276,16 @@ CuImage::~CuImage() image_data_->container.data = nullptr; break; case io::DeviceType::kCUDA: - - if (image_data_->loader) - { - cudaError_t cuda_status; - CUDA_TRY(cudaFree(image_data_->container.data)); - } + { + // Always free CUDA memory allocated for this CuImage. + // If a loader exists and transferred ownership (num_workers==0), CuImage owns the memory. + // If no loader exists, CuImage allocated the memory directly. + // Either way, CuImage is responsible for freeing it. + cudaError_t cuda_status; + CUDA_TRY(cudaFree(image_data_->container.data)); image_data_->container.data = nullptr; break; + } case io::DeviceType::kCUDAHost: case io::DeviceType::kCUDAManaged: case io::DeviceType::kCPUShared: diff --git a/cpp/src/loader/thread_batch_data_loader.cpp b/cpp/src/loader/thread_batch_data_loader.cpp index 2d39046b0..0d900a1af 100644 --- a/cpp/src/loader/thread_batch_data_loader.cpp +++ b/cpp/src/loader/thread_batch_data_loader.cpp @@ -131,8 +131,16 @@ uint8_t* ThreadBatchDataLoader::raster_pointer(const uint64_t location_index) co uint32_t ThreadBatchDataLoader::request(uint32_t load_size) { +#ifdef DEBUG + fmt::print("🔍 request(): ENTRY - num_workers_={}, load_size={}, queued_item_count_={}\n", + num_workers_, load_size, queued_item_count_); +#endif // DEBUG + if (num_workers_ == 0) { +#ifdef DEBUG + fmt::print("🔍 request(): num_workers==0, returning 0\n"); +#endif // DEBUG return 0; } @@ -142,6 +150,10 @@ uint32_t ThreadBatchDataLoader::request(uint32_t load_size) } uint32_t num_items_to_request = std::min(load_size, static_cast(location_len_ - queued_item_count_)); +#ifdef DEBUG + fmt::print("🔍 request(): Will request {} items\n", num_items_to_request); +#endif // DEBUG + for (uint32_t i = 0; i < num_items_to_request; ++i) { uint32_t last_item_count = 0; @@ -149,7 +161,13 @@ uint32_t ThreadBatchDataLoader::request(uint32_t load_size) { last_item_count = tasks_.size(); } +#ifdef DEBUG + fmt::print("🔍 request(): Calling load_func for item {} (location_index={})\n", i, queued_item_count_); +#endif // DEBUG load_func_(this, queued_item_count_); +#ifdef DEBUG + fmt::print("🔍 request(): load_func returned, tasks added: {}\n", tasks_.size() - last_item_count); +#endif // DEBUG ++queued_item_count_; buffer_item_tail_index_ = queued_item_count_ % buffer_item_len_; // Append the number of added tasks to the batch count list. @@ -166,6 +184,11 @@ uint32_t ThreadBatchDataLoader::request(uint32_t load_size) uint32_t ThreadBatchDataLoader::wait_batch() { +#ifdef DEBUG + fmt::print("🔍 wait_batch(): ENTRY - num_workers_={}, batch_item_counts_.size()={}, tasks_.size()={}\n", + num_workers_, batch_item_counts_.size(), tasks_.size()); +#endif // DEBUG + if (num_workers_ == 0) { return 0; @@ -175,10 +198,32 @@ uint32_t ThreadBatchDataLoader::wait_batch() for (uint32_t batch_item_index = 0; batch_item_index < batch_size_ && !batch_item_counts_.empty(); ++batch_item_index) { uint32_t batch_item_count = batch_item_counts_.front(); +#ifdef DEBUG + fmt::print("🔍 wait_batch(): Processing batch_item_index={}, batch_item_count={}\n", + batch_item_index, batch_item_count); +#endif // DEBUG for (uint32_t i = 0; i < batch_item_count; ++i) { +#ifdef DEBUG + fmt::print("🔍 wait_batch(): Waiting for task {} of {}\n", i, batch_item_count); +#endif // DEBUG auto& future = tasks_.front(); - future.wait(); + try { + future.wait(); +#ifdef DEBUG + fmt::print("🔍 wait_batch(): Task {} completed\n", i); +#endif // DEBUG + } catch (const std::exception& e) { +#ifdef DEBUG + fmt::print("❌ wait_batch(): Task {} threw exception: {}\n", i, e.what()); +#endif // DEBUG + throw; + } catch (...) { +#ifdef DEBUG + fmt::print("❌ wait_batch(): Task {} threw unknown exception\n", i); +#endif // DEBUG + throw; + } tasks_.pop_front(); if (batch_data_processor_) { @@ -196,8 +241,16 @@ uint32_t ThreadBatchDataLoader::wait_batch() uint8_t* ThreadBatchDataLoader::next_data() { +#ifdef DEBUG + fmt::print("🔍 next_data(): ENTRY - num_workers_={}, processed_batch_count_={}, location_len_={}\n", + num_workers_, processed_batch_count_, location_len_); +#endif // DEBUG + if (num_workers_ == 0) // (location_len == 1 && batch_size == 1) { +#ifdef DEBUG + fmt::print("🔍 next_data(): num_workers==0 path\n"); +#endif // DEBUG // If it reads entire image with multi threads (using loader), release raster memory from batch data loader // by setting it to nullptr so that it will not be freed by ~ThreadBatchDataLoader (destructor). uint8_t* batch_raster_ptr = raster_data_[0]; @@ -207,12 +260,21 @@ uint8_t* ThreadBatchDataLoader::next_data() if (processed_batch_count_ * batch_size_ >= location_len_) { +#ifdef DEBUG + fmt::print("🔍 next_data(): All batches processed, returning nullptr\n"); +#endif // DEBUG // If all batches are processed, return nullptr. return nullptr; } // Wait until the batch is ready. +#ifdef DEBUG + fmt::print("🔍 next_data(): About to call wait_batch()\n"); +#endif // DEBUG wait_batch(); +#ifdef DEBUG + fmt::print("🔍 next_data(): wait_batch() completed\n"); +#endif // DEBUG uint8_t* batch_raster_ptr = raster_data_[buffer_item_head_index_]; @@ -295,14 +357,36 @@ uint32_t ThreadBatchDataLoader::data_batch_size() const bool ThreadBatchDataLoader::enqueue(std::function task, const TileInfo& tile) { +#ifdef DEBUG + fmt::print("🔍 enqueue(): ENTRY - num_workers_={}, tile.location_index={}, tile.index={}\n", + num_workers_, tile.location_index, tile.index); + fflush(stdout); +#endif // DEBUG + if (num_workers_ > 0) { +#ifdef DEBUG + fmt::print("🔍 enqueue(): About to enqueue task to thread pool\n"); + fflush(stdout); +#endif // DEBUG auto future = thread_pool_.enqueue(task); +#ifdef DEBUG + fmt::print("🔍 enqueue(): Task enqueued, adding future to tasks_\n"); + fflush(stdout); +#endif // DEBUG tasks_.emplace_back(std::move(future)); +#ifdef DEBUG + fmt::print("🔍 enqueue(): tasks_.size()={}\n", tasks_.size()); + fflush(stdout); +#endif // DEBUG if (batch_data_processor_) { batch_data_processor_->add_tile(tile); } +#ifdef DEBUG + fmt::print("🔍 enqueue(): Returning true\n"); + fflush(stdout); +#endif // DEBUG return true; } return false; diff --git a/cucim.code-workspace b/cucim.code-workspace index d39803f30..14524008a 100644 --- a/cucim.code-workspace +++ b/cucim.code-workspace @@ -33,7 +33,7 @@ "CUCIM_TESTDATA_FOLDER": "${workspaceDirectory}/test_data", // Add cuslide plugin's library path to LD_LIBRARY_PATH "LD_LIBRARY_PATH": "${workspaceDirectory}/build-debug/lib:${workspaceDirectory}/cpp/plugins/cucim.kit.cuslide/build-debug/lib:${workspaceDirectory}/temp/cuda/lib64:${os_env:LD_LIBRARY_PATH}", - "CUCIM_TEST_PLUGIN_PATH": "cucim.kit.cuslide@25.12.00.so" + "CUCIM_TEST_PLUGIN_PATH": "cucim.kit.cuslide@26.02.00.so" }, "cwd": "${workspaceDirectory}", "catch2": { @@ -226,7 +226,7 @@ }, { "name": "CUCIM_TEST_PLUGIN_PATH", - "value": "cucim.kit.cuslide@25.12.00.so" + "value": "cucim.kit.cuslide@26.02.00.so" } ], "console": "externalTerminal", @@ -254,7 +254,7 @@ }, { "name": "CUCIM_TEST_PLUGIN_PATH", - "value": "cucim.kit.cuslide@25.12.00.so" + "value": "cucim.kit.cuslide@26.02.00.so" } ], "console": "externalTerminal", @@ -286,7 +286,7 @@ }, { "name": "CUCIM_TEST_PLUGIN_PATH", - "value": "cucim.kit.cuslide@25.12.00.so" + "value": "cucim.kit.cuslide@26.02.00.so" } ], "console": "externalTerminal", diff --git a/dependencies.yaml b/dependencies.yaml index ffa5d100b..20d3ab921 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -161,6 +161,11 @@ dependencies: packages: - cuda-cudart-dev - libnvjpeg-dev + # nvImageCodec 0.7.0 (internal release) - install from local C packages: + # CUDA 12: /home/cdinea/Downloads/cucim_pr3/nvimgcodec/12/ + # CUDA 13: /home/cdinea/Downloads/cucim_pr3/nvimgcodec/13/ + # Set: export LD_LIBRARY_PATH=/path/to/nvimgcodec/{12|13}/lib64:$LD_LIBRARY_PATH + # - libnvimgcodec-dev=0.7.0 # Not yet on conda-forge specific: - output_types: conda matrices: @@ -175,12 +180,12 @@ dependencies: common: - output_types: conda packages: - - cucim==25.12.*,>=0.0.0a0 + - cucim==26.2.*,>=0.0.0a0 depends_on_libcucim: common: - output_types: conda packages: - - libcucim==25.12.*,>=0.0.0a0 + - libcucim==26.2.*,>=0.0.0a0 develop: common: - output_types: [conda, requirements, pyproject] @@ -241,6 +246,9 @@ dependencies: - output_types: conda packages: - &cupy_unsuffixed cupy>=13.6.0 + # nvImageCodec 0.7.0 (internal release) - see cuda section for installation + # - libnvimgcodec==0.7.0 # Not yet on conda-forge + # - nvimgcodec>=0.7.0 # Not yet on conda-forge specific: - output_types: [requirements, pyproject] matrices: @@ -248,10 +256,16 @@ dependencies: cuda: "12.*" packages: - cupy-cuda12x>=13.6.0 + # nvImageCodec 0.7.0 (internal release) - install from local wheel: + # pip install /home/cdinea/Downloads/nvidia_nvimgcodec_cu12-0.7.0.11-py3-none-manylinux_2_28_x86_64.whl + - nvidia-nvimgcodec-cu12>=0.7.0 # fallback to CUDA 13 versions if 'cuda' is '13.*' or not provided - matrix: packages: - cupy-cuda13x>=13.6.0 + # nvImageCodec 0.7.0 (internal release) - install from local wheel: + # pip install /home/cdinea/Downloads/nvidia_nvimgcodec_cu13-0.7.0.11-py3-none-manylinux_2_28_x86_64.whl + - nvidia-nvimgcodec-cu13>=0.7.0 test_python: common: - output_types: [conda, requirements, pyproject] diff --git a/notebooks/Using_Cache.ipynb b/notebooks/Using_Cache.ipynb index e7d68e2f5..b6576f521 100644 --- a/notebooks/Using_Cache.ipynb +++ b/notebooks/Using_Cache.ipynb @@ -432,7 +432,7 @@ "\n", "#### Cache Statistics\n", "\n", - "If used in the multi-processing environment (e.g, using `concurrent.futures.ProcessPoolExecutor()`), cache hit count (`hit_count`) and miss count (`miss_count`) wouldn't be recorded in the release/25.12 process's cache object.\n", + "If used in the multi-processing environment (e.g, using `concurrent.futures.ProcessPoolExecutor()`), cache hit count (`hit_count`) and miss count (`miss_count`) wouldn't be recorded in the main process's cache object.\n", "\n", "\n", "### `shared_memory` strategy\n", diff --git a/notebooks/input/README.md b/notebooks/input/README.md index 401bbc9ae..eb5aefaa6 100644 --- a/notebooks/input/README.md +++ b/notebooks/input/README.md @@ -1,16 +1,11 @@ # Test Dataset -TUPAC-TR-488.svs and TUPAC-TR-467.svs are breast cancer cases from the dataset -of Tumor Proliferation Assessment Challenge 2016 (TUPAC16 | MICCAI Grand Challenge) which are publicly -available through [The Cancer Genome Atlas (TCGA)](https://www.cancer.gov/about-nci/organization/ccg/research/structural-genomics/tcga). +TUPAC-TR-488.svs and TUPAC-TR-467.svs are from the dataset +of Tumor Proliferation Assessment Challenge 2016 (TUPAC16 | MICCAI Grand Challenge). -- Website: https://tupac.grand-challenge.org -- Data link: https://tupac.grand-challenge.org/Dataset/ - - TUPAC-TR-467.svs : https://portal.gdc.cancer.gov/files/575c0465-c4bc-4ea7-ab63-ba48aa5e374b - - TUPAC-TR-488.svs : https://portal.gdc.cancer.gov/files/e27c87c9-e163-4d55-8f27-4cc7dfca08d8 -- License: CC BY 3.0 (https://wiki.cancerimagingarchive.net/display/Public/TCGA-BRCA#3539225f58e64731d8e47d588cedd99d300d5d6) - - See LICENSE-3rdparty file +- Website: http://tupac.tue-image.nl/node/3 +- Data link: https://drive.google.com/drive/u/0/folders/0B--ztKW0d17XYlBqOXppQmw0M2M ## Converted files diff --git a/nvimgcodec_substream_crash_minimal.py b/nvimgcodec_substream_crash_minimal.py new file mode 100644 index 000000000..fa4e81f78 --- /dev/null +++ b/nvimgcodec_substream_crash_minimal.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Minimal reproducer for nvImageCodec 0.7.0 sub-code stream destruction crash. + +This script demonstrates a "free(): invalid pointer" crash that occurs when +destroying sub-code streams obtained via nvimgcodecCodeStreamGetSubCodeStream(). + +CRASH LOCATION: + cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.cpp + Line 342: nvimgcodecCodeStreamDestroy(ifd_info.sub_code_stream) + +EXPECTED BEHAVIOR: + Script should complete cleanly and exit normally. + +ACTUAL BEHAVIOR: + After "Exiting Python..." message, crash occurs during interpreter shutdown: + free(): invalid pointer + Aborted + +CRASH TRIGGER: + The crash is triggered by the combination of: + 1. Plugin configuration via _set_plugin_root() (like test_aperio_svs.py) + 2. Multiple read_region() calls with different devices (GPU and CPU) + 3. Python interpreter shutdown destroying objects in specific order + +REQUIREMENTS: + - nvImageCodec 0.7.0 + - cuslide2 plugin built with nvImageCodec support + - Test TIFF file (Aperio SVS) +""" + +import json +import os +import sys +from pathlib import Path + +def setup_plugin(): + """Setup cuslide2 plugin configuration (like test_aperio_svs.py does)""" + repo_root = Path(__file__).parent + plugin_lib = repo_root / "cpp/plugins/cucim.kit.cuslide2/build-release/lib" + + if not plugin_lib.exists(): + plugin_lib = repo_root / "install/lib" + + # Create plugin configuration + config = { + "plugin": { + "names": [ + "cucim.kit.cuslide2@25.12.00.so", + ] + } + } + + config_path = "/tmp/.cucim_crash_reproducer.json" + with open(config_path, "w") as f: + json.dump(config, f, indent=2) + + os.environ["CUCIM_CONFIG_PATH"] = config_path + print(f" Plugin config: {config_path}") + print(f" Plugin lib: {plugin_lib}") + + return str(plugin_lib) + +def main(): + # Check for test file + test_file = "/tmp/CMU-1-Small-Region.svs" + + if not Path(test_file).exists(): + print(f"\n❌ Test file not found: {test_file}") + print("\nDownload with:") + print(" wget -O /tmp/CMU-1-Small-Region.svs \\") + print(" 'https://openslide.cs.cmu.edu/download/openslide-testdata/Aperio/CMU-1-Small-Region.svs'") + return 1 + + print(f"\nTest file: {test_file}") + + print("\n[Setup] Configuring plugin...") + plugin_lib = setup_plugin() + + try: + # Import and set plugin root BEFORE creating CuImage (like test_aperio_svs.py) + from cucim.clara import _set_plugin_root + _set_plugin_root(plugin_lib) + print(f" ✓ Plugin root set: {plugin_lib}") + + from cucim import CuImage + print(" ✓ cucim imported") + except ImportError as e: + print(f"✗ Failed to import cucim: {e}") + return 1 + + print("\n[Step 1] Opening slide...") + print(" (This creates TiffFileParser with sub-code streams)") + slide = CuImage(test_file) + print(f" ✓ Opened: {slide.shape}") + + print("\n[Step 2] Reading regions (mimicking test_aperio_svs.py)...") + print(" (This creates multiple sub-code streams)") + + # Read GPU tile 512x512 (like test_aperio_svs.py line 98) + print("\n [2a] GPU decode 512x512...") + region1 = slide.read_region((0, 0), (512, 512), level=0, device="cuda") + print(f" ✓ GPU tile: {region1.shape}") + + # Read CPU tile 512x512 (like test_aperio_svs.py line 116) + print(" [2b] CPU decode 512x512...") + region2 = slide.read_region((0, 0), (512, 512), level=0, device="cpu") + print(f" ✓ CPU tile: {region2.shape}") + + # Read larger GPU tile 2048x2048 (like test_aperio_svs.py line 145) + print(" [2c] GPU decode 2048x2048...") + region3 = slide.read_region((0, 0), (2048, 2048), level=0, device="cuda") + print(f" ✓ Large GPU tile: {region3.shape}") + + # Read larger CPU tile 2048x2048 (like test_aperio_svs.py line 151) + print(" [2d] CPU decode 2048x2048...") + region4 = slide.read_region((0, 0), (2048, 2048), level=0, device="cpu") + print(f" ✓ Large CPU tile: {region4.shape}") + + print("\n[Step 3] Letting slide go out of scope...") + print(" (Natural destruction - no explicit 'del')") + print(" (This will destroy TiffFileParser and call:") + print(" nvimgcodecCodeStreamDestroy() on sub-code streams)") + print() + + # Let slide go out of scope naturally instead of explicit del + # THE CRASH HAPPENS during Python shutdown when slide is finally destroyed + # (Not here, but when main() returns and Python cleans up) + + print("=" * 70) + print("✓ Function completed - slide going out of scope now...") + print("=" * 70) + print("\nNOTE: The crash may occur AFTER this message") + print(" during Python interpreter shutdown.") + print("\nWatching for: 'free(): invalid pointer'") + print("=" * 70) + + return 0 + +if __name__ == "__main__": + print("\n" + "=" * 70) + print("STARTING TEST - nvImageCodec Sub-Stream Crash Reproducer") + print("=" * 70) + result = main() + print("\n" + "=" * 70) + print("Exiting Python... (crash may occur during cleanup)") + print("=" * 70) + sys.exit(result) + diff --git a/python/.idea/.gitignore b/python/.idea/.gitignore deleted file mode 100644 index 73f69e095..000000000 --- a/python/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/python/.idea/.name b/python/.idea/.name deleted file mode 100644 index 106496e4f..000000000 --- a/python/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -pycucim diff --git a/python/.idea/codeStyles/Project.xml b/python/.idea/codeStyles/Project.xml deleted file mode 100644 index c8f84c353..000000000 --- a/python/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/python/.idea/codeStyles/codeStyleConfig.xml b/python/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 0f7bc519d..000000000 --- a/python/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/python/.idea/dataSources.xml b/python/.idea/dataSources.xml deleted file mode 100644 index ced8a0f77..000000000 --- a/python/.idea/dataSources.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - sqlite.xerial - true - org.sqlite.JDBC - jdbc:sqlite:$PROJECT_DIR$/cucim/.coverage - - - diff --git a/python/.idea/fileTemplates/includes/NVIDIA_CMAKE_HEADER.cmake b/python/.idea/fileTemplates/includes/NVIDIA_CMAKE_HEADER.cmake deleted file mode 100644 index f11461a8a..000000000 --- a/python/.idea/fileTemplates/includes/NVIDIA_CMAKE_HEADER.cmake +++ /dev/null @@ -1,6 +0,0 @@ -# -# cmake-format: off -# SPDX-FileCopyrightText: Copyright (c) $YEAR, NVIDIA CORPORATION. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# cmake-format: on -# diff --git a/python/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h b/python/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h deleted file mode 100644 index 0255849f3..000000000 --- a/python/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h +++ /dev/null @@ -1,4 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) $YEAR, NVIDIA CORPORATION. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ diff --git a/python/.idea/fileTemplates/internal/C Header File.h b/python/.idea/fileTemplates/internal/C Header File.h deleted file mode 100644 index 9cb1d09e2..000000000 --- a/python/.idea/fileTemplates/internal/C Header File.h +++ /dev/null @@ -1,5 +0,0 @@ -#parse("NVIDIA_C_HEADER.h") -#[[#ifndef]]# ${INCLUDE_GUARD} -#[[#define]]# ${INCLUDE_GUARD} - -#[[#endif]]# //${INCLUDE_GUARD} diff --git a/python/.idea/fileTemplates/internal/C Source File.c b/python/.idea/fileTemplates/internal/C Source File.c deleted file mode 100644 index b04dd6c62..000000000 --- a/python/.idea/fileTemplates/internal/C Source File.c +++ /dev/null @@ -1,4 +0,0 @@ -#parse("NVIDIA_C_HEADER.h") -#if (${HEADER_FILENAME}) -#[[#include]]# "${HEADER_FILENAME}" -#end diff --git a/python/.idea/fileTemplates/internal/C++ Class Header.h b/python/.idea/fileTemplates/internal/C++ Class Header.h deleted file mode 100644 index f521fa555..000000000 --- a/python/.idea/fileTemplates/internal/C++ Class Header.h +++ /dev/null @@ -1,13 +0,0 @@ -#parse("NVIDIA_C_HEADER.h") -#[[#ifndef]]# ${INCLUDE_GUARD} -#[[#define]]# ${INCLUDE_GUARD} - -${NAMESPACES_OPEN} - -class ${NAME} { - -}; - -${NAMESPACES_CLOSE} - -#[[#endif]]# //${INCLUDE_GUARD} diff --git a/python/.idea/fileTemplates/internal/C++ Class.cc b/python/.idea/fileTemplates/internal/C++ Class.cc deleted file mode 100644 index 42f43ccf4..000000000 --- a/python/.idea/fileTemplates/internal/C++ Class.cc +++ /dev/null @@ -1,2 +0,0 @@ -#parse("NVIDIA_C_HEADER.h") -#[[#include]]# "${HEADER_FILENAME}" diff --git a/python/.idea/fileTemplates/internal/CMakeLists.txt.cmake b/python/.idea/fileTemplates/internal/CMakeLists.txt.cmake deleted file mode 100644 index 846356219..000000000 --- a/python/.idea/fileTemplates/internal/CMakeLists.txt.cmake +++ /dev/null @@ -1 +0,0 @@ -#parse("NVIDIA_CMAKE_HEADER.cmake") diff --git a/python/.idea/misc.xml b/python/.idea/misc.xml deleted file mode 100644 index c8370a824..000000000 --- a/python/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/python/.idea/pycucim.iml b/python/.idea/pycucim.iml deleted file mode 100644 index 8afe22e01..000000000 --- a/python/.idea/pycucim.iml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/python/.idea/python.iml b/python/.idea/python.iml deleted file mode 100644 index 8afe22e01..000000000 --- a/python/.idea/python.iml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/python/.idea/vcs.xml b/python/.idea/vcs.xml deleted file mode 100644 index 54e4b961e..000000000 --- a/python/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/python/cucim/pyproject.toml b/python/cucim/pyproject.toml index bed01a323..de40758f0 100644 --- a/python/cucim/pyproject.toml +++ b/python/cucim/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "cupy-cuda13x>=13.6.0", "lazy-loader>=0.4", "numpy>=1.23.4,<3.0a0", + "nvidia-nvimgcodec-cu13>=0.7.0", "scikit-image>=0.19.0,<0.26.0a0", "scipy>=1.11.2", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. diff --git a/run b/run index 4a53d6ee4..9d1d3ba33 100755 --- a/run +++ b/run @@ -313,6 +313,27 @@ build_local_cuslide_() { popd } +build_local_cuslide2_() { + local source_folder=${1:-${TOP}/cpp/plugins/cucim.kit.cuslide2} + local build_type=${2:-debug} + local build_type_str=${3:-Debug} + local prefix=${4:-} + local build_folder=${source_folder}/build-${build_type} + local CMAKE_CMD=${CMAKE_CMD:-cmake} + + pushd "${source_folder}" > /dev/null + + ${CMAKE_CMD} -S "${source_folder}" -B "${build_folder}" -G "Unix Makefiles" \ + -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE \ + -DCMAKE_BUILD_TYPE="${build_type_str}" \ + -DCMAKE_PREFIX_PATH="${prefix}" \ + -DCMAKE_INSTALL_PREFIX="${source_folder}/install" + ${CMAKE_CMD} --build "${build_folder}" --config "${build_type_str}" --target cucim.kit.cuslide2 -- -j "$(nproc)" + ${CMAKE_CMD} --build "${build_folder}" --config "${build_type_str}" --target install -- -j "$(nproc)" + + popd +} + build_local_cumed_() { local source_folder=${1:-${TOP}/cpp/plugins/cucim.kit.cumed} local build_type=${2:-debug} @@ -394,6 +415,7 @@ build_local() { rm -f "${TOP}"/build-*/CMakeCache.txt rm -f "${TOP}"/cpp/plugins/cucim.kit.cuslide/build-*/CMakeCache.txt rm -f "${TOP}"/cpp/plugins/cucim.kit.cumed/build-*/CMakeCache.txt + rm -f "${TOP}"/cpp/plugins/cucim.kit.cuslide2/build-*/CMakeCache.txt rm -f "${TOP}"/python/build-*/CMakeCache.txt fi @@ -404,6 +426,8 @@ build_local() { rm -rf "${TOP}"/cpp/plugins/cucim.kit.cuslide/install rm -rf "${TOP}"/cpp/plugins/cucim.kit.cumed/build-*/ rm -rf "${TOP}"/cpp/plugins/cucim.kit.cumed/install + rm -rf "${TOP}"/cpp/plugins/cucim.kit.cuslide2/build-*/ + rm -rf "${TOP}"/cpp/plugins/cucim.kit.cuslide2/install rm -rf "${TOP}"/python/build-* rm -rf "${TOP}"/python/install fi @@ -416,6 +440,10 @@ build_local() { build_local_cuslide_ "${TOP}"/cpp/plugins/cucim.kit.cuslide "${build_type}" ${build_type_str} "${prefix}" fi + if [ "$subcommand" = "all" ] || [ "$subcommand" = "cuslide2" ]; then + build_local_cuslide2_ "${TOP}"/cpp/plugins/cucim.kit.cuslide2 "${build_type}" ${build_type_str} "${prefix}" + fi + if [ "$subcommand" = "all" ] || [ "$subcommand" = "cumed" ]; then build_local_cumed_ "${TOP}"/cpp/plugins/cucim.kit.cumed "${build_type}" ${build_type_str} "${prefix}" fi @@ -436,7 +464,10 @@ build_local() { cp "${TOP}"/build-"${build_type}"/lib*/libcucim.so."${major_version}" "${TOP}"/python/cucim/src/cucim/clara/ cp -P "${TOP}"/cpp/plugins/cucim.kit.cuslide/build-"${build_type}"/lib*/cucim* "${TOP}"/python/cucim/src/cucim/clara/ cp -P "${TOP}"/cpp/plugins/cucim.kit.cumed/build-"${build_type}"/lib*/cucim* "${TOP}"/python/cucim/src/cucim/clara/ - + # Only copy cuslide2 if it exists (infrastructure-only mode has no .so file) + if ls "${TOP}"/cpp/plugins/cucim.kit.cuslide2/build-"${build_type}"/lib*/cucim* 1> /dev/null 2>&1; then + cp -P "${TOP}"/cpp/plugins/cucim.kit.cuslide2/build-"${build_type}"/lib*/cucim* "${TOP}"/python/cucim/src/cucim/clara/ + fi # Copy .so files from pybind's build folder to cuCIM's Python source folder cp "${TOP}"/python/build-"${build_type}"/lib/cucim/_cucim.*.so "${TOP}"/python/cucim/src/cucim/clara/ fi @@ -529,6 +560,14 @@ test_python() { fi pushd "$TOP"/python/cucim + # Set CUDA environment for CuPy JIT compilation + if [ -n "${CONDA_PREFIX}" ]; then + export CUDA_PATH="${CONDA_PREFIX}" + export CUDA_HOME="${CONDA_PREFIX}" + # CuPy NVRTC needs to find CUDA headers in targets subdirectory + export CUDA_INCLUDE_PATH="${CONDA_PREFIX}/targets/x86_64-linux/include" + echo "🔧 Set CUDA_HOME=${CUDA_HOME} CUDA_INCLUDE_PATH=${CUDA_INCLUDE_PATH}" + fi run_command py.test --cache-clear -vv \ --cov=cucim \ --junitxml="$TOP/junit-cucim.xml" \ diff --git a/scripts/test_aperio_svs.py b/scripts/test_aperio_svs.py new file mode 100755 index 000000000..f36bba5d1 --- /dev/null +++ b/scripts/test_aperio_svs.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Quick test script for cuslide2 plugin with Aperio SVS files +""" + +import json +import os +import sys +import time +from pathlib import Path + + +def setup_environment(): + """Setup cuCIM environment for cuslide2 plugin""" + + # Get current build directory + repo_root = Path(__file__).parent.parent + plugin_lib = repo_root / "cpp/plugins/cucim.kit.cuslide2/build-release/lib" + + if not plugin_lib.exists(): + plugin_lib = repo_root / "install/lib" + + # Create plugin configuration + config = { + "plugin": { + "names": [ + "cucim.kit.cuslide2@25.12.00.so", # Try cuslide2 first + ] + } + } + + config_path = "/tmp/.cucim_aperio_test.json" + with open(config_path, "w") as f: + json.dump(config, f, indent=2) + + os.environ["CUCIM_CONFIG_PATH"] = config_path + + print(f"✅ Plugin configuration: {config_path}") + print(f"✅ Plugin library path: {plugin_lib}") + + return str(plugin_lib) + + +def test_aperio_svs(svs_path, plugin_lib): + """Test cuslide2 plugin with an Aperio SVS file""" + + print("\n🔬 Testing cuslide2 plugin with Aperio SVS") + print("=" * 60) + print(f"📁 File: {svs_path}") + + if not Path(svs_path).exists(): + print(f"❌ File not found: {svs_path}") + return False + + try: + # Set plugin root AFTER importing cucim but BEFORE creating CuImage + from cucim.clara import _set_plugin_root + + _set_plugin_root(str(plugin_lib)) + print(f"✅ Plugin root set: {plugin_lib}") + + from cucim import CuImage + + # Load the SVS file + print("\n📂 Loading SVS file...") + start = time.time() + img = CuImage(svs_path) + load_time = time.time() - start + + print(f"✅ Loaded in {load_time:.3f}s") + + # Show basic info + print("\n📊 Image Information:") + print(f" Dimensions: {img.shape}") + level_count = img.resolutions["level_count"] + print(f" Levels: {level_count}") + print(f" Dtype: {img.dtype}") + print(f" Device: {img.device}") + + # Show all levels + print("\n🔍 Resolution Levels:") + level_dimensions = img.resolutions["level_dimensions"] + level_downsamples = img.resolutions["level_downsamples"] + for level in range(level_count): + level_dims = level_dimensions[level] + level_downsample = level_downsamples[level] + print( + f" Level {level}: {level_dims[0]}x{level_dims[1]} (downsample: {level_downsample:.1f}x)" + ) + + # Try to read a tile from level 0 (GPU) + print("\n🚀 Testing GPU decode (nvImageCodec)...") + try: + start = time.time() + gpu_tile = img.read_region( + location=[0, 0], size=[512, 512], level=0, device="cuda" + ) + gpu_time = time.time() - start + + print("✅ GPU decode successful!") + print(f" Time: {gpu_time:.4f}s") + print(f" Shape: {gpu_tile.shape}") + print(f" Device: {gpu_tile.device}") + except Exception as e: + print(f"⚠️ GPU decode failed: {e}") + print(" (This is expected if CUDA is not available)") + gpu_time = None + + # Try to read same tile from CPU + print("\n🖥️ Testing CPU decode (baseline)...") + try: + start = time.time() + cpu_tile = img.read_region( + location=[0, 0], size=[512, 512], level=0, device="cpu" + ) + cpu_time = time.time() - start + + print("✅ CPU decode successful!") + print(f" Time: {cpu_time:.4f}s") + print(f" Shape: {cpu_tile.shape}") + print(f" Device: {cpu_tile.device}") + + # Calculate speedup + if gpu_time: + speedup = cpu_time / gpu_time + print(f"\n🎯 GPU Speedup: {speedup:.2f}x faster than CPU") + + if speedup > 1.5: + print(" 🚀 nvImageCodec GPU acceleration is working!") + elif speedup > 0.9: + print(" ✅ GPU decode working (speedup may vary by tile size)") + else: + print(" ℹ️ CPU was faster for this small tile") + except Exception as e: + print(f"❌ CPU decode failed: {e}") + + # Test larger tile for better speedup + print("\n📏 Testing larger tile (2048x2048)...") + try: + # GPU + start = time.time() + _ = img.read_region([0, 0], [2048, 2048], 0, device="cuda") + gpu_large_time = time.time() - start + print(f" GPU: {gpu_large_time:.4f}s") + + # CPU + start = time.time() + _ = img.read_region([0, 0], [2048, 2048], 0, device="cpu") + cpu_large_time = time.time() - start + print(f" CPU: {cpu_large_time:.4f}s") + + speedup = cpu_large_time / gpu_large_time + print(f" 🎯 Speedup: {speedup:.2f}x") + + except Exception as e: + print(f" ⚠️ Large tile test failed: {e}") + + print("\n✅ Test completed successfully!") + return True + + except Exception as e: + print(f"❌ Test failed: {e}") + import traceback + + traceback.print_exc() + return False + + +def download_test_svs(): + """Download a small Aperio SVS test file from OpenSlide""" + + print("\n📥 Downloading Aperio SVS test file...") + + test_file = Path("/tmp/CMU-1-Small-Region.svs") + + if test_file.exists(): + print(f"✅ Test file already exists: {test_file}") + return str(test_file) + + try: + import urllib.request + + # Download small test file (2MB) from OpenSlide test data + url = "https://openslide.cs.cmu.edu/download/openslide-testdata/Aperio/CMU-1-Small-Region.svs" + + print(f" Downloading from: {url}") + print(" Size: ~2MB (small test file)") + print(" This may take a minute...") + + urllib.request.urlretrieve(url, test_file) + + print(f"✅ Downloaded: {test_file}") + return str(test_file) + + except Exception as e: + print(f"❌ Download failed: {e}") + return None + + +def list_available_test_files(): + """List available Aperio SVS test files from OpenSlide""" + + print("\n📋 Available Aperio SVS Test Files from OpenSlide:") + print("=" * 70) + + test_files = [ + ("CMU-1-Small-Region.svs", "~2MB", "Small region, JPEG, single pyramid level"), + ("CMU-1.svs", "~177MB", "Brightfield, JPEG compression"), + ("CMU-1-JP2K-33005.svs", "~126MB", "JPEG 2000, RGB"), + ("CMU-2.svs", "~390MB", "Brightfield, JPEG compression"), + ("CMU-3.svs", "~253MB", "Brightfield, JPEG compression"), + ("JP2K-33003-1.svs", "~63MB", "Aorta tissue, JPEG 2000, YCbCr"), + ("JP2K-33003-2.svs", "~275MB", "Heart tissue, JPEG 2000, YCbCr"), + ] + + print(f"{'Filename':<25} {'Size':<10} {'Description'}") + print("-" * 70) + for filename, size, description in test_files: + print(f"{filename:<25} {size:<10} {description}") + + print("\n💡 To download:") + print( + " wget https://openslide.cs.cmu.edu/download/openslide-testdata/Aperio/" + ) + print("\n📖 More info: https://openslide.cs.cmu.edu/download/openslide-testdata/") + + +def main(): + """Main function""" + + if len(sys.argv) < 2: + print("Usage: python test_aperio_svs.py ") + print(" or: python test_aperio_svs.py --download (auto-download test file)") + print("") + print("Example:") + print(" python test_aperio_svs.py /path/to/slide.svs") + print(" python test_aperio_svs.py --download") + print("") + print("This script will:") + print(" ✅ Configure cuslide2 plugin with nvImageCodec") + print(" ✅ Load and analyze the SVS file") + print(" ✅ Test GPU-accelerated decoding") + print(" ✅ Compare CPU vs GPU performance") + + # List available test files + list_available_test_files() + return 1 + + svs_path = sys.argv[1] + + # Handle --download flag + if svs_path == "--download": + svs_path = download_test_svs() + if svs_path is None: + print("\n❌ Failed to download test file") + print("💡 You can manually download with:") + print( + " wget https://openslide.cs.cmu.edu/download/openslide-testdata/Aperio/CMU-1-Small-Region.svs" + ) + return 1 + + # Setup environment + plugin_lib = setup_environment() + + # Test the SVS file + success = test_aperio_svs(svs_path, plugin_lib) + + return 0 if success else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/test_philips_tiff.py b/scripts/test_philips_tiff.py new file mode 100755 index 000000000..6532da1d5 --- /dev/null +++ b/scripts/test_philips_tiff.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Test Philips TIFF support in cuslide2""" + +import json +import os +import sys +import time +from pathlib import Path + + +def setup_environment(): + """Setup cuCIM environment for cuslide2 plugin""" + + # Get current build directory + repo_root = Path(__file__).parent.parent + plugin_lib = repo_root / "cpp/plugins/cucim.kit.cuslide2/build-release/lib" + + if not plugin_lib.exists(): + plugin_lib = repo_root / "install/lib" + + # Create plugin configuration + config = { + "plugin": { + "names": [ + "cucim.kit.cuslide2@25.12.00.so", # Try cuslide2 first + ] + } + } + + config_path = "/tmp/.cucim_philips_test.json" + with open(config_path, "w") as f: + json.dump(config, f, indent=2) + + os.environ["CUCIM_CONFIG_PATH"] = config_path + + print(f"✅ Plugin configuration: {config_path}") + print(f"✅ Plugin library path: {plugin_lib}") + + return str(plugin_lib) + + +def test_philips_tiff(file_path, plugin_lib): + """Test Philips TIFF loading and decoding""" + + print("=" * 60) + print("🔬 Testing Philips TIFF with cuslide2") + print("=" * 60) + print(f"📁 File: {file_path}") + + # Set plugin root to use cuslide2 + import cucim + from cucim.clara import _set_plugin_root + + _set_plugin_root(str(plugin_lib)) + print(f"✅ Plugin root set: {plugin_lib}") + print() + + # Load image + print("📂 Loading Philips TIFF file...") + start = time.time() + img = cucim.CuImage(file_path) + load_time = time.time() - start + print(f"✅ Loaded in {load_time:.3f}s") + print() + + # Check detection + print("📊 Image Information:") + print(" Format: Philips TIFF") + print(f" Dimensions: {img.shape}") + level_count = img.resolutions["level_count"] + print(f" Levels: {level_count}") + print(f" Dtype: {img.dtype}") + print(f" Device: {img.device}") + print() + + # Display resolution levels + print("🔍 Resolution Levels:") + level_count = img.resolutions["level_count"] + level_dimensions = img.resolutions["level_dimensions"] + level_downsamples = img.resolutions["level_downsamples"] + for level in range(level_count): + dims = level_dimensions[level] + downsample = level_downsamples[level] + print(f" Level {level}: {dims[0]}x{dims[1]} (downsample: {downsample:.1f}x)") + print() + + # Check for Philips metadata + print("📋 Philips Metadata:") + metadata = img.metadata + if "philips" in metadata: + philips_data = metadata["philips"] + print(f" ✅ Found {len(philips_data)} Philips metadata entries") + # Show some important keys + important_keys = [ + "DICOM_PIXEL_SPACING", + "DICOM_MANUFACTURER", + "PIM_DP_IMAGE_TYPE", + "DICOM_SOFTWARE_VERSIONS", + "PIM_DP_IMAGE_ROWS", + "PIM_DP_IMAGE_COLUMNS", + ] + for key in important_keys: + if key in philips_data: + print(f" {key}: {philips_data[key]}") + print(f" ... and {len(philips_data) - len(important_keys)} more entries") + else: + print(" ⚠️ No Philips metadata found") + print() + + # Check MPP (microns per pixel) + print("📏 Pixel Spacing:") + if "philips" in metadata and "DICOM_PIXEL_SPACING" in metadata["philips"]: + spacing = metadata["philips"]["DICOM_PIXEL_SPACING"] + print( + f" DICOM Pixel Spacing: {spacing[0] * 1000:.4f} x {spacing[1] * 1000:.4f} μm/pixel" + ) + if "openslide.mpp-x" in metadata: + print(f" OpenSlide MPP-X: {metadata['openslide.mpp-x']} μm/pixel") + print(f" OpenSlide MPP-Y: {metadata['openslide.mpp-y']} μm/pixel") + print() + + # Test GPU decode + print("🚀 Testing GPU decode (nvImageCodec)...") + try: + start = time.time() + region = img.read_region((0, 0), (512, 512), level=0, device="cuda") + decode_time = time.time() - start + print("✅ GPU decode successful!") + print(f" Time: {decode_time:.4f}s") + print(f" Shape: {region.shape}") + print(f" Device: {region.device}") + + # Check pixel values + if hasattr(region, "get"): + region_cpu = region.get() + print(f" Pixel range: [{region_cpu.min()}, {region_cpu.max()}]") + print(f" Mean value: {region_cpu.mean():.2f}") + print() + except Exception as e: + print(f"❌ GPU decode failed: {e}") + import traceback + + traceback.print_exc() + print() + + # Test CPU decode + print("🖥️ Testing CPU decode...") + try: + start = time.time() + region = img.read_region((0, 0), (512, 512), level=0, device="cpu") + decode_time = time.time() - start + + # Check if we got actual data + if hasattr(region, "__array_interface__") or hasattr( + region, "__cuda_array_interface__" + ): + import numpy as np + + if hasattr(region, "get"): # CuPy array + region_cpu = region.get() + else: + region_cpu = np.asarray(region) + + if region_cpu.size > 0: + pixel_sum = region_cpu.sum() + pixel_mean = region_cpu.mean() + print("✅ CPU decode successful:") + print(f" Time: {decode_time:.4f}s") + print(f" Shape: {region_cpu.shape}") + print(f" Pixel sum: {pixel_sum}, mean: {pixel_mean:.2f}") + else: + print("⚠️ CPU decode returned empty data:") + print(f" Time: {decode_time:.4f}s (likely returning cached/empty)") + else: + print(f"⚠️ CPU decode returned unknown type: {type(region)}") + print() + except Exception as e: + print("❌ CPU decode failed:") + print(f" {e}") + print() + + # Test associated images + print("🖼️ Testing associated images...") + try: + label = img.associated_image("label") + print(f" ✅ Label: {label.shape}") + except Exception as e: + print(f" ⚠️ Label not found: {e}") + + try: + macro = img.associated_image("macro") + print(f" ✅ Macro: {macro.shape}") + except Exception as e: + print(f" ⚠️ Macro not found: {e}") + + try: + thumbnail = img.associated_image("thumbnail") + print(f" ✅ Thumbnail: {thumbnail.shape}") + except Exception as e: + print(f" ⚠️ Thumbnail not found: {e}") + print() + + # Test larger tile + print("📏 Testing larger tile (2048x2048)...") + try: + start = time.time() + region = img.read_region((0, 0), (2048, 2048), level=0, device="cuda") + decode_time = time.time() - start + print(f" ✅ GPU: {decode_time:.4f}s") + print(f" Shape: {region.shape}") + except Exception as e: + print(f" ⚠️ Large tile failed: {e}") + print() + + # Test multi-level reads + print("🔀 Testing multi-level reads...") + level_count = img.resolutions["level_count"] + level_dimensions = img.resolutions["level_dimensions"] + for level in range(min(3, level_count)): + try: + start = time.time() + dims = level_dimensions[level] + read_size = (min(512, dims[0]), min(512, dims[1])) + region = img.read_region((0, 0), read_size, level=level, device="cuda") + decode_time = time.time() - start + print(f" ✅ Level {level}: {decode_time:.4f}s ({region.shape})") + except Exception as e: + print(f" ❌ Level {level} failed: {e}") + print() + + print("✅ Philips TIFF test completed!") + return True + + +def download_test_data(): + """List available Philips TIFF test files""" + + print("\n📋 Available Philips TIFF Test Files from OpenSlide:") + print("=" * 70) + print( + "Source: https://openslide.cs.cmu.edu/download/openslide-testdata/Philips-TIFF/" + ) + print() + + test_files = [ + ("Philips-1.tiff", "311 MB", "Lymph node, H&E, BigTIFF, barcode (CAMELYON16)"), + ( + "Philips-2.tiff", + "872 MB", + "Lymph node, H&E, BigTIFF, macro image (CAMELYON16)", + ), + ( + "Philips-3.tiff", + "3.08 GB", + "Lymph node, H&E, BigTIFF, full metadata (CAMELYON16)", + ), + ("Philips-4.tiff", "277 MB", "Lymph node, H&E, BigTIFF, sparse (CAMELYON17)"), + ] + + print(f"{'Filename':<20} {'Size':<12} {'Description'}") + print("-" * 70) + for filename, size, description in test_files: + print(f"{filename:<20} {size:<12} {description}") + + print("\n💡 To download:") + print( + " wget https://openslide.cs.cmu.edu/download/openslide-testdata/Philips-TIFF/" + ) + print("\n📖 Format details:") + print(" - Single-file pyramidal tiled TIFF/BigTIFF") + print(" - Non-standard Philips metadata in ImageDescription XML") + print(" - Label and macro images as Base64 JPEGs in XML or TIFF directories") + print(" - Some tiles may be sparse (TileOffset=0 for blank regions)") + print("\n📜 License: CC0 (Public Domain)") + print(" Credit: Computational Pathology Group, Radboud University Medical Center") + + +def main(): + """Main function""" + + if len(sys.argv) < 2: + print("Usage: python test_philips_tiff.py ") + print(" or: python test_philips_tiff.py --list (show available test files)") + print() + print("Example:") + print(" python test_philips_tiff.py /path/to/Philips-1.tiff") + print(" python test_philips_tiff.py --list") + print() + print("This script will:") + print(" ✅ Configure cuslide2 plugin with nvImageCodec") + print(" ✅ Load and analyze the Philips TIFF file") + print(" ✅ Test GPU-accelerated decoding") + print(" ✅ Display Philips-specific metadata") + print(" ✅ Test multi-level pyramid reads") + + download_test_data() + return 1 + + file_path = sys.argv[1] + + # Handle --list flag + if file_path == "--list": + download_test_data() + return 0 + + # Check file exists + if not Path(file_path).exists(): + print(f"❌ File not found: {file_path}") + print() + download_test_data() + return 1 + + # Setup environment + plugin_lib = setup_environment() + + # Test the Philips TIFF file + try: + success = test_philips_tiff(file_path, plugin_lib) + return 0 if success else 1 + except Exception as e: + print(f"❌ Test failed: {e}") + import traceback + + traceback.print_exc() + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/verify_cuslide2_infrastructure.py b/scripts/verify_cuslide2_infrastructure.py new file mode 100755 index 000000000..1d39d5930 --- /dev/null +++ b/scripts/verify_cuslide2_infrastructure.py @@ -0,0 +1,659 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +cuslide2 Infrastructure Verification Script + +This script validates that the cuslide2 plugin infrastructure is properly set up: +- nvImageCodec installation (conda/pip packages OR CMake build integration) +- cuslide2 plugin build status +- Plugin integration with nvImageCodec (linkage verification) +- Environment configuration + +Supports two nvImageCodec deployment models: +1. Conda/pip packages (specified in dependencies.yaml) - RECOMMENDED +2. CMake FetchContent integration (automatically fetched during build) +""" + +import os +import platform +import subprocess +import sys +from pathlib import Path + + +def print_header(title): + """Print a formatted header""" + print(f"\n{'=' * 60}") + print(f" {title}") + print(f"{'=' * 60}") + + +def print_section(title): + """Print a formatted section""" + print(f"\n{'-' * 40}") + print(f" {title}") + print(f"{'-' * 40}") + + +def check_command_exists(command): + """Check if a command exists in PATH""" + try: + subprocess.run( + [command, "--version"], capture_output=True, check=True, timeout=10 + ) + return True + except ( + subprocess.CalledProcessError, + FileNotFoundError, + subprocess.TimeoutExpired, + ): + try: + subprocess.run( + [command, "--help"], capture_output=True, check=True, timeout=10 + ) + return True + except ( + subprocess.CalledProcessError, + FileNotFoundError, + subprocess.TimeoutExpired, + ): + return False + + +def run_command(command, description=""): + """Run a command and return output""" + try: + result = subprocess.run( + command, shell=True, capture_output=True, text=True, timeout=30 + ) + return result.returncode == 0, result.stdout.strip(), result.stderr.strip() + except subprocess.TimeoutExpired: + return False, "", "Command timed out" + except Exception as e: + return False, "", str(e) + + +def check_conda_environment(): + """Check conda environment and nvImageCodec packages - returns package list or None""" + # Check if we're in a conda environment + conda_prefix = os.environ.get("CONDA_PREFIX") + if not conda_prefix: + return None + + # Find conda executable + conda_executables = ["micromamba", "mamba", "conda"] + conda_cmd = None + + for cmd in conda_executables: + if check_command_exists(cmd): + conda_cmd = cmd + break + + if not conda_cmd: + return None + + # Check for nvImageCodec packages + success, output, error = run_command(f"{conda_cmd} list libnvimgcodec") + if success and "libnvimgcodec" in output: + packages = [] + for line in output.split("\n"): + if "libnvimgcodec" in line: + packages.append(line.strip()) + return packages if packages else None + return None + + +def check_python_packages(): + """Check Python nvImageCodec packages - returns package list or None""" + success, output, error = run_command("pip list | grep nvidia-nvimgcodec") + if success and output: + packages = [] + for line in output.split("\n"): + if line.strip(): + packages.append(line.strip()) + return packages if packages else None + return None + + +def find_plugin_build_dir(): + """Find the cuslide2 plugin build directory""" + script_dir = Path(__file__).parent.parent # Go up to cucim root + + # Possible build locations + build_paths = [ + script_dir / "cpp/plugins/cucim.kit.cuslide2/build-release", + script_dir / "cpp/plugins/cucim.kit.cuslide2/build", + script_dir / "build-release", + script_dir / "build", + ] + + for build_dir in build_paths: + if build_dir.exists(): + return build_dir + + return None + + +def detect_installation_method(): + """Detect and display nvImageCodec installation method - supports both approaches""" + print_section("nvImageCodec Installation Detection") + + conda_packages = check_conda_environment() + pip_packages = check_python_packages() + conda_prefix = os.environ.get("CONDA_PREFIX") + + # Check for conda/pip installation (Method 1 - RECOMMENDED per dependencies.yaml) + if conda_packages: + print("✓ Method 1: Conda packages detected (from dependencies.yaml):") + for pkg in conda_packages: + print(f" {pkg}") + if conda_prefix: + print(f" Environment: {conda_prefix}") + return "conda" + elif pip_packages: + print("✓ Method 1: Pip packages detected:") + for pkg in pip_packages: + print(f" {pkg}") + return "pip" + + # Check for CMake build integration (Method 2 - Alternative) + build_dir = find_plugin_build_dir() + if build_dir: + print("ℹ Checking Method 2: CMake build integration...") + print(f" Build directory: {build_dir}") + + deps_dirs = [ + build_dir / "_deps", + build_dir.parent.parent.parent / "build-release/_deps", + ] + + nvimgcodec_found = False + for deps_dir in deps_dirs: + if not deps_dir.exists(): + continue + for item in deps_dir.iterdir(): + if "nvimgcodec" in item.name.lower(): + nvimgcodec_found = True + print("✓ Method 2: nvImageCodec integrated via CMake:") + print(f" {item.name}") + return "cmake-integrated" + + if not nvimgcodec_found: + print("✗ Method 2: No CMake integration found") + + # Neither method found + if conda_prefix: + print("✗ nvImageCodec not found via any method") + print(" Expected: conda packages from dependencies.yaml") + else: + print("ℹ Not in a conda environment") + print("✗ No nvImageCodec installation detected") + + return None + + +def check_nvimgcodec_library_in_deps(): + """Check for nvImageCodec library files in plugin dependencies""" + build_dir = find_plugin_build_dir() + + if not build_dir: + return False + + # Search for nvImageCodec library in build dependencies + search_paths = [ + build_dir / "_deps", + build_dir.parent.parent.parent / "build-release/_deps", + ] + + library_names = ["libnvimgcodec.so", "libnvimgcodec.so.0", "libnvimgcodec.a"] + + for search_path in search_paths: + if not search_path.exists(): + continue + + # Recursively search for library files + for lib_name in library_names: + for lib_file in search_path.rglob(lib_name): + print( + f"✓ nvImageCodec library found: {lib_file.relative_to(search_path.parent)}" + ) + return True + + return False + + +def check_library_files(install_method): + """Check for nvImageCodec library files based on installation method""" + print_section("nvImageCodec Library Files Check") + + search_paths = [] + + # Method 1: Check conda/pip installation paths + if install_method in ["conda", "pip"]: + conda_prefix = os.environ.get("CONDA_PREFIX") + if conda_prefix: + search_paths.extend([f"{conda_prefix}/lib", f"{conda_prefix}/include"]) + # Add Python site-packages paths for pip installations + for py_ver in ["3.14", "3.13", "3.12", "3.11", "3.10", "3.9"]: + search_paths.append( + f"{conda_prefix}/lib/python{py_ver}/site-packages/nvidia/nvimgcodec" + ) + # Add system paths + search_paths.extend( + ["/usr/local/lib", "/usr/lib", "/usr/local/include", "/usr/include"] + ) + + # Method 2: Check CMake build dependencies + if install_method == "cmake-integrated": + build_dir = find_plugin_build_dir() + if build_dir: + search_paths.extend( + [ + str(build_dir / "_deps"), + str(build_dir.parent.parent.parent / "build-release/_deps"), + ] + ) + + if not search_paths: + print("✗ No search paths available - installation method unknown") + return False + + header_found = False + library_found = False + + # Look for header files + for path in search_paths: + path_obj = Path(path) + if not path_obj.exists(): + continue + + # Check for header directly + header_path = path_obj / "nvimgcodec.h" + if header_path.exists(): + print(f"✓ Header found: {header_path}") + header_found = True + break + + # Check include subdirectory + include_path = path_obj / "include" / "nvimgcodec.h" + if include_path.exists(): + print(f"✓ Header found: {include_path}") + header_found = True + break + + # For CMake integration, search recursively + if install_method == "cmake-integrated": + for header_file in path_obj.rglob("nvimgcodec.h"): + print(f"✓ Header found: {header_file}") + header_found = True + break + + if header_found: + break + + if not header_found: + print("✗ nvimgcodec.h header file not found") + + # Look for library files + library_names = [ + "libnvimgcodec.so.0", + "libnvimgcodec.so", + "libnvimgcodec.a", + "libnvimgcodec.dylib", + "nvimgcodec.dll", + ] + + for path in search_paths: + path_obj = Path(path) + if not path_obj.exists(): + continue + + for lib_name in library_names: + lib_path = path_obj / lib_name + if lib_path.exists(): + size_mb = lib_path.stat().st_size / (1024 * 1024) + print(f"✓ Library found: {lib_path} ({size_mb:.1f} MB)") + library_found = True + break + + # Check lib subdirectory + lib_subpath = path_obj / "lib" / lib_name + if lib_subpath.exists(): + size_mb = lib_subpath.stat().st_size / (1024 * 1024) + print(f"✓ Library found: {lib_subpath} ({size_mb:.1f} MB)") + library_found = True + break + + # For CMake integration, search recursively + if install_method == "cmake-integrated": + for lib_file in path_obj.rglob(lib_name): + size_mb = lib_file.stat().st_size / (1024 * 1024) + print(f"✓ Library found: {lib_file} ({size_mb:.1f} MB)") + library_found = True + break + + if library_found: + break + + if library_found: + break + + if not library_found: + print("✗ nvImageCodec library file not found") + + return header_found and library_found + + +def check_cmake_configuration(): + """Check CMake configuration for nvImageCodec""" + print_section("CMake Build Configuration Check") + + build_dir = find_plugin_build_dir() + + if not build_dir: + print("✗ No build directory found - plugin not built yet") + return False + + # Check for CMakeCache.txt which contains build configuration + cmake_cache = build_dir / "CMakeCache.txt" + + if not cmake_cache.exists(): + print("✗ CMakeCache.txt not found - incomplete build") + return False + + print(f"✓ CMake cache found: {cmake_cache}") + + # Read and check for nvImageCodec-related configuration + try: + with open(cmake_cache) as f: + cache_content = f.read() + + has_nvimgcodec_config = "nvimgcodec" in cache_content.lower() + + if has_nvimgcodec_config: + print("✓ nvImageCodec configuration found in CMake cache") + + # Look for specific nvImageCodec variables + for line in cache_content.split("\n"): + if ( + "nvimgcodec" in line.lower() + and not line.startswith("//") + and "=" in line + ): + print(f" {line.strip()}") + + return True + else: + print("⚠ No nvImageCodec configuration in CMake cache") + print(" The plugin may need to be rebuilt with nvImageCodec support") + return False + + except Exception as e: + print(f"✗ Error reading CMake cache: {e}") + return False + + +def test_python_import(): + """Test Python import of nvImageCodec (if available)""" + print_section("Python Import Test") + + # Try to import nvImageCodec Python bindings (if they exist) + try: + import nvidia.nvimgcodec + + print("✓ nvidia.nvimgcodec module imported successfully") + + # Try to get version + if hasattr(nvidia.nvimgcodec, "__version__"): + print(f" Version: {nvidia.nvimgcodec.__version__}") + + return True + except ImportError as e: + print("⚠ nvidia.nvimgcodec Python module not available") + print(f" Error: {e}") + return False + + +def check_infrastructure_only_mode(): + """Check if this is an infrastructure-only build (no actual plugin binary).""" + script_dir = Path(__file__).parent.parent + cmake_file = script_dir / "cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt" + + if not cmake_file.exists(): + return False + + content = cmake_file.read_text() + # Look for infrastructure-only indicators + if ( + "infrastructure-only" in content.lower() + and "add_custom_target(${CUCIM_PLUGIN_NAME}" in content + ): + return True + return False + + +def check_cuslide2_plugin(): + """Check if cuslide2 plugin is built and integrated with nvImageCodec""" + print_section("cuslide2 Plugin Check") + + # Get script directory and find plugin paths + script_dir = Path(__file__).parent.parent # Go up to cucim root + + # First check if this is an infrastructure-only build + if check_infrastructure_only_mode(): + print("ℹ️ Infrastructure-only build detected") + print(" ✓ This build validates dependencies without generating plugin binary") + print(" ✓ CMake configuration verified - dependencies checked") + print(" ℹ️ Plugin source code not yet added (planned for follow-up PR)") + print("\n 📋 Infrastructure-only build is working as intended!") + return "infrastructure-only" + + # Possible plugin locations + plugin_paths = [ + script_dir / "cpp/plugins/cucim.kit.cuslide2/build-release/lib", + script_dir / "cpp/plugins/cucim.kit.cuslide2/build/lib", + script_dir / "install/lib", + ] + + plugin_file = None + for plugin_dir in plugin_paths: + if plugin_dir.exists(): + # Look for cucim.kit.cuslide2@*.so + for so_file in plugin_dir.glob("cucim.kit.cuslide2@*.so"): + plugin_file = so_file + break + if plugin_file: + break + + if not plugin_file: + print("✗ cuslide2 plugin not found") + print(" Expected locations:") + for path in plugin_paths: + print(f" {path}/cucim.kit.cuslide2@*.so") + print("\n 💡 To build the plugin with nvImageCodec:") + print(" cd cucim && ./run build_local all release $CONDA_PREFIX") + print(" (nvImageCodec will be automatically fetched during build)") + return False + + print(f"✓ Plugin found: {plugin_file.name}") + + # Check file size + size_mb = plugin_file.stat().st_size / (1024 * 1024) + print(f" Size: {size_mb:.1f} MB") + print(f" Location: {plugin_file.parent}") + + # Check if linked with nvImageCodec using ldd + success, output, error = run_command(f"ldd {plugin_file} 2>&1") + if success: + has_nvimgcodec = "nvimgcodec" in output + has_cuda = "cuda" in output.lower() + + if has_nvimgcodec: + print("✓ Plugin linked with nvImageCodec") + # Show the nvimgcodec line + for line in output.split("\n"): + if "nvimgcodec" in line: + print(f" {line.strip()}") + else: + print("ℹ Plugin uses dynamic loading or static linking for nvImageCodec") + print(" Library integrated at build time via plugin system") + + if has_cuda: + print("✓ Plugin linked with CUDA libraries") + + # Check for nvImageCodec symbols using nm + success, output, error = run_command( + f"nm -D {plugin_file} 2>&1 | grep -i nvimg | head -5" + ) + if success and output: + print("✓ Plugin has nvImageCodec symbols:") + for line in output.split("\n")[:3]: + if line.strip(): + print(f" {line.strip()}") + else: + # Try looking for static symbols + success, output, error = run_command( + f"nm {plugin_file} 2>&1 | grep -i nvimg | head -5" + ) + if success and output: + print("✓ Plugin has nvImageCodec symbols (static):") + for line in output.split("\n")[:3]: + if line.strip(): + print(f" {line.strip()}") + + return True + + +def check_cuda_availability(): + """Check CUDA availability""" + print_section("CUDA Environment Check") + + # Check CUDA runtime + success, output, error = run_command("nvidia-smi") + if success: + print("✓ NVIDIA GPU detected:") + # Extract GPU info from nvidia-smi + lines = output.split("\n") + for line in lines: + if "NVIDIA" in line and ( + "GeForce" in line + or "Tesla" in line + or "Quadro" in line + or "RTX" in line + ): + print(f" {line.strip()}") + break + else: + print("⚠ nvidia-smi not available or no NVIDIA GPU detected") + + # Check CUDA version + success, output, error = run_command("nvcc --version") + if success: + for line in output.split("\n"): + if "release" in line.lower(): + print(f"✓ CUDA compiler: {line.strip()}") + break + else: + print("⚠ CUDA compiler (nvcc) not available") + + +def main(): + """Main verification function""" + print_header("cuslide2 Infrastructure Verification") + print(f"Platform: {platform.system()} {platform.release()}") + print(f"Python: {sys.version}") + + # Detect installation method (conda/pip or CMake integration) + install_method = detect_installation_method() + + # Check library files based on installation method + library_files_ok = check_library_files(install_method) if install_method else False + + # Check CMake configuration (optional, informational), no assignment + check_cmake_configuration() + + # Check CUDA environment + check_cuda_availability() + + # Check plugin + plugin_built = check_cuslide2_plugin() + + # Test Python import + test_python_import() + + # Summary + print_header("Infrastructure Summary") + + # Check if this is an infrastructure-only build + if plugin_built == "infrastructure-only": + print("🎉 cuslide2 infrastructure validation PASSED!") + print("✓ This is an infrastructure-only build") + print("✓ CMake configuration verified") + print("✓ All dependencies can be fetched/built") + if install_method: + if install_method == "conda": + print("✓ nvImageCodec packages detected via conda") + elif install_method == "pip": + print("✓ nvImageCodec packages detected via pip") + elif install_method == "cmake-integrated": + print("✓ nvImageCodec CMake integration configured") + print( + "\nℹ️ Note: Plugin binary intentionally not built in infrastructure-only mode" + ) + print(" Actual plugin implementation will be added in follow-up PR") + return True + + all_good = library_files_ok and plugin_built and install_method + + if all_good: + print("🎉 cuslide2 infrastructure is properly set up!") + if install_method == "conda": + print("✓ nvImageCodec installed via conda packages (dependencies.yaml)") + elif install_method == "pip": + print("✓ nvImageCodec installed via pip packages") + elif install_method == "cmake-integrated": + print("✓ nvImageCodec integrated via CMake build system") + print("✓ All required library files accessible") + print("✓ cuslide2 plugin built successfully") + + return True + elif library_files_ok and not plugin_built: + print("⚠ Partial setup - nvImageCodec installed but plugin not built") + print("✓ nvImageCodec is installed") + print("✗ cuslide2 plugin not built yet") + print("\n📋 Next step: Build the plugin") + print(" cd cucim && ./run build_local all release $CONDA_PREFIX") + + return False + elif not install_method or not library_files_ok: + print("✗ nvImageCodec installation incomplete or not found") + + print("\n🔧 Installation Options:") + print("Option 1 (Recommended - via dependencies.yaml):") + print(" micromamba install libnvimgcodec-dev libnvimgcodec0 -c conda-forge") + print("Option 2 (Pip):") + print(" pip install nvidia-nvimgcodec-cu12 # For CUDA 12.x") + print("Option 3 (CMake auto-fetch):") + print(" Build will automatically fetch nvImageCodec if not found") + + print( + "\n💡 The project dependencies.yaml specifies conda packages as the preferred method." + ) + + return False + else: + print("✗ Setup incomplete") + print("\n🔧 Recommendation:") + print(" Install nvImageCodec and build the plugin:") + print( + " 1. micromamba install libnvimgcodec-dev libnvimgcodec0 -c conda-forge" + ) + print(" 2. cd cucim && ./run build_local all release $CONDA_PREFIX") + return False + + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1)