From d25d71dbaeee4721b41991f546629f917f108d86 Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 10:42:33 -0800 Subject: [PATCH 01/34] feat: Add GPU-accelerated TIFF decoding via nvImageCodec v0.6.0 --- .gitignore | 4 + conda/recipes/libcucim/meta.yaml | 1 + cpp/cmake/deps/nvimgcodec.cmake | 232 ++ cpp/plugins/cucim.kit.cuslide2/.clang-format | 86 + cpp/plugins/cucim.kit.cuslide2/.editorconfig | 7 + cpp/plugins/cucim.kit.cuslide2/.gitignore | 2 + cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt | 270 +-- .../benchmarks/CMakeLists.txt | 37 + .../cucim.kit.cuslide2/benchmarks/config.h | 69 + .../cucim.kit.cuslide2/benchmarks/main.cpp | 256 +++ .../cmake/cucim.kit.cuslide-config.cmake.in | 15 + .../cmake/modules/CuCIMUtils.cmake | 52 + .../src/cuslide/cuslide.cpp | 382 ++++ .../cucim.kit.cuslide2/src/cuslide/cuslide.h | 8 + .../cuslide/nvimgcodec/nvimgcodec_decoder.cpp | 454 ++++ .../cuslide/nvimgcodec/nvimgcodec_decoder.h | 57 + .../nvimgcodec/nvimgcodec_tiff_parser.cpp | 870 ++++++++ .../nvimgcodec/nvimgcodec_tiff_parser.h | 432 ++++ .../src/cuslide/tiff/ifd.cpp | 1383 ++++++++++++ .../cucim.kit.cuslide2/src/cuslide/tiff/ifd.h | 174 ++ .../src/cuslide/tiff/tiff.cpp | 1270 +++++++++++ .../src/cuslide/tiff/tiff.h | 127 ++ .../src/cuslide/tiff/tiff_constants.h | 53 + .../src/cuslide/tiff/types.h | 70 + .../src/nvimgcodec_dynlink/nvimgcodec.h | 1942 +++++++++++++++++ .../nvimgcodec_stubs_generated.cc | 260 +++ .../nvimgcodec_dynlink/nvimgcodec_version.h | 23 + .../src/nvimgcodec_dynlink/nvimgcodec_wrap.cc | 87 + cpp/plugins/cucim.kit.cuslide2/test_data | 1 + .../cucim.kit.cuslide2/tests/CMakeLists.txt | 59 + cpp/plugins/cucim.kit.cuslide2/tests/config.h | 52 + cpp/plugins/cucim.kit.cuslide2/tests/main.cpp | 95 + .../tests/test_philips_tiff.cpp | 59 + .../tests/test_read_rawtiff.cpp | 390 ++++ .../tests/test_read_region.cpp | 119 + cpp/src/cuimage.cpp | 16 +- cpp/src/loader/thread_batch_data_loader.cpp | 86 +- notebooks/Using_Cache.ipynb | 2 +- notebooks/input/README.md | 13 +- run | 14 + scripts/test_aperio_svs.py | 284 +++ scripts/test_philips_tiff.py | 339 +++ 42 files changed, 10002 insertions(+), 150 deletions(-) create mode 100644 cpp/cmake/deps/nvimgcodec.cmake create mode 100644 cpp/plugins/cucim.kit.cuslide2/.clang-format create mode 100644 cpp/plugins/cucim.kit.cuslide2/.editorconfig create mode 100644 cpp/plugins/cucim.kit.cuslide2/.gitignore create mode 100644 cpp/plugins/cucim.kit.cuslide2/benchmarks/CMakeLists.txt create mode 100644 cpp/plugins/cucim.kit.cuslide2/benchmarks/config.h create mode 100644 cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp create mode 100644 cpp/plugins/cucim.kit.cuslide2/cmake/cucim.kit.cuslide-config.cmake.in create mode 100644 cpp/plugins/cucim.kit.cuslide2/cmake/modules/CuCIMUtils.cmake create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.cpp create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.cpp create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.h create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.cpp create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.h create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.cpp create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.h create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/tiff.cpp create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/tiff.h create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/tiff_constants.h create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/types.h create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec.h create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec_stubs_generated.cc create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec_version.h create mode 100644 cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec_wrap.cc create mode 120000 cpp/plugins/cucim.kit.cuslide2/test_data create mode 100644 cpp/plugins/cucim.kit.cuslide2/tests/CMakeLists.txt create mode 100644 cpp/plugins/cucim.kit.cuslide2/tests/config.h create mode 100644 cpp/plugins/cucim.kit.cuslide2/tests/main.cpp create mode 100644 cpp/plugins/cucim.kit.cuslide2/tests/test_philips_tiff.cpp create mode 100644 cpp/plugins/cucim.kit.cuslide2/tests/test_read_rawtiff.cpp create mode 100644 cpp/plugins/cucim.kit.cuslide2/tests/test_read_region.cpp create mode 100755 scripts/test_aperio_svs.py create mode 100755 scripts/test_philips_tiff.py 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/meta.yaml b/conda/recipes/libcucim/meta.yaml index 612d9c01c..1a684d341 100644 --- a/conda/recipes/libcucim/meta.yaml +++ b/conda/recipes/libcucim/meta.yaml @@ -24,6 +24,7 @@ build: - cuda-cudart-dev - libcufile-dev - libnvjpeg-dev + - libnvimgcodec-dev script_env: - AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY diff --git a/cpp/cmake/deps/nvimgcodec.cmake b/cpp/cmake/deps/nvimgcodec.cmake new file mode 100644 index 000000000..08c8c5bc6 --- /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.6.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.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 index e760bac62..01c7794a3 100644 --- a/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt +++ b/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt @@ -144,80 +144,36 @@ find_package(cucim CONFIG REQUIRED # TODO: Uncomment this section when src/ directory is added with implementation message(STATUS "=============================================================") -message(STATUS "cuslide2 infrastructure validation - Dependencies verified:") -message(STATUS " ✓ fmt") -message(STATUS " ✓ libjpeg-turbo") -message(STATUS " ✓ libopenjpeg") -message(STATUS " ✓ libtiff") -message(STATUS " ✓ libdeflate") -message(STATUS " ✓ nvImageCodec") -message(STATUS " ✓ pugixml") -message(STATUS " ✓ json") -message(STATUS " ✓ openslide") +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 "Infrastructure setup complete!") -message(STATUS "Next step: Add src/ directory with cuslide2 implementation") +message(STATUS "Build configured: Pure GPU-accelerated decoding with tests!") message(STATUS "=============================================================") -# For infrastructure-only PR: Create a dummy target so build succeeds -# This will be replaced with actual library in follow-up PR -add_custom_target(${CUCIM_PLUGIN_NAME} - COMMAND ${CMAKE_COMMAND} -E echo "cuslide2 infrastructure-only: No library to build - dependencies verified" - COMMENT "Infrastructure-only build - no plugin binary generated" -) -# TODO: Replace dummy target with real library when source files exist -if(FALSE) # Change to TRUE when source files exist -# Add library -# Use local src/ implementation with nvImageCodec support -set(CUSLIDE_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide) -add_library(${CUCIM_PLUGIN_NAME} MODULE - ${CUSLIDE_SRC_DIR}/cuslide.cpp - ${CUSLIDE_SRC_DIR}/cuslide.h - ${CUSLIDE_SRC_DIR}/deflate/deflate.cpp - ${CUSLIDE_SRC_DIR}/deflate/deflate.h - ${CUSLIDE_SRC_DIR}/jpeg/libjpeg_turbo.cpp - ${CUSLIDE_SRC_DIR}/jpeg/libjpeg_turbo.h - ${CUSLIDE_SRC_DIR}/jpeg/libnvjpeg.cpp - ${CUSLIDE_SRC_DIR}/jpeg/libnvjpeg.h - ${CUSLIDE_SRC_DIR}/jpeg2k/color_conversion.cpp - ${CUSLIDE_SRC_DIR}/jpeg2k/color_conversion.h - ${CUSLIDE_SRC_DIR}/jpeg2k/color_table.h - ${CUSLIDE_SRC_DIR}/jpeg2k/libopenjpeg.cpp - ${CUSLIDE_SRC_DIR}/jpeg2k/libopenjpeg.h - ${CUSLIDE_SRC_DIR}/loader/nvjpeg_processor.cpp - ${CUSLIDE_SRC_DIR}/loader/nvjpeg_processor.h - ${deps-libopenjpeg_SOURCE_DIR}/src/bin/common/color.c # for color_sycc_to_rgb() and color_apply_icc_profile() - ${CUSLIDE_SRC_DIR}/lzw/lzw.cpp - ${CUSLIDE_SRC_DIR}/lzw/lzw.h - ${CUSLIDE_SRC_DIR}/lzw/lzw_libtiff.cpp - ${CUSLIDE_SRC_DIR}/lzw/lzw_libtiff.h - ${CUSLIDE_SRC_DIR}/raw/raw.cpp - ${CUSLIDE_SRC_DIR}/raw/raw.h - ${CUSLIDE_SRC_DIR}/tiff/ifd.cpp - ${CUSLIDE_SRC_DIR}/tiff/ifd.h - ${CUSLIDE_SRC_DIR}/tiff/tiff.cpp - ${CUSLIDE_SRC_DIR}/tiff/tiff.h - ${CUSLIDE_SRC_DIR}/tiff/types.h - # nvImageCodec decoder implementation - ${CUSLIDE_SRC_DIR}/nvimgcodec/nvimgcodec_decoder.cpp - ${CUSLIDE_SRC_DIR}/nvimgcodec/nvimgcodec_decoder.h - ${CUSLIDE_SRC_DIR}/nvimgcodec/nvimgcodec_tiff_parser.cpp - ${CUSLIDE_SRC_DIR}/nvimgcodec/nvimgcodec_tiff_parser.h) - -# compile color.c for libopenjpeg with c++ -set_source_files_properties(${deps-libopenjpeg_SOURCE_DIR}/src/bin/common/color.c - PROPERTIES - LANGUAGE C - CMAKE_CXX_VISIBILITY_PRESET default - CMAKE_C_VISIBILITY_PRESET default - CMAKE_VISIBILITY_INLINES_HIDDEN OFF) - -# Ignore warnings in existing source code from libjpeg-turbo -set_source_files_properties(${CUSLIDE_SRC_DIR}/jpeg/libjpeg_turbo.cpp - PROPERTIES - COMPILE_OPTIONS "-Wno-error" # or, "-Wno-write-strings;-Wno-clobbered" - ) +# Use SHARED library (needed for tests to link against it, but still loaded via dlopen at runtime) +add_library(${CUCIM_PLUGIN_NAME} SHARED + # 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} @@ -244,13 +200,61 @@ target_link_libraries(${CUCIM_PLUGIN_NAME} deps::pugixml deps::json deps::libdeflate - $ ) -# Add nvImageCodec compile definition if the target exists -if(TARGET deps::nvimgcodec) - target_compile_definitions(${CUCIM_PLUGIN_NAME} PRIVATE CUCIM_HAS_NVIMGCODEC) - message(STATUS "✓ nvImageCodec enabled - GPU-accelerated JPEG/JPEG2000 decoding available") +# nvImageCodec linking: dynamic (runtime) vs static (compile-time) +option(WITH_DYNAMIC_NVIMGCODEC "Use dynamic loading for nvImageCodec (dlopen at runtime)" ON) + +if(WITH_DYNAMIC_NVIMGCODEC) + + message(STATUS "============== Dynamic nvImageCodec Loading ==============") + + + # Use pre-generated stubs (faster builds, no Python clang dependency) + # To regenerate: cd build-release && make dynlink_nvimgcodec_gen.cc + # then copy to src/nvimgcodec_dynlink/nvimgcodec_stubs_generated.cc + + # Create dynamic loading wrapper library + add_library(nvimgcodec_dynlink STATIC + src/nvimgcodec_dynlink/nvimgcodec_wrap.cc + src/nvimgcodec_dynlink/nvimgcodec_stubs_generated.cc # Pre-generated stubs + ) + set_target_properties(nvimgcodec_dynlink PROPERTIES POSITION_INDEPENDENT_CODE ON) + target_include_directories(nvimgcodec_dynlink SYSTEM PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/src/nvimgcodec_dynlink + ${CUDAToolkit_INCLUDE_DIRS} + ) + target_link_libraries(nvimgcodec_dynlink PRIVATE ${CMAKE_DL_LIBS}) + + # Link cuslide2 against dynlink wrapper (instead of libnvimgcodec.so) + target_link_libraries(${CUCIM_PLUGIN_NAME} PRIVATE nvimgcodec_dynlink) + target_include_directories(${CUCIM_PLUGIN_NAME} SYSTEM PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/nvimgcodec_dynlink) + target_compile_definitions(${CUCIM_PLUGIN_NAME} PRIVATE CUCIM_HAS_NVIMGCODEC WITH_DYNAMIC_NVIMGCODEC) + + # Set rpath for runtime library search + foreach(rpath IN ITEMS + "$ORIGIN" + "$ORIGIN/../nvimgcodec" + "$ORIGIN/../lib" + "$ORIGIN/../../lib") + message(STATUS " Adding to rpath: ${rpath}") + list(APPEND CMAKE_BUILD_RPATH ${rpath}) + list(APPEND CMAKE_INSTALL_RPATH ${rpath}) + endforeach() + + message(STATUS "✓ Dynamic nvImageCodec loading enabled") + message(STATUS " Runtime: libnvimgcodec.so loaded via dlopen") + message(STATUS " Python wheels: Place libnvimgcodec.so in lib/ directory") + message(STATUS "==========================================================") +else() + # Static linking: link directly against nvImageCodec (build-time dependency) + if(TARGET deps::nvimgcodec) + target_link_libraries(${CUCIM_PLUGIN_NAME} PRIVATE deps::nvimgcodec) + target_compile_definitions(${CUCIM_PLUGIN_NAME} PRIVATE CUCIM_HAS_NVIMGCODEC) + message(STATUS "✓ nvImageCodec enabled (static linking) - GPU-accelerated decoding available") + else() + message(STATUS "⚠ nvImageCodec target not found - GPU acceleration disabled") + endif() endif() if (TARGET CUDA::nvjpeg_static) target_link_libraries(${CUCIM_PLUGIN_NAME} @@ -273,7 +277,8 @@ target_include_directories(${CUCIM_PLUGIN_NAME} $ $ PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src + ${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 @@ -293,67 +298,68 @@ set_target_properties(${CUCIM_PLUGIN_NAME} PROPERTIES OUTPUT_NAME "${CUCIM_PLUGI ################################################################################ # Add tests -#########################################################std####################### -# NOTE: Commented out for infrastructure-only PR. Will be enabled in follow-up PR -# with actual implementation. -# add_subdirectory(tests) -# add_subdirectory(benchmarks) +################################################################################ +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}) - -endif() # End of if(FALSE) block for infrastructure-only PR +set(INSTALL_TARGETS + ${CUCIM_PLUGIN_NAME} + # cuslide_tests # Disabled for infrastructure-only PR + # cuslide_benchmarks # Disabled for infrastructure-only PR + ) + +# Add nvimgcodec_dynlink to install targets if dynamic loading is enabled +if(WITH_DYNAMIC_NVIMGCODEC) + list(APPEND INSTALL_TARGETS nvimgcodec_dynlink) +endif() + +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}) 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/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/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..97b65585b --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.cpp @@ -0,0 +1,454 @@ +/* + * 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 +#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 +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 pinned memory for faster GPU-to-host transfers when GPU backend is used + cudaError_t status = cudaMallocHost(&buffer_, size); + return status == cudaSuccess; + } + } + + void reset() + { + if (buffer_) + { + if (is_device_) + cudaFree(buffer_); + else + cudaFreeHost(buffer_); // Pinned memory must use cudaFreeHost + 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 + std::string device_str = std::string(out_device); + bool target_is_cpu = (device_str.find("cpu") != std::string::npos); + + // Always use hybrid decoder - it supports more codecs and can output to both CPU/GPU + // The CPU-only decoder has limited codec support (e.g., no JPEG in some builds) + nvimgcodecDecoder_t decoder = manager.get_decoder(); + + #ifdef DEBUG + if (target_is_cpu) + { + fmt::print(" 💡 Using hybrid decoder for CPU output (better codec support)\n"); + } + else + { + fmt::print(" 💡 Using hybrid decoder for GPU output\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) + { + fmt::print("❌ Failed to create ROI sub-stream (status: {})\n", + static_cast(status)); + return false; + } + UniqueCodeStream roi_stream(roi_stream_raw); + + // Step 2: Determine buffer kind + // IMPORTANT: nvImageCodec v0.6.0 GPU JPEG decoder can ONLY output to device memory + // For CPU requests, we must decode to GPU then copy D2H (device-to-host) + + // Verify GPU is available + int device_count = 0; + cudaError_t cuda_err = cudaGetDeviceCount(&device_count); + bool gpu_available = (cuda_err == cudaSuccess && device_count > 0); + + if (!gpu_available) + { + fmt::print("❌ GPU not available but required for nvImageCodec JPEG decoding\n"); + throw std::runtime_error( + "nvImageCodec GPU JPEG decoder requires CUDA device. " + "No CUDA device found."); + } + + // Always decode to GPU device memory (GPU codec requirement) + nvimgcodecImageBufferKind_t buffer_kind = NVIMGCODEC_IMAGE_BUFFER_KIND_STRIDED_DEVICE; + + #ifdef DEBUG + if (target_is_cpu) + { + fmt::print(" ℹ️ Decoding to GPU (will copy to CPU after decode)\n"); + } + else + { + fmt::print(" ℹ️ Decoding to GPU\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; + output_image_info.buffer_size = buffer_size; + 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)) + { + fmt::print("❌ Failed to allocate {} memory ({} bytes)\n", + use_device_memory ? "GPU" : "host", buffer_size); + 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) + { + fmt::print("❌ Failed to create image object (status: {})\n", + static_cast(status)); + 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) + { + fmt::print("❌ Failed to schedule decoding (status: {}, buffer_kind: {}, size: {}x{})\n", + static_cast(status), + use_device_memory ? "GPU" : "CPU", + width, height); + 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) + { + fmt::print("❌ Failed to get processing status (status: {})\n", static_cast(status)); + return false; // RAII handles cleanup + } + + if (use_device_memory) + { + cudaStreamSynchronize(output_image_info.cuda_stream); + } + + // Step 9: Check decode status + if (decode_status != NVIMGCODEC_PROCESSING_STATUS_SUCCESS) + { + fmt::print("❌ Decoding failed (processing_status: {}, buffer_kind: {}, size: {}x{})\n", + static_cast(decode_status), + use_device_memory ? "GPU" : "CPU", + width, height); + return false; // RAII handles cleanup + } + + #ifdef DEBUG + fmt::print("✅ Successfully decoded IFD[{}] region\n", ifd_info.index); + #endif + + // Step 10: Handle D2H copy if user requested CPU output + if (target_is_cpu) + { + // User requested CPU but we decoded to GPU - copy D2H + size_t buffer_size_bytes = width * height * 3; // RGB + + #ifdef DEBUG + fmt::print(" ℹ️ Copying decoded data from GPU to CPU ({} bytes)...\n", buffer_size_bytes); + #endif + + // Allocate CPU memory using cucim_malloc (standard malloc) + // IMPORTANT: Do NOT use cudaMallocHost here! + // cuCIM will free this buffer with cucim_free/free(), not cudaFreeHost() + uint8_t* cpu_buffer = static_cast(cucim_malloc(buffer_size_bytes)); + if (!cpu_buffer) + { + fmt::print("❌ Failed to allocate CPU memory\n"); + return false; + } + + // Copy from GPU to CPU + cuda_err = cudaMemcpy(cpu_buffer, decode_buffer.get(), buffer_size_bytes, cudaMemcpyDeviceToHost); + if (cuda_err != cudaSuccess) + { + fmt::print("❌ D2H copy failed: {}\n", cudaGetErrorString(cuda_err)); + cucim_free(cpu_buffer); + return false; + } + + #ifdef DEBUG + fmt::print(" ✅ D2H copy completed\n"); + #endif + + // Return CPU buffer (decode_buffer GPU memory will be freed by RAII) + *output_buffer = cpu_buffer; + } + else + { + // GPU output: release buffer ownership to caller (skip RAII cleanup) + *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 cleaned up by RAII + } + catch (const std::exception& e) + { + fmt::print("❌ Exception in ROI decoding: {}\n", e.what()); + return false; + } +} + +#else // !CUCIM_HAS_NVIMGCODEC + +// Fallback stub when nvImageCodec is not available +// cuslide2 plugin requires nvImageCodec, so this should never be called +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..be67fbbaf --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.h @@ -0,0 +1,57 @@ +/* + * 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 +{ + +// Forward declaration (needed in both cases) +struct IfdInfo; + +#ifdef CUCIM_HAS_NVIMGCODEC +// nvImageCodec types are only available when CUCIM_HAS_NVIMGCODEC is defined +// (nvimgcodecCodeStream_t is defined in nvimgcodec.h) +#else +// When nvImageCodec is not available, provide a dummy type for the signature +typedef void* nvimgcodecCodeStream_t; +#endif + +/** + * 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 + * + * @note When CUCIM_HAS_NVIMGCODEC is false, this function throws a runtime error. + */ +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); + +} // 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..6a95e2e49 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.cpp @@ -0,0 +1,870 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +// ============================================================================ +// nvImageCodec v0.6.0 TIFF Parser Implementation +// ============================================================================ +// +// This implementation provides TIFF structure parsing and metadata extraction +// using nvImageCodec v0.6.0 APIs: +// +// 1. **File-level metadata**: Vendor-specific formats (Aperio SVS, Philips TIFF) +// 2. **IFD (Image File Directory) enumeration**: Multi-resolution image structure +// 3. **Compression detection**: Inferred from file extension and vendor metadata +// +// Key Implementation Details: +// - get_nvimgcodec_version(): Returns runtime version (e.g., 600 for v0.6.0) +// - extract_tiff_tags(): Uses file extension heuristics for compression detection +// - Supports JPEG-compressed SVS and TIFF files commonly used in digital pathology +// +// ============================================================================ + +#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 + +// ============================================================================ +// 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; + + fprintf(stderr, "[DEBUG] About to call nvimgcodecInstanceCreate...\n"); + nvimgcodecStatus_t status = nvimgcodecInstanceCreate(&instance_, &create_info); + fprintf(stderr, "[DEBUG] nvimgcodecInstanceCreate returned: %d\n", static_cast(status)); + + 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; + + fprintf(stderr, "[DEBUG] About to call nvimgcodecDecoderCreate...\n"); + status = nvimgcodecDecoderCreate(instance_, &decoder_, &exec_params, nullptr); + fprintf(stderr, "[DEBUG] nvimgcodecDecoderCreate returned: %d\n", static_cast(status)); + + 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; + + fprintf(stderr, "[DEBUG] About to call nvimgcodecDecoderCreate (CPU-only)...\n"); + nvimgcodecStatus_t cpu_status = nvimgcodecDecoderCreate(instance_, &cpu_decoder_, &cpu_exec_params, nullptr); + fprintf(stderr, "[DEBUG] nvimgcodecDecoderCreate (CPU-only) returned: %d\n", static_cast(cpu_status)); + if (cpu_status == 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() +{ + 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 + fprintf(stderr, "[DEBUG] About to call nvimgcodecCodeStreamCreateFromFile...\n"); + nvimgcodecStatus_t status = nvimgcodecCodeStreamCreateFromFile( + manager.get_instance(), + &main_code_stream_, + file_path.c_str() + ); + fprintf(stderr, "[DEBUG] nvimgcodecCodeStreamCreateFromFile returned: %d\n", static_cast(status)); + + 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) + { + // Cleanup on error + fprintf(stderr, "[DEBUG] Exception caught: %s\n", e.what()); + if (main_code_stream_) + { + fprintf(stderr, "[DEBUG] About to call nvimgcodecCodeStreamDestroy (cleanup) with handle=%p...\n", (void*)main_code_stream_); + fflush(stderr); + // Don't call destroy in error path - might cause crash + // nvimgcodecCodeStreamDestroy(main_code_stream_); + fprintf(stderr, "[DEBUG] Skipping nvimgcodecCodeStreamDestroy to avoid crash\n"); + main_code_stream_ = nullptr; + } + + fprintf(stderr, "[DEBUG] Re-throwing exception...\n"); + throw; // Re-throw + } +} + +TiffFileParser::~TiffFileParser() +{ + // Destroy sub-code streams first + 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; + + fprintf(stderr, "[DEBUG] About to call nvimgcodecCodeStreamGetCodeStreamInfo...\n"); + nvimgcodecStatus_t status = nvimgcodecCodeStreamGetCodeStreamInfo( + main_code_stream_, &stream_info); + fprintf(stderr, "[DEBUG] nvimgcodecCodeStreamGetCodeStreamInfo returned: %d\n", static_cast(status)); + + if (status != NVIMGCODEC_STATUS_SUCCESS) + { + fprintf(stderr, "[DEBUG] nvimgcodecCodeStreamGetCodeStreamInfo failed with status %d, throwing exception...\n", static_cast(status)); + 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 + // Clean up the sub_code_stream before continuing + if (ifd_info.sub_code_stream) + { + nvimgcodecCodeStreamDestroy(ifd_info.sub_code_stream); + 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); + + // Current limitation (nvImageCodec v0.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.) + // + + 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 string key "COMPRESSION") + auto compression_it = ifd_info.tiff_tags.find("COMPRESSION"); + if (compression_it != ifd_info.tiff_tags.end()) + { + try + { + // Parse compression value from string + uint16_t compression_value = static_cast(std::stoi(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; + } + } + catch (const std::exception& e) + { + #ifdef DEBUG + fmt::print(" ⚠️ Failed to parse COMPRESSION tag value: {}\n", e.what()); + #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 blobs + // 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_blobs(metadata_count); // Final storage (no copy needed!) + + // 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 final blob storage and point nvImageCodec directly to it (no copy!) + for (int i = 0; i < metadata_count; i++) + { + size_t required_size = metadata_structs[i].buffer_size; + if (required_size > 0) + { + // Pre-allocate blob data and point nvImageCodec buffer directly to it + metadata_blobs[i].data.resize(required_size); + metadata_structs[i].buffer = metadata_blobs[i].data.data(); // Decode directly into final destination! + #ifdef DEBUG + fmt::print(" 📦 Allocated {} bytes for metadata[{}]\n", required_size, i); + #endif + } + } + + // Step 3: Get actual metadata content (decoding directly into final blobs - no copy!) + 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: Move blobs into metadata_blobs map (no copy - just move!) + 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; + + #ifdef DEBUG + // Map kind to human-readable name for debugging (nvImageCodec v0.6.0 enum values) + const char* kind_name = "UNKNOWN"; + switch (kind) { + case NVIMGCODEC_METADATA_KIND_UNKNOWN: kind_name = "UNKNOWN"; 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; + } + fmt::print(" Metadata[{}]: kind={} ({}), format={}, size={}\n", + j, kind, kind_name, format, buffer_size); + #endif + + // Move blob into metadata_blobs map (no copy!) + if (buffer_size > 0) + { + metadata_blobs[j].format = format; + + // Get data pointer BEFORE moving (crucial!) + const uint8_t* data_ptr = metadata_blobs[j].data.data(); + + // Special handling: extract ImageDescription if it's a text format + // nvimgcodecMetadataFormat_t: RAW=0, XML=1, JSON=2, etc. + // For RAW format, treat as text if it looks like ASCII + if (kind == NVIMGCODEC_METADATA_KIND_MED_APERIO && ifd_info.image_description.empty()) + { + // Aperio metadata is typically in RAW format as text + ifd_info.image_description.assign(data_ptr, data_ptr + buffer_size); + #ifdef DEBUG + fmt::print(" ✅ Extracted Aperio ImageDescription ({} bytes)\n", buffer_size); + #endif + } + else if (kind == NVIMGCODEC_METADATA_KIND_MED_PHILIPS && ifd_info.image_description.empty()) + { + // Philips metadata is typically XML + ifd_info.image_description.assign(data_ptr, data_ptr + buffer_size); + #ifdef DEBUG + fmt::print(" ✅ Extracted Philips ImageDescription XML ({} bytes)\n", buffer_size); + + // Show preview of XML + if (buffer_size > 0) { + std::string preview(data_ptr, data_ptr + std::min(buffer_size, size_t(100))); + fmt::print(" XML preview: {}...\n", preview); + } + #endif + } + + // NOW move the blob (after we're done using it!) + ifd_info.metadata_blobs[kind] = std::move(metadata_blobs[j]); + } + } + + // WORKAROUND for nvImageCodec 0.6.0: Philips TIFF metadata limitation + // ======================================================================== + // nvImageCodec 0.6.0 does NOT expose: + // 1. Individual TIFF tags (SOFTWARE, ImageDescription, etc.) + // 2. Philips format detection for some files + // + +} + +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]; +} + +uint32_t TiffFileParser::get_nvimgcodec_version() const +{ + nvimgcodecProperties_t props{}; + props.struct_type = NVIMGCODEC_STRUCTURE_TYPE_PROPERTIES; + props.struct_size = sizeof(nvimgcodecProperties_t); + props.struct_next = nullptr; + + if (nvimgcodecGetProperties(&props) == NVIMGCODEC_STATUS_SUCCESS) + { + return props.version; // Format: major*1000 + minor*100 + patch + } + + return 0; // Unknown/unavailable +} + +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 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.6.0: File extension heuristics for compression detection + // ======================================================================== + // nvImageCodec v0.6.0 does not expose individual TIFF tags, so we infer + // compression type from file extension for common WSI formats. + + int extracted_count = 0; + + // File extension heuristics for known WSI (Whole Slide Imaging) formats + 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") + { + // Only use heuristics if we don't have direct tag access + if (ifd_info.tiff_tags.find("COMPRESSION") == ifd_info.tiff_tags.end()) + { + ifd_info.tiff_tags["COMPRESSION"] = "7"; // TIFF_COMPRESSION_JPEG + #ifdef DEBUG + fmt::print(" ✅ Inferred JPEG compression (WSI format: {})\n", ext); + #endif // DEBUG + extracted_count++; + } + } + + // Store ImageDescription if available + if (!ifd_info.image_description.empty()) + { + if (ifd_info.tiff_tags.find("IMAGEDESCRIPTION") == ifd_info.tiff_tags.end()) + { + ifd_info.tiff_tags["IMAGEDESCRIPTION"] = ifd_info.image_description; + } + } + + // Summary + #ifdef DEBUG + if (extracted_count > 0 || !ifd_info.tiff_tags.empty()) + { + fmt::print(" ✅ Compression detection successful (file extension heuristics)\n"); + } + else + { + fmt::print(" ⚠️ Unable to determine compression type from file extension\n"); + } + #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); + } + + return kinds; +} + +std::string TiffFileParser::get_detected_format() const +{ + if (ifd_infos_.empty()) + return "Unknown"; + + // Check first IFD for vendor-specific metadata + 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"; + 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..48da9f05b --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.h @@ -0,0 +1,432 @@ +/* + * 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 + +namespace cuslide2::nvimgcodec +{ + +#ifdef CUCIM_HAS_NVIMGCODEC + +/** + * @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; + + // TIFF tag storage inferred from file extension and metadata + // tag_name -> tag_value (e.g., "COMPRESSION" -> "7" for JPEG) + std::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; + } + + // ======================================================================== + // TIFF Metadata and Tag Access (nvImageCodec v0.6.0) + // ======================================================================== + + /** + * @brief Get a specific TIFF tag value as string + * + * Returns tags inferred from file extension and vendor metadata (limited). + * nvImageCodec v0.6.0 does not expose individual TIFF tags directly. + * + * @param ifd_index IFD index + * @param tag_name TIFF tag name (case-sensitive, e.g., "COMPRESSION", "IMAGEDESCRIPTION") + * @return Tag value as string, or empty if not found/unavailable + */ + std::string get_tiff_tag(uint32_t ifd_index, const std::string& tag_name) const; + + /** + * @brief Get SUBFILETYPE tag for format classification + * + * Returns the SUBFILETYPE value used in formats like Aperio SVS: + * - 0 = full resolution image + * - 1 = reduced resolution image (thumbnail/label/macro) + * + * Note: nvImageCodec v0.6.0 does not expose SUBFILETYPE tag. + * + * @param ifd_index IFD index + * @return SUBFILETYPE value, or -1 (not available in v0.6.0) + */ + int get_subfile_type(uint32_t ifd_index) const; + + /** + * @brief Query all available metadata kinds in file + * + * Returns a list of metadata kinds present in the file for discovery. + * Useful for detecting file format (Aperio, Philips, Generic TIFF, etc.) + * + * Example kinds: 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 + * + * Automatically detects format by checking available metadata kinds. + * + * @return Format name: "Aperio SVS", "Philips TIFF", "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 Get the runtime nvImageCodec version + * + * Returns the actual version of nvImageCodec library loaded at runtime. + * Format: major*1000 + minor*100 + patch (e.g., 600 for v0.6.0) + * + * @return Version number, or 0 if unavailable + */ + uint32_t get_nvimgcodec_version() const; + +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 TIFF tags using file extension heuristics + * + * nvImageCodec v0.6.0 does not expose individual TIFF tags, so this function + * infers compression type from file extension for common WSI formats. + * + * Populates ifd_info.tiff_tags map with inferred tags (primarily COMPRESSION). + * + * @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_; +}; + +/** + * @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 +enum class ImageType { + RESOLUTION_LEVEL, + THUMBNAIL, + LABEL, + MACRO, + UNKNOWN +}; + +struct IfdInfo { + struct MetadataBlob { + int format; + std::vector data; + }; +}; + +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..c76cbb1bf --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.cpp @@ -0,0 +1,1383 @@ +/* + * 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 owned by TiffFileParser + // TiffFileParser::~TiffFileParser() will destroy all sub-code streams + // DO NOT call nvimgcodecCodeStreamDestroy here to avoid double-free or use-after-free + // + // The destruction order in TIFF is: + // 1. nvimgcodec_parser_ destroyed → TiffFileParser destroys sub-code streams + // 2. ifds_ destroyed → IFD destructors run (we're here) + // + // By this point, sub-code streams are already destroyed, so we 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..cb8e8b5ca --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/tiff.h @@ -0,0 +1,127 @@ +/* + * 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" + +// Forward declaration removed - no longer using libtiff +// typedef struct tiff TIFF; // REMOVED: libtiff forward declaration + +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_; + // REMOVED: file_handle_ raw pointer - use file_handle_shared_.get() instead + // REMOVED: tiff_client_ - no longer using libtiff + 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/src/nvimgcodec_dynlink/nvimgcodec.h b/cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec.h new file mode 100644 index 000000000..440107d87 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec.h @@ -0,0 +1,1942 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + */ + +/** + * @brief The nvImageCodec library and extension API + * + * @file nvimgcodec.h + * + */ + +#ifndef NVIMGCODEC_HEADER +#define NVIMGCODEC_HEADER + +#include +#include +#include +#include "nvimgcodec_version.h" + +#ifndef NVIMGCODECAPI + #ifdef _WIN32 + #define NVIMGCODECAPI __declspec(dllexport) + #elif __GNUC__ >= 4 + #define NVIMGCODECAPI __attribute__((visibility("default"))) + #else + #define NVIMGCODECAPI + #endif +#endif + +#if defined(__cplusplus) +extern "C" +{ +#endif + +/** + * @brief Maximal size of codec name + */ +#define NVIMGCODEC_MAX_CODEC_NAME_SIZE 256 + +/** + * @brief Defines device id as current device + */ +#define NVIMGCODEC_DEVICE_CURRENT -1 + +/** + * @brief Defines device id as a CPU only + */ +#define NVIMGCODEC_DEVICE_CPU_ONLY -99999 + +/** + * @brief Maximal number of dimensions. + */ +#define NVIMGCODEC_MAX_NUM_DIM 5 + +/** + * @brief Maximum number of image planes. + */ +#define NVIMGCODEC_MAX_NUM_PLANES 32 + +/** + * @brief Maximum number of JPEG2000 resolutions. + */ +#define NVIMGCODEC_JPEG2K_MAXRES 33 + + /** + * @brief Opaque nvImageCodec library instance type. + */ + struct nvimgcodecInstance; + + /** + * @brief Handle to opaque nvImageCodec library instance type. + */ + typedef struct nvimgcodecInstance* nvimgcodecInstance_t; + + /** + * @brief Opaque Image type. + */ + struct nvimgcodecImage; + + /** + * @brief Handle to opaque Image type. + */ + typedef struct nvimgcodecImage* nvimgcodecImage_t; + + /** + * @brief Opaque Code Stream type. + */ + struct nvimgcodecCodeStream; + + /** + * @brief Handle to opaque Code Stream type. + */ + typedef struct nvimgcodecCodeStream* nvimgcodecCodeStream_t; + + /** + * @brief Opaque Parser type. + */ + struct nvimgcodecParser; + + /** + * @brief Handle to opaque Parser type. + */ + typedef struct nvimgcodecParser* nvimgcodecParser_t; + + /** + * @brief Opaque Encoder type. + */ + struct nvimgcodecEncoder; + + /** + * @brief Handle to opaque Encoder type. + */ + typedef struct nvimgcodecEncoder* nvimgcodecEncoder_t; + + /** + * @brief Opaque Decoder type. + */ + struct nvimgcodecDecoder; + + /** + * @brief Handle to opaque Decoder type. + */ + typedef struct nvimgcodecDecoder* nvimgcodecDecoder_t; + + /** + * @brief Opaque Debug Messenger type. + */ + struct nvimgcodecDebugMessenger; + + /** + * @brief Handle to opaque Debug Messenger type. + */ + typedef struct nvimgcodecDebugMessenger* nvimgcodecDebugMessenger_t; + + /** + * @brief Opaque Extension type. + */ + struct nvimgcodecExtension; + + /** + * @brief Handle to opaque Extension type. + */ + typedef struct nvimgcodecExtension* nvimgcodecExtension_t; + + /** + * @brief Opaque Future type. + */ + struct nvimgcodecFuture; + + /** + * @brief Handle to opaque Future type. + */ + typedef struct nvimgcodecFuture* nvimgcodecFuture_t; + + /** + * @brief Structure types supported by the nvImageCodec API. + * + * Each value corresponds to a particular structure with a type member and matching structure name. + */ + typedef enum + { + NVIMGCODEC_STRUCTURE_TYPE_UNKNOWN = 0, + NVIMGCODEC_STRUCTURE_TYPE_PROPERTIES, + NVIMGCODEC_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + NVIMGCODEC_STRUCTURE_TYPE_DEVICE_ALLOCATOR, + NVIMGCODEC_STRUCTURE_TYPE_PINNED_ALLOCATOR, + NVIMGCODEC_STRUCTURE_TYPE_DECODE_PARAMS, + NVIMGCODEC_STRUCTURE_TYPE_ENCODE_PARAMS, + NVIMGCODEC_STRUCTURE_TYPE_ORIENTATION, + NVIMGCODEC_STRUCTURE_TYPE_REGION, + NVIMGCODEC_STRUCTURE_TYPE_CODE_STREAM_VIEW, + NVIMGCODEC_STRUCTURE_TYPE_CODE_STREAM_INFO, + NVIMGCODEC_STRUCTURE_TYPE_IMAGE_INFO, + NVIMGCODEC_STRUCTURE_TYPE_IMAGE_PLANE_INFO, + NVIMGCODEC_STRUCTURE_TYPE_JPEG_IMAGE_INFO, + NVIMGCODEC_STRUCTURE_TYPE_JPEG_ENCODE_PARAMS, + NVIMGCODEC_STRUCTURE_TYPE_TILE_GEOMETRY_INFO, + NVIMGCODEC_STRUCTURE_TYPE_JPEG2K_ENCODE_PARAMS, + NVIMGCODEC_STRUCTURE_TYPE_BACKEND, + NVIMGCODEC_STRUCTURE_TYPE_IO_STREAM_DESC, + NVIMGCODEC_STRUCTURE_TYPE_FRAMEWORK_DESC, + NVIMGCODEC_STRUCTURE_TYPE_DECODER_DESC, + NVIMGCODEC_STRUCTURE_TYPE_ENCODER_DESC, + NVIMGCODEC_STRUCTURE_TYPE_PARSER_DESC, + NVIMGCODEC_STRUCTURE_TYPE_IMAGE_DESC, + NVIMGCODEC_STRUCTURE_TYPE_CODE_STREAM_DESC, + NVIMGCODEC_STRUCTURE_TYPE_DEBUG_MESSENGER_DESC, + NVIMGCODEC_STRUCTURE_TYPE_DEBUG_MESSAGE_DATA, + NVIMGCODEC_STRUCTURE_TYPE_EXTENSION_DESC, + NVIMGCODEC_STRUCTURE_TYPE_EXECUTOR_DESC, + NVIMGCODEC_STRUCTURE_TYPE_BACKEND_PARAMS, + NVIMGCODEC_STRUCTURE_TYPE_EXECUTION_PARAMS, + NVIMGCODEC_STRUCTURE_TYPE_METADATA, + NVIMGCODEC_STRUCTURE_TYPE_METADATA_KIND, + NVIMGCODEC_STRUCTURE_TYPE_METADATA_FORMAT, + NVIMGCODEC_STRUCTURE_TYPE_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecStructureType_t; + + /** + * @brief The nvImageCodec properties. + * + * @see nvimgcodecGetProperties + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + uint32_t version; /**< The nvImageCodec library version. */ + uint32_t cudart_version; /**< The version of CUDA Runtime with which nvImageCodec library was built. */ + + } nvimgcodecProperties_t; + + /** + * @brief Function type for device memory resource allocation. + * + * @param [in] ctx Pointer to user context. + * @param [in] ptr Pointer where to write pointer to allocated memory. + * @param [in] size How many bytes to allocate. + * @param [in] stream CUDA stream + * @returns They will return 0 in case of success, and non-zero otherwise + */ + typedef int (*nvimgcodecDeviceMalloc_t)(void* ctx, void** ptr, size_t size, cudaStream_t stream); + + /** + * @brief Function type for device memory deallocation. + * + * @param [in] ctx Pointer to user context. + * @param [in] ptr Pointer to memory buffer to be deallocated. + * If NULL, the operation must do nothing, successfully. + * @param [in] size How many bytes was allocated (size passed during allocation). + * @param [in] stream CUDA stream + * @returns They will return 0 in case of success, and non-zero otherwise + */ + typedef int (*nvimgcodecDeviceFree_t)(void* ctx, void* ptr, size_t size, cudaStream_t stream); + + /** + * @brief Function type for host pinned memory resource allocation. + * + * @param [in] ctx Pointer to user context. + * @param [in] ptr Pointer where to write pointer to allocated memory. + * @param [in] size How many bytes to allocate. + * @param [in] stream CUDA stream + * @returns They will return 0 in case of success, and non-zero otherwise + */ + typedef int (*nvimgcodecPinnedMalloc_t)(void* ctx, void** ptr, size_t size, cudaStream_t stream); + + /** + * @brief Function type for host pinned memory deallocation. + * + * @param [in] ctx Pointer to user context. + * @param [in] ptr Pointer to memory buffer to be deallocated. + * If NULL, the operation must do nothing, successfully. + * @param [in] size How many bytes was allocated (size passed during allocation). + * @param [in] stream CUDA stream + * @returns They will return 0 in case of success, and non-zero otherwise + */ + typedef int (*nvimgcodecPinnedFree_t)(void* ctx, void* ptr, size_t size, cudaStream_t stream); + + /** + * @brief Device memory allocator. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + nvimgcodecDeviceMalloc_t device_malloc; /**< Allocate memory on the device. */ + nvimgcodecDeviceFree_t device_free; /**< Frees memory on the device.*/ + void* device_ctx; /**< When invoking the allocators, this context will + be pass as input to allocator functions.*/ + size_t device_mem_padding; /**< Any device memory allocation + would be padded to the multiple of specified number of bytes */ + } nvimgcodecDeviceAllocator_t; + + /** + * @brief Host pinned memory allocator. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + nvimgcodecPinnedMalloc_t pinned_malloc; /**< Allocate host pinned memory: memory directly + accessible by both CPU and cuda-enabled GPU. */ + nvimgcodecPinnedFree_t pinned_free; /**< Frees host pinned memory.*/ + void* pinned_ctx; /**< When invoking the allocators, this context will + be pass as input to allocator functions.*/ + size_t pinned_mem_padding; /**< Any pinned host memory allocation + would be padded to the multiple of specified number of bytes */ + } nvimgcodecPinnedAllocator_t; + + /** + * @brief The return status codes of the nvImageCodec API + */ + typedef enum + { + NVIMGCODEC_STATUS_SUCCESS = 0, + NVIMGCODEC_STATUS_NOT_INITIALIZED = 1, + NVIMGCODEC_STATUS_INVALID_PARAMETER = 2, + NVIMGCODEC_STATUS_BAD_CODESTREAM = 3, + NVIMGCODEC_STATUS_CODESTREAM_UNSUPPORTED = 4, + NVIMGCODEC_STATUS_ALLOCATOR_FAILURE = 5, + NVIMGCODEC_STATUS_EXECUTION_FAILED = 6, + NVIMGCODEC_STATUS_ARCH_MISMATCH = 7, + NVIMGCODEC_STATUS_INTERNAL_ERROR = 8, + NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED = 9, + NVIMGCODEC_STATUS_MISSED_DEPENDENCIES = 10, + NVIMGCODEC_STATUS_EXTENSION_NOT_INITIALIZED = 11, + NVIMGCODEC_STATUS_EXTENSION_INVALID_PARAMETER = 12, + NVIMGCODEC_STATUS_EXTENSION_BAD_CODE_STREAM = 13, + NVIMGCODEC_STATUS_EXTENSION_CODESTREAM_UNSUPPORTED = 14, + NVIMGCODEC_STATUS_EXTENSION_ALLOCATOR_FAILURE = 15, + NVIMGCODEC_STATUS_EXTENSION_ARCH_MISMATCH = 16, + NVIMGCODEC_STATUS_EXTENSION_INTERNAL_ERROR = 17, + NVIMGCODEC_STATUS_EXTENSION_IMPLEMENTATION_NOT_SUPPORTED = 18, + NVIMGCODEC_STATUS_EXTENSION_INCOMPLETE_BITSTREAM = 19, + NVIMGCODEC_STATUS_EXTENSION_EXECUTION_FAILED = 20, + NVIMGCODEC_STATUS_EXTENSION_CUDA_CALL_ERROR = 21, + NVIMGCODEC_STATUS_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecStatus_t; + + /** + * @brief Describes type sample of data. + * + * Meaning of bits: + * 0 bit -> 0 - unsigned, 1- signed + * 1..7 bits -> define type + * 8..15 bits -> type bitdepth + * + */ + typedef enum + { + NVIMGCODEC_SAMPLE_DATA_TYPE_UNKNOWN = 0, + NVIMGCODEC_SAMPLE_DATA_TYPE_INT8 = 0x0801, + NVIMGCODEC_SAMPLE_DATA_TYPE_UINT8 = 0x0802, + NVIMGCODEC_SAMPLE_DATA_TYPE_INT16 = 0x1003, + NVIMGCODEC_SAMPLE_DATA_TYPE_UINT16 = 0x1004, + NVIMGCODEC_SAMPLE_DATA_TYPE_INT32 = 0x2005, + NVIMGCODEC_SAMPLE_DATA_TYPE_UINT32 = 0x2006, + NVIMGCODEC_SAMPLE_DATA_TYPE_INT64 = 0x4007, + NVIMGCODEC_SAMPLE_DATA_TYPE_UINT64 = 0x4008, + NVIMGCODEC_SAMPLE_DATA_TYPE_FLOAT16 = 0x1009, + NVIMGCODEC_SAMPLE_DATA_TYPE_FLOAT32 = 0x200B, + NVIMGCODEC_SAMPLE_DATA_TYPE_FLOAT64 = 0x400D, + NVIMGCODEC_SAMPLE_DATA_TYPE_UNSUPPORTED = -1, + NVIMGCODEC_SAMPLE_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecSampleDataType_t; + + /** + * @brief Chroma subsampling. + */ + typedef enum + { + NVIMGCODEC_SAMPLING_NONE = 0, + NVIMGCODEC_SAMPLING_444 = NVIMGCODEC_SAMPLING_NONE, + NVIMGCODEC_SAMPLING_422 = 2, + NVIMGCODEC_SAMPLING_420 = 3, + NVIMGCODEC_SAMPLING_440 = 4, + NVIMGCODEC_SAMPLING_411 = 5, + NVIMGCODEC_SAMPLING_410 = 6, + NVIMGCODEC_SAMPLING_GRAY = 7, + NVIMGCODEC_SAMPLING_410V = 8, + NVIMGCODEC_SAMPLING_UNSUPPORTED = -1, + NVIMGCODEC_SAMPLING_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecChromaSubsampling_t; + + /** + * @brief Provides information how color components are matched to channels in given order and channels are matched to planes. + */ + typedef enum + { + NVIMGCODEC_SAMPLEFORMAT_UNKNOWN = 0, + NVIMGCODEC_SAMPLEFORMAT_P_UNCHANGED = 1, //**< unchanged planar */ + NVIMGCODEC_SAMPLEFORMAT_I_UNCHANGED = 2, //**< unchanged interleaved */ + NVIMGCODEC_SAMPLEFORMAT_P_RGB = 3, //**< planar RGB */ + NVIMGCODEC_SAMPLEFORMAT_I_RGB = 4, //**< interleaved RGB */ + NVIMGCODEC_SAMPLEFORMAT_P_BGR = 5, //**< planar BGR */ + NVIMGCODEC_SAMPLEFORMAT_I_BGR = 6, //**< interleaved BGR */ + NVIMGCODEC_SAMPLEFORMAT_P_Y = 7, //**< Y component only */ + NVIMGCODEC_SAMPLEFORMAT_P_YUV = 9, //**< YUV planar format */ + NVIMGCODEC_SAMPLEFORMAT_UNSUPPORTED = -1, + NVIMGCODEC_SAMPLEFORMAT_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecSampleFormat_t; + + /** + * @brief Defines color specification. + */ + typedef enum + { + NVIMGCODEC_COLORSPEC_UNKNOWN = 0, + NVIMGCODEC_COLORSPEC_UNCHANGED = NVIMGCODEC_COLORSPEC_UNKNOWN, + NVIMGCODEC_COLORSPEC_SRGB = 1, + NVIMGCODEC_COLORSPEC_GRAY = 2, + NVIMGCODEC_COLORSPEC_SYCC = 3, + NVIMGCODEC_COLORSPEC_CMYK = 4, + NVIMGCODEC_COLORSPEC_YCCK = 5, + NVIMGCODEC_COLORSPEC_UNSUPPORTED = -1, + NVIMGCODEC_COLORSPEC_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecColorSpec_t; + + /** + * @brief Defines orientation of an image. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + int rotated; /**< Rotation angle in degrees (clockwise). Only multiples of 90 are allowed. */ + int flip_x; /**< Flip horizontal 0 or 1*/ + int flip_y; /**< Flip vertical 0 or 1*/ + } nvimgcodecOrientation_t; + + /** + * @brief Defines plane of an image. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + uint32_t width; /**< Plane width. First plane defines width of image. */ + uint32_t height; /**< Plane height. First plane defines height of image.*/ + size_t row_stride; /**< Number of bytes need to offset to next row of plane. */ + uint32_t num_channels; /**< Number of channels. Color components, are always first + but there can be more channels than color components.*/ + nvimgcodecSampleDataType_t sample_type; /**< Sample data type. @see nvimgcodecSampleDataType_t */ + uint8_t precision; /**< Value 0 means that precision is equal to sample type bitdepth */ + } nvimgcodecImagePlaneInfo_t; + + /** + * @brief Defines region of an image. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + int ndim; /**< Number of dimensions, 0 value means no region. */ + int start[NVIMGCODEC_MAX_NUM_DIM]; /**< Region start position at the particular dimension. */ + int end[NVIMGCODEC_MAX_NUM_DIM]; /**< Region end position at the particular dimension. */ + } nvimgcodecRegion_t; + + /** + * @brief Defines buffer kind in which image data is stored. + */ + typedef enum + { + NVIMGCODEC_IMAGE_BUFFER_KIND_UNKNOWN = 0, + NVIMGCODEC_IMAGE_BUFFER_KIND_STRIDED_DEVICE = 1, /**< GPU-accessible with planes in pitch-linear layout. */ + NVIMGCODEC_IMAGE_BUFFER_KIND_STRIDED_HOST = 2, /**< Host-accessible with planes in pitch-linear layout. */ + NVIMGCODEC_IMAGE_BUFFER_KIND_UNSUPPORTED = -1, + NVIMGCODEC_IMAGE_BUFFER_KIND_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecImageBufferKind_t; + + /** + * @brief Defines information about an image. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + char codec_name[NVIMGCODEC_MAX_CODEC_NAME_SIZE]; /**< Information about codec used. Only valid when used with code stream. */ + + nvimgcodecColorSpec_t color_spec; /**< Image color specification. */ + nvimgcodecChromaSubsampling_t chroma_subsampling; /**< Image chroma subsampling. Only valid with chroma components. */ + nvimgcodecSampleFormat_t sample_format; /**< Defines how color components are matched to channels in given order and channels + are matched to planes. */ + nvimgcodecOrientation_t orientation; /**< Image orientation. */ + + uint32_t num_planes; /**< Number of image planes. */ + nvimgcodecImagePlaneInfo_t plane_info[NVIMGCODEC_MAX_NUM_PLANES]; /**< Array with information about image planes. */ + + void* buffer; /**< Pointer to buffer in which image data is stored. */ + size_t buffer_size; /**< Size of buffer in which image data is stored. */ + nvimgcodecImageBufferKind_t buffer_kind; /**< Buffer kind in which image data is stored.*/ + + cudaStream_t cuda_stream; /**< CUDA stream to synchronize with */ + } nvimgcodecImageInfo_t; + + /** + * @brief Defines view (selection) of CodeStream. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + size_t image_idx; /**< Image index starts from 0. */ + nvimgcodecRegion_t region; /**< Region of interest. */ + } nvimgcodecCodeStreamView_t; + + /** + * @brief Defines information about a CodeStream + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + const nvimgcodecCodeStreamView_t* code_stream_view; /**< Code stream view defines concerned part */ + char codec_name[NVIMGCODEC_MAX_CODEC_NAME_SIZE]; /**< Information about codec used. Only valid when used with code stream. */ + + size_t num_images; /**< Number of images in CodeStream. */ + } nvimgcodecCodeStreamInfo_t; + + /** + * @brief JPEG Encoding + * + * Currently parseable JPEG encodings (SOF markers) + * https://www.w3.org/Graphics/JPEG/itu-t81.pdf + * Table B.1 Start of Frame markers + */ + typedef enum + { + NVIMGCODEC_JPEG_ENCODING_UNKNOWN = 0x0, + NVIMGCODEC_JPEG_ENCODING_BASELINE_DCT = 0xc0, + NVIMGCODEC_JPEG_ENCODING_EXTENDED_SEQUENTIAL_DCT_HUFFMAN = 0xc1, + NVIMGCODEC_JPEG_ENCODING_PROGRESSIVE_DCT_HUFFMAN = 0xc2, + NVIMGCODEC_JPEG_ENCODING_LOSSLESS_HUFFMAN = 0xc3, + NVIMGCODEC_JPEG_ENCODING_DIFFERENTIAL_SEQUENTIAL_DCT_HUFFMAN = 0xc5, + NVIMGCODEC_JPEG_ENCODING_DIFFERENTIAL_PROGRESSIVE_DCT_HUFFMAN = 0xc6, + NVIMGCODEC_JPEG_ENCODING_DIFFERENTIAL_LOSSLESS_HUFFMAN = 0xc7, + NVIMGCODEC_JPEG_ENCODING_RESERVED_FOR_JPEG_EXTENSIONS = 0xc8, + NVIMGCODEC_JPEG_ENCODING_EXTENDED_SEQUENTIAL_DCT_ARITHMETIC = 0xc9, + NVIMGCODEC_JPEG_ENCODING_PROGRESSIVE_DCT_ARITHMETIC = 0xca, + NVIMGCODEC_JPEG_ENCODING_LOSSLESS_ARITHMETIC = 0xcb, + NVIMGCODEC_JPEG_ENCODING_DIFFERENTIAL_SEQUENTIAL_DCT_ARITHMETIC = 0xcd, + NVIMGCODEC_JPEG_ENCODING_DIFFERENTIAL_PROGRESSIVE_DCT_ARITHMETIC = 0xce, + NVIMGCODEC_JPEG_ENCODING_DIFFERENTIAL_LOSSLESS_ARITHMETIC = 0xcf, + NVIMGCODEC_JPEG_ENCODING_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecJpegEncoding_t; + + /** + * @brief Defines image information related to JPEG format. + * + * This structure extends information provided in nvimgcodecImageInfo_t + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + nvimgcodecJpegEncoding_t encoding; /**< JPEG encoding type. */ + } nvimgcodecJpegImageInfo_t; + + /** + * @brief Defines image information related to JPEG2000 format. + * + * This structure extends information provided in nvimgcodecImageInfo_t + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + uint32_t num_tiles_y; /**< Number of tile rows. */ + uint32_t num_tiles_x; /**< Number of tile columns. */ + uint32_t tile_height; /**< Height of the tile. */ + uint32_t tile_width; /**< Width of the tile. */ + } nvimgcodecTileGeometryInfo_t; + + /** + * @brief Defines decoding/encoding backend kind. + */ + typedef enum + { + NVIMGCODEC_BACKEND_KIND_CPU_ONLY = 1, /**< Decoding/encoding is executed only on CPU. */ + NVIMGCODEC_BACKEND_KIND_GPU_ONLY = 2, /**< Decoding/encoding is executed only on GPU. */ + NVIMGCODEC_BACKEND_KIND_HYBRID_CPU_GPU = 3, /**< Decoding/encoding is executed on both CPU and GPU.*/ + NVIMGCODEC_BACKEND_KIND_HW_GPU_ONLY = 4, /**< Decoding/encoding is executed on GPU dedicated hardware engine. */ + } nvimgcodecBackendKind_t; + + /** + * @brief Defines how to interpret the load hint parameter. + */ + typedef enum + { + NVIMGCODEC_LOAD_HINT_POLICY_IGNORE = 1, /**< Load hint is not taken into account. */ + NVIMGCODEC_LOAD_HINT_POLICY_FIXED = 2, /**< Load hint is used to calculate the backend batch size once */ + NVIMGCODEC_LOAD_HINT_POLICY_ADAPTIVE_MINIMIZE_IDLE_TIME = + 3, /**< Load hint is used as an initial hint, and it is recalculated on every iteration to reduce the idle time of threads */ + } nvimgcodecLoadHintPolicy_t; + + + /** + * @brief Defines decoding/encoding backend parameters. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + /** + * Hint to calculate the fraction of the batch items that will be picked by this backend. + * This is just a hint and a particular implementation can choose to ignore it. + * Different policies can be selected, see `nvimgcodecLoadHintPolicy_t` + */ + float load_hint; + + /** + * If true, the backend load will be adapted on every iteration to minimize idle time of the threads. + */ + nvimgcodecLoadHintPolicy_t load_hint_policy; + } nvimgcodecBackendParams_t; + + /** + * @brief Defines decoding/encoding backend. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + nvimgcodecBackendKind_t kind; /**< Decoding/encoding backend kind. */ + nvimgcodecBackendParams_t params; /**< Decoding/encoding backend parameters. */ + } nvimgcodecBackend_t; + + /** + * @brief Processing status bitmask for decoding or encoding . + */ + typedef enum + { + NVIMGCODEC_PROCESSING_STATUS_UNKNOWN = 0x0, + NVIMGCODEC_PROCESSING_STATUS_SUCCESS = 0x1, /**< Processing finished with success. */ + NVIMGCODEC_PROCESSING_STATUS_SATURATED = 0x2, /**< Decoder/encoder could potentially process + image but is saturated. + @see nvimgcodecBackendParams_t load_hint. */ + + NVIMGCODEC_PROCESSING_STATUS_FAIL = 0x3, /**< Processing failed because unknown reason. */ + NVIMGCODEC_PROCESSING_STATUS_IMAGE_CORRUPTED = 0x7, /**< Processing failed because compressed image stream is corrupted. */ + NVIMGCODEC_PROCESSING_STATUS_CODEC_UNSUPPORTED = 0xb, /**< Processing failed because codec is unsupported */ + NVIMGCODEC_PROCESSING_STATUS_BACKEND_UNSUPPORTED = 0x13, /**< Processing failed because no one from allowed + backends is supported. */ + NVIMGCODEC_PROCESSING_STATUS_ENCODING_UNSUPPORTED = 0x23, /**< Processing failed because codec encoding is unsupported. */ + NVIMGCODEC_PROCESSING_STATUS_RESOLUTION_UNSUPPORTED = 0x43, /**< Processing failed because image resolution is unsupported. */ + NVIMGCODEC_PROCESSING_STATUS_CODESTREAM_UNSUPPORTED = + 0x83, /**< Processing failed because some feature of compressed stream is unsupported */ + + //These values below describe cases when processing could be possible but with different image format or parameters + NVIMGCODEC_PROCESSING_STATUS_COLOR_SPEC_UNSUPPORTED = 0x5, /**< Color specification unsupported. */ + NVIMGCODEC_PROCESSING_STATUS_ORIENTATION_UNSUPPORTED = 0x9, /**< Apply orientation was enabled but it is unsupported. */ + NVIMGCODEC_PROCESSING_STATUS_ROI_UNSUPPORTED = 0x11, /**< Decoding region of interest is unsupported. */ + NVIMGCODEC_PROCESSING_STATUS_SAMPLING_UNSUPPORTED = 0x21, /**< Selected unsupported chroma subsampling . */ + NVIMGCODEC_PROCESSING_STATUS_SAMPLE_TYPE_UNSUPPORTED = 0x41, /**< Selected unsupported sample type. */ + NVIMGCODEC_PROCESSING_STATUS_SAMPLE_FORMAT_UNSUPPORTED = 0x81, /**< Selected unsupported sample format. */ + NVIMGCODEC_PROCESSING_STATUS_NUM_IMAGES_UNSUPPORTED = 0x101, /**< Unsupported number of images to decode/encode. */ + NVIMGCODEC_PROCESSING_STATUS_NUM_PLANES_UNSUPPORTED = 0x201, /**< Unsupported number of planes to decode/encode. */ + NVIMGCODEC_PROCESSING_STATUS_NUM_CHANNELS_UNSUPPORTED = 0x401, /**< Unsupported number of channels to decode/encode. */ + NVIMGCODEC_PROCESSING_STATUS_QUALITY_TYPE_UNSUPPORTED = 0x801, /**< Unsupported quality type to encode. */ + NVIMGCODEC_PROCESSING_STATUS_QUALITY_VALUE_UNSUPPORTED = 0x1001, /**< Unsupported quality value to encode. */ + + NVIMGCODEC_PROCESSING_STATUS_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecProcessingStatus; + + /** + * @brief Processing status type which combine processing status bitmasks + */ + typedef uint32_t nvimgcodecProcessingStatus_t; + + /** + * @brief Decode parameters + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + int apply_exif_orientation; /**< Apply exif orientation if available. Valid values 0 or 1. */ + + } nvimgcodecDecodeParams_t; + + /** + * @brief Supported quality types (algorithms), which determines how `quality_value` is interpreted. + */ + typedef enum + { + NVIMGCODEC_QUALITY_TYPE_DEFAULT = 0, /**< Each plugin decides what is best quality setting to use. `quality_value` is ignored.*/ + NVIMGCODEC_QUALITY_TYPE_LOSSLESS = 1, /**< Image encoding is reversible and keeps original image quality. `quality_value` is ignored except for the CUDA tiff encoder backend, + for which `quality_value=0` means no compression, and `quality_value=1` means LZW compression. */ + NVIMGCODEC_QUALITY_TYPE_QUALITY = 2, /**< `quality_value` is interpreted as JPEG-like quality in range from 1 (worst) to 100 (best). */ + NVIMGCODEC_QUALITY_TYPE_QUANTIZATION_STEP = 3, /**< `quality_value` is interpreted as quantization step (by how much pixel data will be divided). + The higher the value, the worse quality image is produced.*/ + NVIMGCODEC_QUALITY_TYPE_PSNR = 4, /**< `quality_value` is interpreted as desired Peak Signal-to-Noise Ratio (PSNR) target for the encoded image. + The higher the value, the better quality image is produced. Value should be positive. */ + NVIMGCODEC_QUALITY_TYPE_SIZE_RATIO = 5, /**< `quality_value` is interpreted as desired encoded image size ratio compared to original size, should be floating in range (0.0, 1.0). + E.g. value 0.1 means target size of 10% of original image. */ + NVIMGCODEC_QUALITY_TYPE_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecQualityType_t; + + /** + * @brief Encode parameters + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + nvimgcodecQualityType_t quality_type; /**< Quality type (algorithm) that will be used to encode image. */ + float quality_value; /**< Specifies how good encoded image should look like. + Refer to the {@link nvimgcodecQualityType_t nvimgcodecQualityType_t} for the allowed values for each quality type. */ + + + } nvimgcodecEncodeParams_t; + + /** + * @brief Progression orders defined in the JPEG2000 standard. + */ + typedef enum + { + NVIMGCODEC_JPEG2K_PROG_ORDER_LRCP = 0, //**< Layer-Resolution-Component-Position progression order. */ + NVIMGCODEC_JPEG2K_PROG_ORDER_RLCP = 1, //**< Resolution-Layer-Component-Position progression order. */ + NVIMGCODEC_JPEG2K_PROG_ORDER_RPCL = 2, //**< Resolution-Position-Component-Layer progression order. */ + NVIMGCODEC_JPEG2K_PROG_ORDER_PCRL = 3, //**< Position-Component-Resolution-Layer progression order. */ + NVIMGCODEC_JPEG2K_PROG_ORDER_CPRL = 4, //**< Component-Position-Resolution-Layer progression order. */ + NVIMGCODEC_JPEG2K_PROG_ORDER_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecJpeg2kProgOrder_t; + + /** + * @brief JPEG2000 code stream type + */ + typedef enum + { + NVIMGCODEC_JPEG2K_STREAM_J2K = 0, /**< Corresponds to the JPEG2000 code stream.*/ + NVIMGCODEC_JPEG2K_STREAM_JP2 = 1, /**< Corresponds to the .jp2 container.*/ + NVIMGCODEC_JPEG2K_STREAM_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecJpeg2kBitstreamType_t; + + /** + * @brief JPEG2000 Encode parameters + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + nvimgcodecJpeg2kBitstreamType_t stream_type; /**< JPEG2000 code stream type. */ + nvimgcodecJpeg2kProgOrder_t prog_order; /**< JPEG2000 progression order. */ + uint32_t num_resolutions; /**< Number of resolutions. */ + uint32_t code_block_w; /**< Code block width. Allowed values 32, 64 */ + uint32_t code_block_h; /**< Code block height. Allowed values 32, 64 */ + int ht; /**< Sets whether or not to use High-Throughput encoding. Valid values 0 or 1. */ + } nvimgcodecJpeg2kEncodeParams_t; + + /** + * @brief JPEG Encode parameters + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + /** + * Sets whether or not to use optimized Huffman. Valid values 0 or 1. + * + * @note Using optimized Huffman produces smaller JPEG bitstream sizes with the same quality, but with slower performance. + */ + int optimized_huffman; + } nvimgcodecJpegEncodeParams_t; + + + /** + * @brief Specifies the kind of metadata contained in an image + */ + typedef enum + { + NVIMGCODEC_METADATA_KIND_UNKNOWN = 0x0, /**< Unknown metadata kind */ + NVIMGCODEC_METADATA_KIND_EXIF, /**< EXIF metadata (reserved for future use)*/ + NVIMGCODEC_METADATA_KIND_GEO, /**< Geographic metadata */ + NVIMGCODEC_METADATA_KIND_MED_APERIO, /**< Medical metadata - Aperio format */ + NVIMGCODEC_METADATA_KIND_MED_PHILIPS, /**< Medical metadata - Philips format */ + + NVIMGCODEC_METADATA_KIND_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecMetadataKind_t; + + /** + * @brief Specifies the format of metadata contained in an image + */ + typedef enum + { + NVIMGCODEC_METADATA_FORMAT_UNKNOWN = 0x0, /**< Unknown metadata format */ + NVIMGCODEC_METADATA_FORMAT_RAW, /**< Raw binary metadata format */ + NVIMGCODEC_METADATA_FORMAT_JSON, /**< JSON metadata format */ + NVIMGCODEC_METADATA_FORMAT_XML, /**< XML metadata format */ + + NVIMGCODEC_METADATA_FORMAT_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecMetadataFormat_t; + + + /** + * @brief Defines metadata information for an image. + * + * This structure contains information about image metadata, such as EXIF. + * The metadata is stored in a buffer with a specified format and kind. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + nvimgcodecMetadataKind_t kind; /**< The kind of metadata (e.g. EXIF) */ + nvimgcodecMetadataFormat_t format; /**< The format of the metadata buffer */ + + size_t buffer_size; /**< Size of the metadata buffer in bytes */ + void* buffer; /**< Pointer to the metadata buffer */ + + } nvimgcodecMetadata_t; + + /** + * @brief Bitmask specifying which severities of events cause a debug messenger callback + */ + typedef enum + { + NVIMGCODEC_DEBUG_MESSAGE_SEVERITY_NONE = 0x00000000, + NVIMGCODEC_DEBUG_MESSAGE_SEVERITY_TRACE = 0x00000001, /**< Diagnostic message useful for developers */ + NVIMGCODEC_DEBUG_MESSAGE_SEVERITY_DEBUG = 0x00000010, /**< Diagnostic message useful for developers */ + NVIMGCODEC_DEBUG_MESSAGE_SEVERITY_INFO = 0x00000100, /**< Informational message like the creation of a resource */ + NVIMGCODEC_DEBUG_MESSAGE_SEVERITY_WARNING = 0x00001000, /**< Message about behavior that is not necessarily an error, + but very likely a bug in your application */ + NVIMGCODEC_DEBUG_MESSAGE_SEVERITY_ERROR = 0x00010000, /**< Message about behavior that is invalid and may cause + improper execution or result of operation (e.g. can't open file) + but not application */ + NVIMGCODEC_DEBUG_MESSAGE_SEVERITY_FATAL = 0x00100000, /**< Message about behavior that is invalid and may cause crashes + and forcing to shutdown application */ + NVIMGCODEC_DEBUG_MESSAGE_SEVERITY_ALL = 0x0FFFFFFF, /**< Used in case filtering out by message severity */ + NVIMGCODEC_DEBUG_MESSAGE_SEVERITY_DEFAULT = + NVIMGCODEC_DEBUG_MESSAGE_SEVERITY_WARNING | NVIMGCODEC_DEBUG_MESSAGE_SEVERITY_ERROR | NVIMGCODEC_DEBUG_MESSAGE_SEVERITY_FATAL, + NVIMGCODEC_DEBUG_MESSAGE_SEVERITY_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecDebugMessageSeverity_t; + + /** + * @brief Bitmask specifying which category of events cause a debug messenger callback + */ + typedef enum + { + NVIMGCODEC_DEBUG_MESSAGE_CATEGORY_NONE = 0x00000000, + NVIMGCODEC_DEBUG_MESSAGE_CATEGORY_GENERAL = + 0x00000001, /**< Some event has happened that is unrelated to the specification or performance */ + NVIMGCODEC_DEBUG_MESSAGE_CATEGORY_VALIDATION = 0x00000010, /**< Something has happened that indicates a possible mistake */ + NVIMGCODEC_DEBUG_MESSAGE_CATEGORY_PERFORMANCE = 0x00000100, /**< Potential non-optimal use */ + NVIMGCODEC_DEBUG_MESSAGE_CATEGORY_ALL = 0x0FFFFFFF, /**< Used in case filtering out by message category */ + NVIMGCODEC_DEBUG_MESSAGE_CATEGORY_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecDebugMessageCategory_t; + + /** + * @brief Describing debug message passed to debug callback function + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + const char* message; /**< Null-terminated string detailing the trigger conditions */ + uint32_t internal_status_id; /**< It is internal codec status id */ + const char* codec; /**< Codec name if codec is rising message or NULL otherwise (e.g framework) */ + const char* codec_id; /**< Codec id if codec is rising message or NULL otherwise */ + uint32_t codec_version; /**< Codec version if codec is rising message or 0 otherwise */ + } nvimgcodecDebugMessageData_t; + + /** + * @brief Debug callback function type. + * + * @param message_severity [in] Message severity + * @param message_category [in] Message category + * @param callback_data [in] Debug message data + * @param user_data [in] Pointer that was specified during the setup of the callback + * @returns 1 if message should not be passed further to other callbacks and 0 otherwise + */ + typedef int (*nvimgcodecDebugCallback_t)(const nvimgcodecDebugMessageSeverity_t message_severity, + const nvimgcodecDebugMessageCategory_t message_category, const nvimgcodecDebugMessageData_t* callback_data, void* user_data); + + /** + * @brief Debug messenger description. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + uint32_t message_severity; /**< Bitmask of message severity to listen for e.g. error or warning. */ + uint32_t message_category; /**< Bitmask of message category to listen for e.g. general or performance related. */ + nvimgcodecDebugCallback_t user_callback; /**< Debug callback function */ + void* user_data; /**< Pointer to user data which will be passed back to debug callback function. */ + } nvimgcodecDebugMessengerDesc_t; + + /** + * @brief Executor description. + * + * Codec plugins can use executor available via execution parameters to schedule execution of asynchronous task. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + const void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + void* instance; /**< Executor instance pointer which will be passed back in functions */ + + /** + * @brief Schedule execution of asynchronous task. + * + * @param instance [in] Pointer to nvimgcodecExecutorDesc_t instance. + * @param device_id [in] Device id on which task will be executed. + * @param sample_idx [in] Index of batch sample to process task on; It will be passed back as an argument in task function. + * @param task_context [in] Pointer to task context which will be passed back as an argument in task function. + * @param task [in] Pointer to task function to schedule. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*schedule)(void* instance, int device_id, int sample_idx, void* task_context, + void (*task)(int thread_id, int sample_idx, void* task_context)); + + /** + * @brief Starts the execution of all the queued work + */ + nvimgcodecStatus_t (*run)(void* instance, int device_id); + + /** + * @brief Blocks until all work issued earlier is complete + * @remarks It must be called only after `run` + */ + nvimgcodecStatus_t (*wait)(void* instance, int device_id); + + /** + * @brief Gets number of threads. + * + * @param instance [in] Pointer to nvimgcodecExecutorDesc_t instance. + * @return Number of threads in executor. + */ + int (*getNumThreads)(void* instance); + } nvimgcodecExecutorDesc_t; + + /** + * @brief Execution parameters + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + nvimgcodecDeviceAllocator_t* device_allocator; /**< Custom allocator for device memory */ + nvimgcodecPinnedAllocator_t* pinned_allocator; /**< Custom allocator for pinned memory */ + int max_num_cpu_threads; /**< Max number of CPU threads in default executor + (0 means default value equal to number of cpu cores) */ + nvimgcodecExecutorDesc_t* executor; /**< Points an executor. If NULL default executor will be used. + @note At plugin level API it always points to executor, either custom or default. */ + int device_id; /**< Device id to process decoding on. It can be also specified + using defines NVIMGCODEC_DEVICE_CURRENT or NVIMGCODEC_DEVICE_CPU_ONLY. */ + int pre_init; /**< If true, all relevant resources are initialized at creation of the instance */ + int skip_pre_sync; /**< If true, synchronization between user stream and per-thread streams is skipped before + decoding (we only synchronize after decoding). This can be useful when we are sure that + there are no actions that need synchronization (e.g. a CUDA async allocation on + the user stream) */ + int num_backends; /**< Number of allowed backends passed (if any) + in backends parameter. For 0, all backends are allowed.*/ + const nvimgcodecBackend_t* backends; /**< Points a nvimgcodecBackend_t array with defined allowed backends. + For nullptr, all backends are allowed. */ + } nvimgcodecExecutionParams_t; + + /** + * @brief Input/Output stream description. + * + * This abstracts source or sink for code stream bytes. + * + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + void* instance; /**< I/O stream description instance pointer which will be passed back in functions */ + uint64_t id; /**< Generated id that uniquely identifies the instance */ + + /** + * @brief Reads all requested data from the stream. + * + * @param instance [in] Pointer to nvimgcodecIoStreamDesc_t instance. + * @param output_size [in/out] Pointer to where to return number of read bytes. + * @param buf [in] Pointer to output buffer + * @param bytes [in] Number of bytes to read + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*read)(void* instance, size_t* output_size, void* buf, size_t bytes); + + /** + * @brief Writes all requested data to the stream. + * + * @param instance [in] Pointer to nvimgcodecIoStreamDesc_t instance. + * @param output_size [in/out] Pointer to where to return number of written bytes. + * @param buf [in] Pointer to input buffer + * @param bytes [in] Number of bytes to write + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*write)(void* instance, size_t* output_size, void* buf, size_t bytes); + + /** + * @brief Writes one character to the stream. + * + * @param instance [in] Pointer to nvimgcodecIoStreamDesc_t instance. + * @param output_size [in/out] Pointer to where to return number of written bytes. + * @param ch [in] Character to write. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*putc)(void* instance, size_t* output_size, unsigned char ch); + + /** + * @brief Skips `count` objects in the stream + * + * @param instance [in] Pointer to nvimgcodecIoStreamDesc_t instance. + * @param count [in] Number bytes to skip + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*skip)(void* instance, size_t count); + + /** + * @brief Moves the read pointer in the stream. + * + * @param instance [in] Pointer to nvimgcodecIoStreamDesc_t instance. + * @param offset [in] Offset to move. + * @param whence [in] Beginning - SEEK_SET, SEEK_CUR or SEEK_END. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*seek)(void* instance, ptrdiff_t offset, int whence); + + /** + * @brief Retrieves current position, in bytes from the beginning, in the stream. + * + * @param instance [in] Pointer to nvimgcodecIoStreamDesc_t instance. + * @param offset [in/out] Pointer where to return current position. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*tell)(void* instance, ptrdiff_t* offset); + + /** + * @brief Retrieves the length, in bytes, of the stream. + * + * @param instance [in] Pointer to nvimgcodecIoStreamDesc_t instance. + * @param size [in/out] Pointer where to return length of the stream. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*size)(void* instance, size_t* size); + + /** + * @brief Provides expected bytes which are going to be written. + * + * This function gives possibility to pre/re-allocate map function. + * + * @param instance [in] Pointer to nvimgcodecIoStreamDesc_t instance. + * @param bytes [in] Number of expected bytes which are going to be written. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*reserve)(void* instance, size_t bytes); + + /** + * @brief Requests all data to be written to the output. + * + * @param instance [in] Pointer to nvimgcodecIoStreamDesc_t instance. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*flush)(void* instance); + + /** + * @brief Maps data into host memory + * + * @param instance [in] Pointer to nvimgcodecIoStreamDesc_t instance. + * @param buffer [in/out] Points where to return pointer to mapped data. If data cannot be mapped, NULL will be returned. + * @param offset [in] Offset in the stream to begin mapping. + * @param size [in] Length of the mapping + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*map)(void* instance, void** buffer, size_t offset, size_t size); + + /** + * @brief Unmaps previously mapped data + * + * @param instance [in] Pointer to nvimgcodecIoStreamDesc_t instance. * + * @param buffer [in] Pointer to mapped data + * @param size [in] Length of data to unmap + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*unmap)(void* instance, void* buffer, size_t size); + } nvimgcodecIoStreamDesc_t; + + /** + * @brief Code stream description. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + void* instance; /**< Code stream description instance pointer which will be passed back in functions */ + + nvimgcodecIoStreamDesc_t* io_stream; /**< I/O stream which works as a source or sink of code stream bytes */ + + /** + * @brief Retrieves code stream information. + * + * @param instance [in] Pointer to nvimgcodecCodeStreamDesc_t instance. + * @param codestream_info [in/out] Points where to return code stream information. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*getCodeStreamInfo)(void* instance, nvimgcodecCodeStreamInfo_t* codestream_info); + + /** + * @brief Retrieves image information. + * + * @param instance [in] Pointer to nvimgcodecCodeStreamDesc_t instance. + * @param image_info [in/out] Points where to return image information. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*getImageInfo)(void* instance, nvimgcodecImageInfo_t* image_info); + } nvimgcodecCodeStreamDesc_t; + + /** + * @brief Image description. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + void* instance; /**< Image instance pointer which will be passed back in functions */ + + /** + * @brief Retrieves image info information. + * + * @param instance [in] Pointer to nvimgcodecImageDesc_t instance. + * @param image_info [in/out] Points where to return image information. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*getImageInfo)(void* instance, nvimgcodecImageInfo_t* image_info); + + /** + * @brief Informs that host side of processing of image is ready. + * + * @param instance [in] Pointer to nvimgcodecImageDesc_t instance. + * @param processing_status [in] Processing status. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*imageReady)(void* instance, nvimgcodecProcessingStatus_t processing_status); + } nvimgcodecImageDesc_t; + + /** + * @brief Parser description. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + void* + instance; /**:=". + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*create)( + void* instance, nvimgcodecEncoder_t* encoder, const nvimgcodecExecutionParams_t* exec_params, const char* options); + + /** + * Destroys encoder. + * + * @param encoder [in] Encoder handle to destroy. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*destroy)(nvimgcodecEncoder_t encoder); + + /** + * @brief Checks whether encoder can encode given image to code stream with provided parameters. + * + * @param encoder [in] Encoder handle. + * @param code_stream [in] Encoded stream. + * @param image [in] Image descriptor. + * @param params [in] Encode parameters which will be used with check. + * @param thread_idx [in] Index of the caller thread (can be from 0 to the executor's number of threads, or -1 for non-threaded execution) + * @return nvimgcodecProcessingStatus_t - Processing status + */ + nvimgcodecProcessingStatus_t (*canEncode)( + nvimgcodecEncoder_t encoder, + const nvimgcodecCodeStreamDesc_t* code_stream, + const nvimgcodecImageDesc_t* image, + const nvimgcodecEncodeParams_t* params, + int thread_idx); + + /** + * @brief Encode given image to code stream with provided parameters. + * + * @param encoder [in] Encoder handle. + * @param image [in] Image descriptor. + * @param code_stream [in] Encoded stream. + * @param params [in] Encode parameters. + * @param thread_idx [in] Index of the caller thread (can be from 0 to the executor's number of threads, or -1 for non-threaded execution) + * @return nvimgcodecProcessingStatus_t - Processing status + */ + nvimgcodecStatus_t (*encode)( + nvimgcodecEncoder_t encoder, + const nvimgcodecCodeStreamDesc_t* code_stream, + const nvimgcodecImageDesc_t* image, + const nvimgcodecEncodeParams_t* params, + int thread_idx); + } nvimgcodecEncoderDesc_t; + + /** + * Decoder description. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + void* instance; /**< Decoder description instance pointer which will be passed back in functions */ + const char* id; /**< Codec named identifier e.g. nvJpeg2000 */ + const char* codec; /**< Codec name e.g. jpeg2000 */ + nvimgcodecBackendKind_t backend_kind; /**< Backend kind */ + + /** + * @brief Creates decoder. + * + * @param instance [in] Pointer to nvimgcodecDecoderDesc_t instance. + * @param decoder [in/out] Points where to return handle to created decoder. + * @param exec_params [in] Points an execution parameters. + * @param options [in] String with optional, space separated, list of parameters for decoders, in format + * ":=". + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*create)( + void* instance, nvimgcodecDecoder_t* decoder, const nvimgcodecExecutionParams_t* exec_params, const char* options); + + /** + * @brief Destroys decoder. + * + * @param decoder [in] Decoder handle to destroy. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*destroy)(nvimgcodecDecoder_t decoder); + + /** + * @brief Retrieves metadata from code stream. + * + * @param decoder [in] Decoder handle to use for metadata retrieval. + * @param code_stream [in] Code stream to get metadata from. + * @param metadata [in/out] Points where to return metadata. + * @param metadata_count [in/out] Points where to return metadata count. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*getMetadata)( + nvimgcodecDecoder_t decoder, const nvimgcodecCodeStreamDesc_t* code_stream, nvimgcodecMetadata_t** metadata, int* metadata_count); + + /** + * @brief Checks whether decoder can decode given code stream to image with provided parameters. + * + * @param decoder [in] Decoder handle to use for check. + * @param info [in] Image information, including requested format. + * @param code_stream [in] Encoded stream. + * @param params [in] Decode parameters which will be used with check. + * @param thread_idx [in] Index of the caller thread (can be from 0 to the executor's number of threads, or -1 for non-threaded execution) + * @return nvimgcodecStatus_t + */ + nvimgcodecProcessingStatus_t (*canDecode)( + nvimgcodecDecoder_t decoder, + const nvimgcodecImageDesc_t* image, + const nvimgcodecCodeStreamDesc_t* code_stream, + const nvimgcodecDecodeParams_t* params, + int thread_idx); + + /** + * @brief Decode given code stream to image with provided parameters. + * + * @param decoder [in] Decoder handle to use for decoding. + * @param image [in/out] Image descriptor. + * @param code_stream [in] Encoded stream. + * @param params [in] Decode parameters. + * @param thread_idx [in] Index of the caller thread (can be from 0 to the executor's number of threads, or -1 for non-threaded execution) + * @return nvimgcodecStatus_t + */ + nvimgcodecStatus_t (*decode)( + nvimgcodecDecoder_t decoder, + const nvimgcodecImageDesc_t* image, + const nvimgcodecCodeStreamDesc_t* code_stream, + const nvimgcodecDecodeParams_t* params, + int thread_idx); + + /** + * @brief Decode given batch of code streams to images with provided parameters. + * @param decoder [in] Decoder handle to use for decoding. + * @param images [in/out] Pointer to array of pointers of batch size with image descriptors. + * @param code_streams [in] Pointer to array of batch size of pointers to encoded stream instances. + * @param batch_size [in] Number of items in batch to decode. + * @param params [in] Decode parameters. + * @param thread_idx [in] Index of the caller thread (can be from 0 to the executor's number of threads, or -1 for non-threaded execution) + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*decodeBatch)( + nvimgcodecDecoder_t decoder, + const nvimgcodecImageDesc_t** images, + const nvimgcodecCodeStreamDesc_t** code_streams, + int batch_size, + const nvimgcodecDecodeParams_t* params, + int thread_idx); + + /** + * @brief Retrieve preferred minibatch size. The library will try to use batch sizes that are multiples of this value. + * @param decoder [in] Decoder handle to use. + * @param batch_size [out] Preferred minibatch size. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*getMiniBatchSize)(nvimgcodecDecoder_t decoder, int* batch_size); + + } nvimgcodecDecoderDesc_t; + + /** + * @brief Defines decoder or encoder priority in codec. + * + * For each codec there can be more decoders and encoders registered. Every decoder and encoder is registered with defined priority. + * Decoding process starts with selecting highest priority decoder and checks whether it can decode particular code stream. In case + * decoding could not be handled by selected decoder, there is fallback mechanism which selects next in priority decoder. There can be + * more decoders registered with the same priority. In such case decoders with the same priority are selected in order of registration. + */ + typedef enum + { + NVIMGCODEC_PRIORITY_HIGHEST = 0, + NVIMGCODEC_PRIORITY_VERY_HIGH = 100, + NVIMGCODEC_PRIORITY_HIGH = 200, + NVIMGCODEC_PRIORITY_NORMAL = 300, + NVIMGCODEC_PRIORITY_LOW = 400, + NVIMGCODEC_PRIORITY_VERY_LOW = 500, + NVIMGCODEC_PRIORITY_LOWEST = 1000, + NVIMGCODEC_PRIORITY_ENUM_FORCE_INT = INT32_MAX + } nvimgcodecPriority_t; + + /** + * @brief Pointer to logging function. + * + * @param instance [in] Plugin framework instance pointer + * @param message_severity [in] Message severity e.g. error or warning. + * @param message_category [in] Message category e.g. general or performance related. + * @param data [in] Debug message data i.e. message string, status, codec etc. + */ + typedef nvimgcodecStatus_t (*nvimgcodecLogFunc_t)(void* instance, const nvimgcodecDebugMessageSeverity_t message_severity, + const nvimgcodecDebugMessageCategory_t message_category, const nvimgcodecDebugMessageData_t* data); + + /** + * @brief Plugin Framework + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + void* instance; /**< Plugin framework instance pointer which will be passed back in functions */ + const char* id; /**< Plugin framework named identifier e.g. nvImageCodec */ + uint32_t version; /**< Plugin framework version. */ + uint32_t cudart_version; /**< The version of CUDA Runtime with which plugin framework was built. */ + nvimgcodecLogFunc_t log; /**< Pointer to logging function. @see nvimgcodecLogFunc_t */ + + /** + * @brief Registers encoder plugin. + * + * @param instance [in] Pointer to nvimgcodecFrameworkDesc_t instance. + * @param desc [in] Pointer to encoder description. + * @param priority [in] Priority of encoder. @see nvimgcodecPriority_t + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*registerEncoder)(void* instance, const nvimgcodecEncoderDesc_t* desc, float priority); + + /** + * @brief Unregisters encoder plugin. + * + * @param instance [in] Pointer to nvimgcodecFrameworkDesc_t instance. + * @param desc [in] Pointer to encoder description to unregister. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*unregisterEncoder)(void* instance, const nvimgcodecEncoderDesc_t* desc); + + /** + * @brief Registers decoder plugin. + * + * @param instance [in] Pointer to nvimgcodecFrameworkDesc_t instance. + * @param desc [in] Pointer to decoder description. + * @param priority [in] Priority of decoder. @see nvimgcodecPriority_t + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*registerDecoder)(void* instance, const nvimgcodecDecoderDesc_t* desc, float priority); + + /** + * @brief Unregisters decoder plugin. + * + * @param instance [in] Pointer to nvimgcodecFrameworkDesc_t instance. + * @param desc [in] Pointer to decoder description to unregister. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*unregisterDecoder)(void* instance, const nvimgcodecDecoderDesc_t* desc); + + /** + * @brief Registers parser plugin. + * + * @param instance [in] Pointer to nvimgcodecFrameworkDesc_t instance. + * @param desc [in] Pointer to parser description. + * @param priority [in] Priority of decoder. @see nvimgcodecPriority_t + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*registerParser)(void* instance, const nvimgcodecParserDesc_t* desc, float priority); + + /** + * @brief Unregisters parser plugin. + * + * @param instance [in] Pointer to nvimgcodecFrameworkDesc_t instance. + * @param desc [in] Pointer to parser description to unregister. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*unregisterParser)(void* instance, const nvimgcodecParserDesc_t* desc); + + } nvimgcodecFrameworkDesc_t; + + /** + * @brief Extension description + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + void* instance; /**< Extension instance pointer which will be passed back in functions */ + const char* id; /**< Extension named identifier e.g. nvjpeg_ext */ + uint32_t version; /**< Extension version. Used when registering extension to check if there are newer.*/ + uint32_t ext_api_version; /**< The version of nvImageCodec extension API with which the extension was built. */ + + /** + * @brief Creates extension. + * + * @param instance [in] Pointer to nvimgcodecExtensionDesc_t instance. + * @param extension [in/out] Points where to return handle to created extension. + * @param framework [in] Pointer to framework description which can be use to register plugins. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*create)(void* instance, nvimgcodecExtension_t* extension, const nvimgcodecFrameworkDesc_t* framework); + + /** + * Destroys extension. + * + * @param extension [in] Extension handle to destroy. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + nvimgcodecStatus_t (*destroy)(nvimgcodecExtension_t extension); + } nvimgcodecExtensionDesc_t; + + /** + * @brief Extension module entry function type + * + * @param ext_desc [in/out] Points a nvimgcodecExtensionDesc_t handle in which the extension description is returned. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + typedef nvimgcodecStatus_t (*nvimgcodecExtensionModuleEntryFunc_t)(nvimgcodecExtensionDesc_t* ext_desc); + + /** + * @brief Extension shared module exported entry function. + * + * @param ext_desc [in/out] Points a nvimgcodecExtensionDesc_t handle in which the extension description is returned. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecExtensionModuleEntry(nvimgcodecExtensionDesc_t* ext_desc); + + /** + * @brief Provides nvImageCodec library properties. + * + * @param properties [in/out] Points a nvimgcodecProperties_t handle in which the properties are returned. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecGetProperties(nvimgcodecProperties_t* properties); + + /** + * @brief The nvImageCodec library instance create information structure. + */ + typedef struct + { + nvimgcodecStructureType_t struct_type; /**< The type of the structure. */ + size_t struct_size; /**< The size of the structure, in bytes. */ + void* struct_next; /**< Is NULL or a pointer to an extension structure type. */ + + int load_builtin_modules; /**< Load default modules. Valid values 0 or 1. */ + int load_extension_modules; /**< Discover and load extension modules on start. Valid values 0 or 1. */ + const char* extension_modules_path; /**< There may be several paths separated by ':' on Linux or ';' on Windows */ + int create_debug_messenger; /**< Create debug messenger during instance creation. Valid values 0 or 1. */ + /** Pointer to description to use when creating debug messenger. If NULL, default internal description will be used, + * together with following message_severity and message_category fields. */ + const nvimgcodecDebugMessengerDesc_t* debug_messenger_desc; + uint32_t message_severity; /**< Severity for default debug messenger */ + uint32_t message_category; /**< Message category for default debug messenger */ + } nvimgcodecInstanceCreateInfo_t; + + /** + * @brief Creates an instance of the library using the input arguments. + * + * @param instance [in/out] Points a nvimgcodecInstance_t handle in which the resulting instance is returned. + * @param create_info [in] Pointer to a nvimgcodecInstanceCreateInfo_t structure controlling creation of the instance. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecInstanceCreate( + nvimgcodecInstance_t* instance, const nvimgcodecInstanceCreateInfo_t* create_info); + + /** + * @brief Destroys the nvImageCodec library instance. + * + * @param instance [in] The library instance handle to destroy + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecInstanceDestroy(nvimgcodecInstance_t instance); + + /** + * @brief Creates library extension. + * + * @param instance [in] The library instance handle the extension will be used with. + * @param extension [in/out] Points a nvimgcodecExtension_t handle in which the resulting extension is returned. + * @param extension_desc [in] Pointer to a nvimgcodecExtensionDesc_t structure which defines extension to create. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecExtensionCreate( + nvimgcodecInstance_t instance, nvimgcodecExtension_t* extension, nvimgcodecExtensionDesc_t* extension_desc); + + /** + * @brief Destroys library extension. + * + * @param extension [in] The extension handle to destroy + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecExtensionDestroy(nvimgcodecExtension_t extension); + + /** + * @brief Creates a debug messenger. + * + * @param instance [in] The library instance handle the messenger will be used with. + * @param dbg_messenger [in/out] Points a nvimgcodecDebugMessenger_t handle in which the resulting debug messenger is returned. + * @param messenger_desc [in] Pointer to nvimgcodecDebugMessengerDesc_t structure which defines debug messenger to create. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecDebugMessengerCreate( + nvimgcodecInstance_t instance, nvimgcodecDebugMessenger_t* dbg_messenger, const nvimgcodecDebugMessengerDesc_t* messenger_desc); + + /** + * @brief Destroys debug messenger. + * + * @param dbg_messenger [in] The debug messenger handle to destroy + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecDebugMessengerDestroy(nvimgcodecDebugMessenger_t dbg_messenger); + + /** + * @brief Waits for processing items to be finished. + * + * @param future [in] Handle to future object created by decode or encode functions. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + * @warning Please note that when future is ready, it only means that all host work was done and it can be that + * some work was scheduled to be executed on device (depending on codec). To further synchronize work on + * device, there is cuda_stream field available in nvimgcodecImageInfo_t which can be used to specify + * cuda_stream to synchronize with. + * @see nvimgcodecImageInfo_t cuda_stream field. + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecFutureWaitForAll(nvimgcodecFuture_t future); + + /** + * @brief Destroys future. + * + * @param future [in] The future handle to destroy + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecFutureDestroy(nvimgcodecFuture_t future); + + /** + * @brief Receives processing statuses of batch items scheduled for decoding or encoding + * + * @param future [in] The future handle returned by decode or encode function for given batch items. + * @param processing_status [in/out] Points a nvimgcodecProcessingStatus_t handle in which the processing statuses is returned. + * @param size [in/out] Points a size_t in which the size of processing statuses returned. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecFutureGetProcessingStatus( + nvimgcodecFuture_t future, nvimgcodecProcessingStatus_t* processing_status, size_t* size); + + /** + * @brief Creates Image which wraps sample buffer together with format information. + * + * @param instance [in] The library instance handle the image will be used with. + * @param image [in/out] Points a nvimgcodecImage_t handle in which the resulting image is returned. + * @param image_info [in] Points a nvimgcodecImageInfo_t struct which describes sample buffer together with format. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecImageCreate( + nvimgcodecInstance_t instance, nvimgcodecImage_t* image, const nvimgcodecImageInfo_t* image_info); + + /** + * @brief Destroys image. + * + * @param image [in] The image handle to destroy + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecImageDestroy(nvimgcodecImage_t image); + + /** + * @brief Retrieves image information from provided opaque image object. + * + * @param image [in] The image handle to retrieve information from. + * @param image_info [in/out] Points a nvimgcodecImageInfo_t handle in which the image information is returned. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecImageGetImageInfo(nvimgcodecImage_t image, nvimgcodecImageInfo_t* image_info); + + /** + * @brief Creates code stream which wraps file source of compressed data + * + * @param instance [in] The library instance handle the code stream will be used with. + * @param code_stream [in/out] Points a nvimgcodecCodeStream_t handle in which the resulting code stream is returned. + * @param file_name [in] File name with compressed image data to wrap. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecCodeStreamCreateFromFile( + nvimgcodecInstance_t instance, nvimgcodecCodeStream_t* code_stream, const char* file_name); + + /** + * @brief Creates code stream which wraps host memory source of compressed data. + * + * @param instance [in] The library instance handle the code stream will be used with. + * @param code_stream [in/out] Points a nvimgcodecCodeStream_t handle in which the resulting code stream is returned. + * @param data [in] Pointer to buffer with compressed data. + * @param length [in] Length of compressed data in provided buffer. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecCodeStreamCreateFromHostMem( + nvimgcodecInstance_t instance, nvimgcodecCodeStream_t* code_stream, const unsigned char* data, size_t length); + + /** + * @brief Creates code stream which wraps file sink for compressed data with given format. + * + * @param instance [in] The library instance handle the code stream will be used with. + * @param code_stream [in/out] Points a nvimgcodecCodeStream_t handle in which the resulting code stream is returned. + * @param file_name [in] File name sink for compressed image data to wrap. + * @param image_info [in] Points a nvimgcodecImageInfo_t struct which describes output image format. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecCodeStreamCreateToFile( + nvimgcodecInstance_t instance, nvimgcodecCodeStream_t* code_stream, const char* file_name, const nvimgcodecImageInfo_t* image_info); + + /** + * @brief Function type to resize and provide host buffer. + * + * @param ctx [in] Pointer to context provided together with function. + * @param req_size [in] Requested size of buffer. + * @return Pointer to requested buffer. + * + * @note This function can be called multiple times and requested size can be lower at the end so buffer can be shrunk. + */ + typedef unsigned char* (*nvimgcodecResizeBufferFunc_t)(void* ctx, size_t req_size); + + /** + * @brief Creates code stream which wraps host memory sink for compressed data with given format. + * + * @param instance [in] The library instance handle the code stream will be used with. + * @param code_stream [in/out] Points a nvimgcodecCodeStream_t handle in which the resulting code stream is returned. + * @param ctx [in] Pointer to user defined context with which get buffer function will be called back. + * @param resize_buffer_func [in] Points a nvimgcodecResizeBufferFunc_t function handle which will be used to resize and providing host output buffer. + * @param image_info [in] Points a nvimgcodecImageInfo_t struct which describes output image format. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecCodeStreamCreateToHostMem(nvimgcodecInstance_t instance, nvimgcodecCodeStream_t* code_stream, + void* ctx, nvimgcodecResizeBufferFunc_t resize_buffer_func, const nvimgcodecImageInfo_t* image_info); + + /** + * @brief Destroys code stream. + * + * @param code_stream [in] The code stream handle to destroy + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecCodeStreamDestroy(nvimgcodecCodeStream_t code_stream); + + /** + * @brief Retrieves information about the specified code stream. + * + * @param code_stream [in] The code stream handle from which information is to be retrieved. + * @param codestream_info [in/out] Points to a nvimgcodecCodeStreamInfo_t handle where the code stream information will be stored. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecCodeStreamGetCodeStreamInfo(nvimgcodecCodeStream_t code_stream, nvimgcodecCodeStreamInfo_t* codestream_info); + + /** + * @brief Creates a sub-code stream from the specified code stream view. + * + * @param code_stream [in] The code stream handle from which the sub-code stream is to be created. + * @param sub_code_stream [in/out] Points to a nvimgcodecCodeStream_t handle in which the resulting sub-code stream is returned. + * @param code_stream_view [in] Points to a nvimgcodecCodeStreamView_t struct which describes the view of the code stream to be used for the sub-code stream. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecCodeStreamGetSubCodeStream(nvimgcodecCodeStream_t code_stream, nvimgcodecCodeStream_t* sub_code_stream, + const nvimgcodecCodeStreamView_t* code_stream_view); + + /** + * @brief Retrieves compressed image information from code stream. + * + * @param code_stream [in] The code stream handle from which information is to be retrieved. + * @param image_info [in/out] Points to a nvimgcodecImageInfo_t handle where the image information will be stored. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecCodeStreamGetImageInfo( + nvimgcodecCodeStream_t code_stream, nvimgcodecImageInfo_t* image_info); + + /** + * @brief Creates generic image decoder. + * + * @param instance [in] The library instance handle the decoder will be used with. + * @param decoder [in/out] Points a nvimgcodecDecoder_t handle in which the decoder is returned. + * @param exec_params [in] Points an execution parameters. + * @param options [in] String with optional space separated list of parameters for specific decoders in format + * ":=". For example "nvjpeg:fancy_upsampling=1" + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecDecoderCreate( + nvimgcodecInstance_t instance, nvimgcodecDecoder_t* decoder, const nvimgcodecExecutionParams_t* exec_params, const char* options); + + /** + * @brief Destroys decoder. + * + * @param decoder [in] The decoder handle to destroy + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecDecoderDestroy(nvimgcodecDecoder_t decoder); + + /** + * @brief Retrieves metadata from code stream. + * + * @param decoder [in] The decoder handle from which metadata is to be retrieved. + * @param code_stream [in] Pointer to input nvimgcodecCodeStream_t to get metadata from. + * @param metadata [in/out] Points to a nvimgcodecMetadata_t handle where the metadata will be stored. When set to nullptr (first call), only metadata_count will be returned. + * @param metadata_count [in/out] Points to an int handle where the metadata count will be stored. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecDecoderGetMetadata(nvimgcodecDecoder_t decoder, nvimgcodecCodeStream_t code_stream, nvimgcodecMetadata_t** metadata, int* metadata_count); + + /** + * @brief Checks if decoder can decode provided code stream to given output images with specified parameters. + * + * @param decoder [in] The decoder handle to use for checks. + * @param streams [in] Pointer to input nvimgcodecCodeStream_t array to check decoding with. + * @param images [in] Pointer to output nvimgcodecImage_t array to check decoding with. + * @param batch_size [in] Batch size of provided code streams and images. + * @param params [in] Pointer to nvimgcodecDecodeParams_t struct to check decoding with. + * @param processing_status [in/out] Points a nvimgcodecProcessingStatus_t handle in which the processing statuses is returned. + * @param force_format [in] Valid values 0 or 1. If 1 value, and high priority codec does not support provided format it will + * fallback to lower priority codec for further checks. For 0 value, when high priority codec does not + * support provided format or parameters but it can process input in general, it will stop check and + * return processing status with flags which shows what format or parameters need to be changed to + * avoid fallback to lower priority codec. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecDecoderCanDecode(nvimgcodecDecoder_t decoder, const nvimgcodecCodeStream_t* streams, + const nvimgcodecImage_t* images, int batch_size, const nvimgcodecDecodeParams_t* params, + nvimgcodecProcessingStatus_t* processing_status, int force_format); + + /** + * @brief Decode batch of provided code streams to given output images with specified parameters. + * + * @param decoder [in] The decoder handle to use for decoding. + * @param streams [in] Pointer to input nvimgcodecCodeStream_t array to decode. + * @param images [in] Pointer to output nvimgcodecImage_t array to decode to. + * @param batch_size [in] Batch size of provided code streams and images. + * @param params [in] Pointer to nvimgcodecDecodeParams_t struct to decode with. + * @param future [in/out] Points a nvimgcodecFuture_t handle in which the future is returned. + * The future object can be used to waiting and getting processing statuses. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + * + * @see nvimgcodecFutureGetProcessingStatus + * @see nvimgcodecFutureWaitForAll + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecDecoderDecode(nvimgcodecDecoder_t decoder, const nvimgcodecCodeStream_t* streams, + const nvimgcodecImage_t* images, int batch_size, const nvimgcodecDecodeParams_t* params, nvimgcodecFuture_t* future); + + /** + * @brief Creates generic image encoder. + * + * @param instance [in] The library instance handle the encoder will be used with. + * @param encoder [in/out] Points a nvimgcodecEncoder_t handle in which the decoder is returned. + * @param exec_params [in] Points an execution parameters. + * @param options [in] String with optional, space separated, list of parameters for specific encoders, in format + * ":=." + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecEncoderCreate( + nvimgcodecInstance_t instance, nvimgcodecEncoder_t* encoder, const nvimgcodecExecutionParams_t* exec_params, const char* options); + + /** + * @brief Destroys encoder. + * + * @param encoder [in] The encoder handle to destroy + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecEncoderDestroy(nvimgcodecEncoder_t encoder); + + /** + * @brief Checks if encoder can encode provided images to given output code streams with specified parameters. + * + * @param encoder [in] The encoder handle to use for checks. + * @param images [in] Pointer to input nvimgcodecImage_t array to check encoding with. + * @param streams [in] Pointer to output nvimgcodecCodeStream_t array to check encoding with. + * @param batch_size [in] Batch size of provided code streams and images. + * @param params [in] Pointer to nvimgcodecEncodeParams_t struct to check decoding with. + * @param processing_status [in/out] Points a nvimgcodecProcessingStatus_t handle in which the processing statuses is returned. + * @param force_format [in] Valid values 0 or 1. If 1 value, and high priority codec does not support provided format it will + * fallback to lower priority codec for further checks. For 0 value, when high priority codec does not + * support provided format or parameters but it can process input in general, it will stop check and + * return processing status with flags which shows what format or parameters need to be changed to + * avoid fallback to lower priority codec. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecEncoderCanEncode(nvimgcodecEncoder_t encoder, const nvimgcodecImage_t* images, + const nvimgcodecCodeStream_t* streams, int batch_size, const nvimgcodecEncodeParams_t* params, + nvimgcodecProcessingStatus_t* processing_status, int force_format); + + /** + * @brief Encode batch of provided images to given output code streams with specified parameters. + * + * @param encoder [in] The encoder handle to use for encoding. + * @param images [in] Pointer to input nvimgcodecImage_t array to encode. + * @param streams [in] Pointer to output nvimgcodecCodeStream_t array to encode to. + * @param batch_size [in] Batch size of provided code streams and images. + * @param params [in] Pointer to nvimgcodecEncodeParams_t struct to encode with. + * @param future [in/out] Points a nvimgcodecFuture_t handle in which the future is returned. + * The future object can be used to waiting and getting processing statuses. + * @return nvimgcodecStatus_t - An error code as specified in {@link nvimgcodecStatus_t API Return Status Codes} + * + * @see nvimgcodecFutureGetProcessingStatus + * @see nvimgcodecFutureWaitForAll + */ + NVIMGCODECAPI nvimgcodecStatus_t nvimgcodecEncoderEncode(nvimgcodecEncoder_t encoder, const nvimgcodecImage_t* images, + const nvimgcodecCodeStream_t* streams, int batch_size, const nvimgcodecEncodeParams_t* params, nvimgcodecFuture_t* future); + +#if defined(__cplusplus) +} +#endif + +#endif diff --git a/cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec_stubs_generated.cc b/cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec_stubs_generated.cc new file mode 100644 index 000000000..146d15066 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec_stubs_generated.cc @@ -0,0 +1,260 @@ +#include + +void *NvimgcodecLoadSymbol(const char *name); + +#define LOAD_SYMBOL_FUNC Nvimgcodec##LoadSymbol + +#pragma GCC diagnostic ignored "-Wattributes" + + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecGetPropertiesNotFound(nvimgcodecProperties_t *) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecGetProperties(nvimgcodecProperties_t * properties) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecProperties_t *); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecGetProperties")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecGetProperties")) : + nvimgcodecGetPropertiesNotFound; + return func_ptr(properties); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecInstanceCreateNotFound(nvimgcodecInstance_t *, const nvimgcodecInstanceCreateInfo_t *) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecInstanceCreate(nvimgcodecInstance_t * instance, const nvimgcodecInstanceCreateInfo_t * create_info) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecInstance_t *, const nvimgcodecInstanceCreateInfo_t *); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecInstanceCreate")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecInstanceCreate")) : + nvimgcodecInstanceCreateNotFound; + return func_ptr(instance, create_info); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecInstanceDestroyNotFound(nvimgcodecInstance_t) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecInstanceDestroy(nvimgcodecInstance_t instance) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecInstance_t); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecInstanceDestroy")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecInstanceDestroy")) : + nvimgcodecInstanceDestroyNotFound; + return func_ptr(instance); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecExtensionCreateNotFound(nvimgcodecInstance_t, nvimgcodecExtension_t *, nvimgcodecExtensionDesc_t *) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecExtensionCreate(nvimgcodecInstance_t instance, nvimgcodecExtension_t * extension, nvimgcodecExtensionDesc_t * extension_desc) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecInstance_t, nvimgcodecExtension_t *, nvimgcodecExtensionDesc_t *); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecExtensionCreate")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecExtensionCreate")) : + nvimgcodecExtensionCreateNotFound; + return func_ptr(instance, extension, extension_desc); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecExtensionDestroyNotFound(nvimgcodecExtension_t) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecExtensionDestroy(nvimgcodecExtension_t extension) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecExtension_t); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecExtensionDestroy")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecExtensionDestroy")) : + nvimgcodecExtensionDestroyNotFound; + return func_ptr(extension); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecFutureWaitForAllNotFound(nvimgcodecFuture_t) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecFutureWaitForAll(nvimgcodecFuture_t future) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecFuture_t); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecFutureWaitForAll")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecFutureWaitForAll")) : + nvimgcodecFutureWaitForAllNotFound; + return func_ptr(future); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecFutureDestroyNotFound(nvimgcodecFuture_t) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecFutureDestroy(nvimgcodecFuture_t future) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecFuture_t); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecFutureDestroy")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecFutureDestroy")) : + nvimgcodecFutureDestroyNotFound; + return func_ptr(future); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecFutureGetProcessingStatusNotFound(nvimgcodecFuture_t, nvimgcodecProcessingStatus_t *, size_t *) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecFutureGetProcessingStatus(nvimgcodecFuture_t future, nvimgcodecProcessingStatus_t * processing_status, size_t * size) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecFuture_t, nvimgcodecProcessingStatus_t *, size_t *); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecFutureGetProcessingStatus")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecFutureGetProcessingStatus")) : + nvimgcodecFutureGetProcessingStatusNotFound; + return func_ptr(future, processing_status, size); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecImageCreateNotFound(nvimgcodecInstance_t, nvimgcodecImage_t *, const nvimgcodecImageInfo_t *) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecImageCreate(nvimgcodecInstance_t instance, nvimgcodecImage_t * image, const nvimgcodecImageInfo_t * image_info) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecInstance_t, nvimgcodecImage_t *, const nvimgcodecImageInfo_t *); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecImageCreate")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecImageCreate")) : + nvimgcodecImageCreateNotFound; + return func_ptr(instance, image, image_info); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecImageDestroyNotFound(nvimgcodecImage_t) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecImageDestroy(nvimgcodecImage_t image) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecImage_t); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecImageDestroy")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecImageDestroy")) : + nvimgcodecImageDestroyNotFound; + return func_ptr(image); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecCodeStreamCreateFromFileNotFound(nvimgcodecInstance_t, nvimgcodecCodeStream_t *, const char *) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecCodeStreamCreateFromFile(nvimgcodecInstance_t instance, nvimgcodecCodeStream_t * code_stream, const char * file_name) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecInstance_t, nvimgcodecCodeStream_t *, const char *); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecCodeStreamCreateFromFile")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecCodeStreamCreateFromFile")) : + nvimgcodecCodeStreamCreateFromFileNotFound; + return func_ptr(instance, code_stream, file_name); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecCodeStreamCreateFromHostMemNotFound(nvimgcodecInstance_t, nvimgcodecCodeStream_t *, const unsigned char *, size_t) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecCodeStreamCreateFromHostMem(nvimgcodecInstance_t instance, nvimgcodecCodeStream_t * code_stream, const unsigned char * data, size_t length) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecInstance_t, nvimgcodecCodeStream_t *, const unsigned char *, size_t); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecCodeStreamCreateFromHostMem")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecCodeStreamCreateFromHostMem")) : + nvimgcodecCodeStreamCreateFromHostMemNotFound; + return func_ptr(instance, code_stream, data, length); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecCodeStreamDestroyNotFound(nvimgcodecCodeStream_t) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecCodeStreamDestroy(nvimgcodecCodeStream_t code_stream) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecCodeStream_t); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecCodeStreamDestroy")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecCodeStreamDestroy")) : + nvimgcodecCodeStreamDestroyNotFound; + return func_ptr(code_stream); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecCodeStreamGetCodeStreamInfoNotFound(nvimgcodecCodeStream_t, nvimgcodecCodeStreamInfo_t *) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecCodeStreamGetCodeStreamInfo(nvimgcodecCodeStream_t code_stream, nvimgcodecCodeStreamInfo_t * codestream_info) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecCodeStream_t, nvimgcodecCodeStreamInfo_t *); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecCodeStreamGetCodeStreamInfo")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecCodeStreamGetCodeStreamInfo")) : + nvimgcodecCodeStreamGetCodeStreamInfoNotFound; + return func_ptr(code_stream, codestream_info); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecCodeStreamGetSubCodeStreamNotFound(nvimgcodecCodeStream_t, nvimgcodecCodeStream_t *, const nvimgcodecCodeStreamView_t *) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecCodeStreamGetSubCodeStream(nvimgcodecCodeStream_t code_stream, nvimgcodecCodeStream_t * sub_code_stream, const nvimgcodecCodeStreamView_t * code_stream_view) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecCodeStream_t, nvimgcodecCodeStream_t *, const nvimgcodecCodeStreamView_t *); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecCodeStreamGetSubCodeStream")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecCodeStreamGetSubCodeStream")) : + nvimgcodecCodeStreamGetSubCodeStreamNotFound; + return func_ptr(code_stream, sub_code_stream, code_stream_view); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecCodeStreamGetImageInfoNotFound(nvimgcodecCodeStream_t, nvimgcodecImageInfo_t *) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecCodeStreamGetImageInfo(nvimgcodecCodeStream_t code_stream, nvimgcodecImageInfo_t * image_info) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecCodeStream_t, nvimgcodecImageInfo_t *); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecCodeStreamGetImageInfo")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecCodeStreamGetImageInfo")) : + nvimgcodecCodeStreamGetImageInfoNotFound; + return func_ptr(code_stream, image_info); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecDecoderCreateNotFound(nvimgcodecInstance_t, nvimgcodecDecoder_t *, const nvimgcodecExecutionParams_t *, const char *) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecDecoderCreate(nvimgcodecInstance_t instance, nvimgcodecDecoder_t * decoder, const nvimgcodecExecutionParams_t * exec_params, const char * options) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecInstance_t, nvimgcodecDecoder_t *, const nvimgcodecExecutionParams_t *, const char *); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecDecoderCreate")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecDecoderCreate")) : + nvimgcodecDecoderCreateNotFound; + return func_ptr(instance, decoder, exec_params, options); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecDecoderDestroyNotFound(nvimgcodecDecoder_t) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecDecoderDestroy(nvimgcodecDecoder_t decoder) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecDecoder_t); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecDecoderDestroy")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecDecoderDestroy")) : + nvimgcodecDecoderDestroyNotFound; + return func_ptr(decoder); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecDecoderGetMetadataNotFound(nvimgcodecDecoder_t, nvimgcodecCodeStream_t, nvimgcodecMetadata_t **, int *) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecDecoderGetMetadata(nvimgcodecDecoder_t decoder, nvimgcodecCodeStream_t code_stream, nvimgcodecMetadata_t ** metadata, int * metadata_count) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecDecoder_t, nvimgcodecCodeStream_t, nvimgcodecMetadata_t **, int *); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecDecoderGetMetadata")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecDecoderGetMetadata")) : + nvimgcodecDecoderGetMetadataNotFound; + return func_ptr(decoder, code_stream, metadata, metadata_count); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecDecoderCanDecodeNotFound(nvimgcodecDecoder_t, const nvimgcodecCodeStream_t *, const nvimgcodecImage_t *, int, const nvimgcodecDecodeParams_t *, nvimgcodecProcessingStatus_t *, int) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecDecoderCanDecode(nvimgcodecDecoder_t decoder, const nvimgcodecCodeStream_t * streams, const nvimgcodecImage_t * images, int batch_size, const nvimgcodecDecodeParams_t * params, nvimgcodecProcessingStatus_t * processing_status, int force_format) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecDecoder_t, const nvimgcodecCodeStream_t *, const nvimgcodecImage_t *, int, const nvimgcodecDecodeParams_t *, nvimgcodecProcessingStatus_t *, int); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecDecoderCanDecode")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecDecoderCanDecode")) : + nvimgcodecDecoderCanDecodeNotFound; + return func_ptr(decoder, streams, images, batch_size, params, processing_status, force_format); +} + +nvimgcodecStatus_t NVIMGCODECAPI nvimgcodecDecoderDecodeNotFound(nvimgcodecDecoder_t, const nvimgcodecCodeStream_t *, const nvimgcodecImage_t *, int, const nvimgcodecDecodeParams_t *, nvimgcodecFuture_t *) { + return NVIMGCODEC_STATUS_IMPLEMENTATION_UNSUPPORTED; +} + +nvimgcodecStatus_t nvimgcodecDecoderDecode(nvimgcodecDecoder_t decoder, const nvimgcodecCodeStream_t * streams, const nvimgcodecImage_t * images, int batch_size, const nvimgcodecDecodeParams_t * params, nvimgcodecFuture_t * future) { + using FuncPtr = nvimgcodecStatus_t (NVIMGCODECAPI *)(nvimgcodecDecoder_t, const nvimgcodecCodeStream_t *, const nvimgcodecImage_t *, int, const nvimgcodecDecodeParams_t *, nvimgcodecFuture_t *); + static auto func_ptr = reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecDecoderDecode")) ? + reinterpret_cast(LOAD_SYMBOL_FUNC("nvimgcodecDecoderDecode")) : + nvimgcodecDecoderDecodeNotFound; + return func_ptr(decoder, streams, images, batch_size, params, future); +} diff --git a/cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec_version.h b/cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec_version.h new file mode 100644 index 000000000..bb3e2f35a --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec_version.h @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +// clang-format off +#ifndef NVIMGCODEC_VERSION_H__ +#define NVIMGCODEC_VERSION_H__ + +#define NVIMGCODEC_VER_MAJOR 0 +#define NVIMGCODEC_VER_MINOR 6 +#define NVIMGCODEC_VER_PATCH 0 +#define NVIMGCODEC_VER_BUILD + +#define MAKE_SEMANTIC_VERSION(major, minor, patch) ((major * 1000) + (minor * 100) + patch) +#define NVIMGCODEC_MAJOR_FROM_SEMVER(ver) (ver / 1000) +#define NVIMGCODEC_MINOR_FROM_SEMVER(ver) ((ver % 1000) / 100) +#define NVIMGCODEC_PATCH_FROM_SEMVER(ver) ((ver % 1000) % 100) +#define NVIMGCODEC_STREAM_VER(ver) \ + NVIMGCODEC_MAJOR_FROM_SEMVER(ver) << "." << NVIMGCODEC_MINOR_FROM_SEMVER(ver) << "." << NVIMGCODEC_PATCH_FROM_SEMVER(ver) + +#define NVIMGCODEC_VER MAKE_SEMANTIC_VERSION(NVIMGCODEC_VER_MAJOR, NVIMGCODEC_VER_MINOR, NVIMGCODEC_VER_PATCH) + +#endif // NVIMGCODEC_VERSION diff --git a/cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec_wrap.cc b/cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec_wrap.cc new file mode 100644 index 000000000..85b4e43e2 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/src/nvimgcodec_dynlink/nvimgcodec_wrap.cc @@ -0,0 +1,87 @@ +// Copyright (c) 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// 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 +#include +#include + +#define STR_IMPL_(x) #x +#define STR(x) STR_IMPL_(x) +#define FULL_VER_STR \ + STR(NVIMGCODEC_VER_MAJOR) \ + "." STR(NVIMGCODEC_VER_MINOR) "." STR(NVIMGCODEC_VER_PATCH) +#define MAJOR_VER_STR STR(NVIMGCODEC_VER_MAJOR) + +namespace { + +typedef void *NVIMGCODECDRIVER; + +const char nvimgcodecLibNameFullVer[] = "libnvimgcodec.so." FULL_VER_STR; +const char nvimgcodecLibNameMajorVer[] = "libnvimgcodec.so." MAJOR_VER_STR; +const char nvimgcodecLibName[] = "libnvimgcodec.so"; + +NVIMGCODECDRIVER loadNvimgcodecLibrary() { + static const char *paths[] = {nvimgcodecLibNameFullVer, + nvimgcodecLibNameMajorVer, + nvimgcodecLibName}; + NVIMGCODECDRIVER ret = nullptr; + std::string last_error; + for (const char *path : paths) { + ret = dlopen(path, RTLD_NOW); + if (ret) { + fprintf(stderr, "[nvimgcodec_dynlink] Successfully loaded: %s\n", path); + break; + } else { + const char* err = dlerror(); + fprintf(stderr, "[nvimgcodec_dynlink] Failed to load %s: %s\n", path, err ? err : "unknown error"); + if (err) last_error = err; + } + } + + if (!ret) { + std::string error_msg = "dlopen libnvimgcodec.so failed! Last error: " + last_error + + "\nPlease install nvimagecodec: See https://developer.nvidia.com/nvimgcodec-downloads."; + throw std::runtime_error(error_msg); + } + return ret; +} + +} // namespace + +void *NvimgcodecLoadSymbol(const char *name) { + static NVIMGCODECDRIVER nvimgcodecDrvLib = loadNvimgcodecLibrary(); + void *ret = nvimgcodecDrvLib ? dlsym(nvimgcodecDrvLib, name) : nullptr; + if (!ret) { + fprintf(stderr, "[nvimgcodec_dynlink] WARNING: Symbol '%s' not found\n", name); + } + return ret; +} + +bool nvimgcodecIsSymbolAvailable(const char *name) { + static std::mutex symbol_mutex; + static std::unordered_map symbol_map; + std::lock_guard lock(symbol_mutex); + auto it = symbol_map.find(name); + if (it == symbol_map.end()) { + auto *ptr = NvimgcodecLoadSymbol(name); + symbol_map.insert({name, ptr}); + return ptr != nullptr; + } + return it->second != nullptr; +} 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..3e536b5a4 --- /dev/null +++ b/cpp/plugins/cucim.kit.cuslide2/tests/test_read_rawtiff.cpp @@ -0,0 +1,390 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#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]); + } +} + +/** + * I/O Performance Benchmark Test for TIFF Tile Reading + * + * This test benchmarks different low-level I/O methods for reading TIFF tiles: + * - Regular POSIX I/O (standard pread) + * - O_DIRECT I/O (bypassing OS page cache) + * - Memory-mapped I/O (mmap) + * - GPUDirect Storage / cuFile (direct GPU reads, if available) + * + * For each I/O method, the test measures: + * 1. Reading the entire TIFF file as a single operation + * 2. Reading individual tiles using their offsets and byte counts + * + * The tile read order can be shuffled to test random vs. sequential access patterns. + * Results help optimize the I/O strategy for large whole-slide images. + */ +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/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/run b/run index 4030af759..3830d6f9f 100755 --- a/run +++ b/run @@ -560,6 +560,20 @@ 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 + # Detect platform architecture for correct CUDA include path + CUDA_ARCH=$(uname -m) + if [ "${CUDA_ARCH}" = "aarch64" ]; then + export CUDA_INCLUDE_PATH="${CONDA_PREFIX}/targets/sbsa-linux/include" + else + export CUDA_INCLUDE_PATH="${CONDA_PREFIX}/targets/x86_64-linux/include" + fi + 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..495f8fc7f --- /dev/null +++ b/scripts/test_aperio_svs.py @@ -0,0 +1,284 @@ +#!/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" + + # Read version from VERSION file + version_file = repo_root / "VERSION" + if version_file.exists(): + version = version_file.read_text().strip() + else: + version = "25.12.00" # Fallback version + + # Create plugin configuration + config = { + "plugin": { + "names": [ + f"cucim.kit.cuslide2@{version}.so", # Dynamically use current version + ] + } + } + + 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..637ceffe1 --- /dev/null +++ b/scripts/test_philips_tiff.py @@ -0,0 +1,339 @@ +#!/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" + + # Read version from VERSION file + version_file = repo_root / "VERSION" + if version_file.exists(): + version = version_file.read_text().strip() + else: + version = "25.12.00" # Fallback version + + # Create plugin configuration + config = { + "plugin": { + "names": [ + f"cucim.kit.cuslide2@{version}.so", # Dynamically use current version + ] + } + } + + 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()) From e5248dfc98486c2090e3faaa599f6dc9caa789f6 Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 12:37:25 -0800 Subject: [PATCH 02/34] refactor: Remove unnecessary CUDA_INCLUDE_PATH from run script --- run | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/run b/run index 3830d6f9f..c68326ae0 100755 --- a/run +++ b/run @@ -561,18 +561,11 @@ test_python() { pushd "$TOP"/python/cucim # Set CUDA environment for CuPy JIT compilation + # CuPy automatically finds headers in ${CUDA_PATH}/targets/*/include 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 - # Detect platform architecture for correct CUDA include path - CUDA_ARCH=$(uname -m) - if [ "${CUDA_ARCH}" = "aarch64" ]; then - export CUDA_INCLUDE_PATH="${CONDA_PREFIX}/targets/sbsa-linux/include" - else - export CUDA_INCLUDE_PATH="${CONDA_PREFIX}/targets/x86_64-linux/include" - fi - echo "🔧 Set CUDA_HOME=${CUDA_HOME} CUDA_INCLUDE_PATH=${CUDA_INCLUDE_PATH}" + echo "🔧 Set CUDA_HOME=${CUDA_HOME}" fi run_command py.test --cache-clear -vv \ --cov=cucim \ From 2f2f1e009936325f37c061d0880524bccc0e4e0e Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 12:44:16 -0800 Subject: [PATCH 03/34] build: Add libnvimgcodec metapackage to capture run_exports --- conda/recipes/libcucim/meta.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/conda/recipes/libcucim/meta.yaml b/conda/recipes/libcucim/meta.yaml index 1a684d341..5feb3af08 100644 --- a/conda/recipes/libcucim/meta.yaml +++ b/conda/recipes/libcucim/meta.yaml @@ -72,6 +72,7 @@ requirements: - cuda-cudart - libnvjpeg - libnvimgcodec0 {{ nvimgcodec_version }} # nvImageCodec runtime library + - libnvimgcodec {{ nvimgcodec_version }} # nvImageCodec runtime library metapackage run_constrained: - {{ pin_compatible('openslide') }} - libnvimgcodec-dev {{ nvimgcodec_version }} # Optional: for development/debugging From 9c698d1cc3c334e085898b2e51abfc6d731eb0c2 Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 13:18:53 -0800 Subject: [PATCH 04/34] refactor: Use cucim.__version__ instead of reading VERSION file --- scripts/test_aperio_svs.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/scripts/test_aperio_svs.py b/scripts/test_aperio_svs.py index 495f8fc7f..5c923371f 100755 --- a/scripts/test_aperio_svs.py +++ b/scripts/test_aperio_svs.py @@ -15,6 +15,7 @@ def setup_environment(): """Setup cuCIM environment for cuslide2 plugin""" + import cucim # Get current build directory repo_root = Path(__file__).parent.parent @@ -25,12 +26,8 @@ def setup_environment(): if not plugin_lib.exists(): plugin_lib = repo_root / "install" / "lib" - # Read version from VERSION file - version_file = repo_root / "VERSION" - if version_file.exists(): - version = version_file.read_text().strip() - else: - version = "25.12.00" # Fallback version + # Use installed cucim version + version = cucim.__version__ # Create plugin configuration config = { From 184a11a83d9d5bed43570fe30f1beadd856f8890 Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 13:23:41 -0800 Subject: [PATCH 05/34] refactor: Use cucim.__version__ in test_philips_tiff.py --- scripts/test_philips_tiff.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/scripts/test_philips_tiff.py b/scripts/test_philips_tiff.py index 637ceffe1..502a40f6f 100755 --- a/scripts/test_philips_tiff.py +++ b/scripts/test_philips_tiff.py @@ -13,6 +13,7 @@ def setup_environment(): """Setup cuCIM environment for cuslide2 plugin""" + import cucim # Get current build directory repo_root = Path(__file__).parent.parent @@ -21,12 +22,8 @@ def setup_environment(): if not plugin_lib.exists(): plugin_lib = repo_root / "install/lib" - # Read version from VERSION file - version_file = repo_root / "VERSION" - if version_file.exists(): - version = version_file.read_text().strip() - else: - version = "25.12.00" # Fallback version + # Use installed cucim version + version = cucim.__version__ # Create plugin configuration config = { From 417c71555f203f7eb70dc10b9a238ccf366ee7de Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 13:42:07 -0800 Subject: [PATCH 06/34] build: Bump minimum CuPy version to 14.0.0 --- conda/environments/all_cuda-129_arch-aarch64.yaml | 2 +- conda/environments/all_cuda-129_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-130_arch-aarch64.yaml | 2 +- conda/environments/all_cuda-130_arch-x86_64.yaml | 2 +- conda/recipes/cucim/meta.yaml | 4 ++-- dependencies.yaml | 6 +++--- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index a4ddc65ce..b741ce78e 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -11,7 +11,7 @@ dependencies: - cuda-cudart-dev - cuda-nvcc - cuda-version=12.9 -- cupy>=13.6.0 +- cupy>=14.0.0 - cxx-compiler - gcc_linux-aarch64=14.* - imagecodecs>=2021.6.8 diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 1eb3eee59..3cf5cd520 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -11,7 +11,7 @@ dependencies: - cuda-cudart-dev - cuda-nvcc - cuda-version=12.9 -- cupy>=13.6.0 +- cupy>=14.0.0 - cxx-compiler - gcc_linux-64=14.* - imagecodecs>=2021.6.8 diff --git a/conda/environments/all_cuda-130_arch-aarch64.yaml b/conda/environments/all_cuda-130_arch-aarch64.yaml index e06fb7194..819641f8a 100644 --- a/conda/environments/all_cuda-130_arch-aarch64.yaml +++ b/conda/environments/all_cuda-130_arch-aarch64.yaml @@ -11,7 +11,7 @@ dependencies: - cuda-cudart-dev - cuda-nvcc - cuda-version=13.0 -- cupy>=13.6.0 +- cupy>=14.0.0 - cxx-compiler - gcc_linux-aarch64=14.* - imagecodecs>=2021.6.8 diff --git a/conda/environments/all_cuda-130_arch-x86_64.yaml b/conda/environments/all_cuda-130_arch-x86_64.yaml index 8b21f3781..56539319c 100644 --- a/conda/environments/all_cuda-130_arch-x86_64.yaml +++ b/conda/environments/all_cuda-130_arch-x86_64.yaml @@ -11,7 +11,7 @@ dependencies: - cuda-cudart-dev - cuda-nvcc - cuda-version=13.0 -- cupy>=13.6.0 +- cupy>=14.0.0 - cxx-compiler - gcc_linux-64=14.* - imagecodecs>=2021.6.8 diff --git a/conda/recipes/cucim/meta.yaml b/conda/recipes/cucim/meta.yaml index 52ad682cc..b4c596b8f 100644 --- a/conda/recipes/cucim/meta.yaml +++ b/conda/recipes/cucim/meta.yaml @@ -52,7 +52,7 @@ requirements: - click - cuda-version ={{ cuda_version }} - cuda-cudart-dev - - cupy >=13.6.0 + - cupy >=14.0.0 - libcucim ={{ version }} - python - pip @@ -65,7 +65,7 @@ requirements: - cuda-cudart - numpy >=1.23,<3.0a0 - click - - cupy >=13.6.0 + - cupy >=14.0.0 - lazy_loader >=0.1 - libcucim ={{ version }} - python diff --git a/dependencies.yaml b/dependencies.yaml index 9efe50fd6..211a18b3e 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -241,7 +241,7 @@ dependencies: - scipy>=1.11.2 - output_types: conda packages: - - &cupy_unsuffixed cupy>=13.6.0 + - &cupy_unsuffixed cupy>=14.0.0 - libnvimgcodec>=0.6.0,<0.7.0 - nvimgcodec>=0.6.0,<0.7.0 specific: @@ -250,12 +250,12 @@ dependencies: - matrix: cuda: "12.*" packages: - - cupy-cuda12x>=13.6.0 + - cupy-cuda12x>=14.0.0 - nvidia-nvimgcodec-cu12>=0.6.0,<0.7.0 # fallback to CUDA 13 versions if 'cuda' is '13.*' or not provided - matrix: packages: - - cupy-cuda13x>=13.6.0 + - cupy-cuda13x>=14.0.0 - nvidia-nvimgcodec-cu13>=0.6.0,<0.7.0 test_python: common: From c5503059bc4c22420068e92b8e5055f87a13151a Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 13:58:52 -0800 Subject: [PATCH 07/34] refactor: Remove AUTO_INSTALL_NVIMGCODEC feature --- cpp/cmake/deps/nvimgcodec.cmake | 121 +------------------------------- 1 file changed, 1 insertion(+), 120 deletions(-) diff --git a/cpp/cmake/deps/nvimgcodec.cmake b/cpp/cmake/deps/nvimgcodec.cmake index 08c8c5bc6..38e5689f9 100644 --- a/cpp/cmake/deps/nvimgcodec.cmake +++ b/cpp/cmake/deps/nvimgcodec.cmake @@ -6,124 +6,6 @@ # 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.6.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) @@ -224,9 +106,8 @@ if (NOT TARGET deps::nvimgcodec) 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 1 (conda): conda 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() From 785efe2a83156918c0ef4155c3f0dfb3407da2fb Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 14:25:30 -0800 Subject: [PATCH 08/34] build: Update pyproject.toml for CuPy 14.0.0 --- python/cucim/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cucim/pyproject.toml b/python/cucim/pyproject.toml index 7bf5db883..5b403f6b5 100644 --- a/python/cucim/pyproject.toml +++ b/python/cucim/pyproject.toml @@ -26,7 +26,7 @@ license = { text = "Apache 2.0" } requires-python = ">=3.10" dependencies = [ "click", - "cupy-cuda13x>=13.6.0", + "cupy-cuda13x>=14.0.0", "lazy-loader>=0.4", "numpy>=1.23.4,<3.0a0", "nvidia-nvimgcodec-cu13>=0.6.0,<0.7.0", From d85de65ea4e0e365d82265fdc8ebfc8a00ad6b35 Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 15:31:32 -0800 Subject: [PATCH 09/34] refactor: Use const string_views for spacing units --- .../cucim.kit.cuslide2/src/cuslide/cuslide.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.cpp b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.cpp index 60b0e3b4f..952201c29 100644 --- a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.cpp +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.cpp @@ -197,6 +197,10 @@ static bool CUCIM_ABI parser_parse(CuCIMFileHandle_ptr handle_ptr, cucim::io::fo } // Spacing units + static constexpr std::string_view empty_unit{}; + static constexpr std::string_view micrometer_unit{"micrometer"}; + static constexpr std::string_view color_unit{"color"}; + std::pmr::vector spacing_units(&resource); spacing_units.reserve(ndim); @@ -213,30 +217,30 @@ static bool CUCIM_ABI parser_parse(CuCIMFileHandle_ptr handle_ptr, cucim::io::fo spacing.emplace_back(x_resolution); spacing.emplace_back(1.0f); - spacing_units.emplace_back(std::string_view{ "" }); - spacing_units.emplace_back(std::string_view{ "" }); + spacing_units.emplace_back(empty_unit); + spacing_units.emplace_back(empty_unit); 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" }); + spacing_units.emplace_back(micrometer_unit); + spacing_units.emplace_back(micrometer_unit); 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" }); + spacing_units.emplace_back(micrometer_unit); + spacing_units.emplace_back(micrometer_unit); break; default: spacing.insert(spacing.end(), ndim, 1.0f); } - spacing_units.emplace_back(std::string_view{ "color" }); + spacing_units.emplace_back(color_unit); std::pmr::vector origin({ 0.0, 0.0, 0.0 }, &resource); // Direction cosines (size is always 3x3) From 5eb6a1d23b37ceca633a745c0233f8aa0f2ddc49 Mon Sep 17 00:00:00 2001 From: cdinea <123968711+cdinea@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:38:59 -0800 Subject: [PATCH 10/34] Update cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.cpp Co-authored-by: jakirkham --- .../src/cuslide/nvimgcodec/nvimgcodec_decoder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 97b65585b..ddbc5ce44 100644 --- a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.cpp +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.cpp @@ -39,7 +39,7 @@ struct CodeStreamDeleter { void operator()(nvimgcodecCodeStream_t stream) const { - if (stream) nvimgcodecCodeStreamDestroy(stream); + if (stream) { nvimgcodecCodeStreamDestroy(stream); } } }; using UniqueCodeStream = std::unique_ptr, CodeStreamDeleter>; From 76dd669c92a6f886c55c6d2cfeba28677986afb7 Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 15:48:44 -0800 Subject: [PATCH 11/34] refactor: Use reference to pointer for output_buffer parameter --- .../src/cuslide/nvimgcodec/nvimgcodec_decoder.cpp | 12 ++++++------ .../src/cuslide/nvimgcodec/nvimgcodec_decoder.h | 4 ++-- .../cucim.kit.cuslide2/src/cuslide/tiff/ifd.cpp | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) 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 index ddbc5ce44..90c6eb7fc 100644 --- a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.cpp +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.cpp @@ -49,7 +49,7 @@ struct ImageDeleter { void operator()(nvimgcodecImage_t image) const { - if (image) nvimgcodecImageDestroy(image); + if (image) { nvimgcodecImageDestroy(image); } } }; using UniqueImage = std::unique_ptr, ImageDeleter>; @@ -59,7 +59,7 @@ struct FutureDeleter { void operator()(nvimgcodecFuture_t future) const { - if (future) nvimgcodecFutureDestroy(future); + if (future) { nvimgcodecFutureDestroy(future); } } }; using UniqueFuture = std::unique_ptr, FutureDeleter>; @@ -147,7 +147,7 @@ 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, + uint8_t*& output_buffer, const cucim::io::Device& out_device) { if (!main_code_stream) @@ -414,12 +414,12 @@ bool decode_ifd_region_nvimgcodec(const IfdInfo& ifd_info, #endif // Return CPU buffer (decode_buffer GPU memory will be freed by RAII) - *output_buffer = cpu_buffer; + output_buffer = cpu_buffer; } else { // GPU output: release buffer ownership to caller (skip RAII cleanup) - *output_buffer = reinterpret_cast(decode_buffer.release()); + output_buffer = reinterpret_cast(decode_buffer.release()); } #ifdef DEBUG @@ -443,7 +443,7 @@ bool decode_ifd_region_nvimgcodec(const IfdInfo&, nvimgcodecCodeStream_t, uint32_t, uint32_t, uint32_t, uint32_t, - uint8_t**, + uint8_t*&, const cucim::io::Device&) { throw std::runtime_error("cuslide2 plugin requires nvImageCodec to be enabled at compile time"); 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 index be67fbbaf..496db821c 100644 --- a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.h +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/nvimgcodec/nvimgcodec_decoder.h @@ -39,7 +39,7 @@ typedef void* nvimgcodecCodeStream_t; * @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 output_buffer Reference to pointer to receive allocated buffer (caller must free) * @param out_device Output device ("cpu" or "cuda") * @return true if successful, false otherwise * @@ -49,7 +49,7 @@ 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, + uint8_t*& output_buffer, const cucim::io::Device& out_device); } // 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 index c76cbb1bf..6e0920ffd 100644 --- a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.cpp +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.cpp @@ -267,7 +267,7 @@ bool IFD::read([[maybe_unused]] const TIFF* tiff, ifd_info, tiff->nvimgcodec_parser_->get_main_code_stream(), sx, sy, w, h, - &output_buffer, + output_buffer, out_device); if (success) From 539f33f146416ddb7b1f07eda81331a2c20a35ee Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 16:24:41 -0800 Subject: [PATCH 12/34] test: Remove assertionn --- cpp/plugins/cucim.kit.cuslide2/tests/test_philips_tiff.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/tests/test_philips_tiff.cpp b/cpp/plugins/cucim.kit.cuslide2/tests/test_philips_tiff.cpp index 82fb5ddb7..b3cb8a9fc 100644 --- a/cpp/plugins/cucim.kit.cuslide2/tests/test_philips_tiff.cpp +++ b/cpp/plugins/cucim.kit.cuslide2/tests/test_philips_tiff.cpp @@ -54,6 +54,4 @@ TEST_CASE("Verify philips tiff file", "[test_philips_tiff.cpp]") tif->read(&metadata.desc(), &request, &image_data, nullptr /*out_metadata*/); tif->close(); - - REQUIRE(1 == 1); } From 214d6fb455b27d8125f391bdab9632fd8cce53af Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 16:31:52 -0800 Subject: [PATCH 13/34] style: Update copyright header to SPDX format in test_philips_tiff.cpp --- .../tests/test_philips_tiff.cpp | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/tests/test_philips_tiff.cpp b/cpp/plugins/cucim.kit.cuslide2/tests/test_philips_tiff.cpp index b3cb8a9fc..eb886bf3c 100644 --- a/cpp/plugins/cucim.kit.cuslide2/tests/test_philips_tiff.cpp +++ b/cpp/plugins/cucim.kit.cuslide2/tests/test_philips_tiff.cpp @@ -1,17 +1,6 @@ /* - * 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. + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 */ #include From e3f25b24c777d4ceed3ad44944eca10f1488ccce Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 17:03:12 -0800 Subject: [PATCH 14/34] build: Remove redundant libnvimgcodec0 dependency. --- conda/recipes/libcucim/meta.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/conda/recipes/libcucim/meta.yaml b/conda/recipes/libcucim/meta.yaml index 5feb3af08..51c9f3818 100644 --- a/conda/recipes/libcucim/meta.yaml +++ b/conda/recipes/libcucim/meta.yaml @@ -71,8 +71,7 @@ requirements: - libcufile - cuda-cudart - libnvjpeg - - libnvimgcodec0 {{ nvimgcodec_version }} # nvImageCodec runtime library - - libnvimgcodec {{ nvimgcodec_version }} # nvImageCodec runtime library metapackage + - libnvimgcodec {{ nvimgcodec_version }} # nvImageCodec metapackage (includes libnvimgcodec0 + run_exports) run_constrained: - {{ pin_compatible('openslide') }} - libnvimgcodec-dev {{ nvimgcodec_version }} # Optional: for development/debugging From 0285c3bd65327d19d274ab4b7de39633cb716292 Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 17:07:28 -0800 Subject: [PATCH 15/34] style: Add braces to single-line if statements in TIFF parser. --- .../nvimgcodec/nvimgcodec_tiff_parser.cpp | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) 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 index 6a95e2e49..b0c071e9e 100644 --- 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 @@ -517,10 +517,12 @@ void TiffFileParser::extract_ifd_metadata(IfdInfo& ifd_info) if (!manager.get_decoder() || !ifd_info.sub_code_stream) { - if (!manager.get_decoder()) + if (!manager.get_decoder()) { fmt::print(" ⚠️ Decoder not available\n"); - if (!ifd_info.sub_code_stream) + } + if (!ifd_info.sub_code_stream) { fmt::print(" ⚠️ No sub-code stream for this IFD\n"); + } return; // No decoder or stream available } @@ -624,8 +626,9 @@ void TiffFileParser::extract_ifd_metadata(IfdInfo& ifd_info) // Step 4: Move blobs into metadata_blobs map (no copy - just move!) for (int j = 0; j < metadata_count; ++j) { - if (!metadata_ptrs[j]) + if (!metadata_ptrs[j]) { continue; + } nvimgcodecMetadata_t* metadata = metadata_ptrs[j]; @@ -723,12 +726,14 @@ uint32_t TiffFileParser::get_nvimgcodec_version() const std::string TiffFileParser::get_tiff_tag(uint32_t ifd_index, const std::string& tag_name) const { - if (ifd_index >= ifd_infos_.size()) + 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()) + if (it != ifd_infos_[ifd_index].tiff_tags.end()) { return it->second; + } return ""; } @@ -787,8 +792,7 @@ void TiffFileParser::extract_tiff_tags(IfdInfo& ifd_info) // Store ImageDescription if available if (!ifd_info.image_description.empty()) { - if (ifd_info.tiff_tags.find("IMAGEDESCRIPTION") == ifd_info.tiff_tags.end()) - { + if (ifd_info.tiff_tags.find("IMAGEDESCRIPTION") == ifd_info.tiff_tags.end()) { ifd_info.tiff_tags["IMAGEDESCRIPTION"] = ifd_info.image_description; } } @@ -809,8 +813,9 @@ void TiffFileParser::extract_tiff_tags(IfdInfo& ifd_info) int TiffFileParser::get_subfile_type(uint32_t ifd_index) const { std::string subfile_str = get_tiff_tag(ifd_index, "SUBFILETYPE"); - if (subfile_str.empty()) + if (subfile_str.empty()) { return -1; + } try { return std::stoi(subfile_str); @@ -823,8 +828,9 @@ std::vector TiffFileParser::query_metadata_kinds(uint32_t ifd_index) const { std::vector kinds; - if (ifd_index >= ifd_infos_.size()) + 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) @@ -837,8 +843,9 @@ std::vector TiffFileParser::query_metadata_kinds(uint32_t ifd_index) const std::string TiffFileParser::get_detected_format() const { - if (ifd_infos_.empty()) + if (ifd_infos_.empty()) { return "Unknown"; + } // Check first IFD for vendor-specific metadata const auto& kinds = query_metadata_kinds(0); From 228864e82e84141240326d58b7cc10843cce5009 Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 17:33:19 -0800 Subject: [PATCH 16/34] refactor: Remove unused tile-based decoding methods --- .../src/cuslide/tiff/ifd.cpp | 840 ------------------ .../cucim.kit.cuslide2/src/cuslide/tiff/ifd.h | 20 - 2 files changed, 860 deletions(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.cpp b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.cpp index 6e0920ffd..fd61595c3 100644 --- a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.cpp +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.cpp @@ -528,846 +528,6 @@ 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 diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.h b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.h index d2d3b99a7..ec2e24b72 100644 --- a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.h +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/tiff/ifd.h @@ -38,26 +38,6 @@ class EXPORT_VISIBLE IFD : public std::enable_shared_from_this #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, From e9b22e035c2fac76a75ca8255abab9b1bdd4bade Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 17:33:47 -0800 Subject: [PATCH 17/34] Revert "build: Update pyproject.toml for CuPy 14.0.0" This reverts commit 785efe2a83156918c0ef4155c3f0dfb3407da2fb. --- python/cucim/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cucim/pyproject.toml b/python/cucim/pyproject.toml index 5b403f6b5..7bf5db883 100644 --- a/python/cucim/pyproject.toml +++ b/python/cucim/pyproject.toml @@ -26,7 +26,7 @@ license = { text = "Apache 2.0" } requires-python = ">=3.10" dependencies = [ "click", - "cupy-cuda13x>=14.0.0", + "cupy-cuda13x>=13.6.0", "lazy-loader>=0.4", "numpy>=1.23.4,<3.0a0", "nvidia-nvimgcodec-cu13>=0.6.0,<0.7.0", From ffdabfa19a1bf2a1fe83c601fe3811f9ebba6688 Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 17:33:53 -0800 Subject: [PATCH 18/34] Revert "build: Bump minimum CuPy version to 14.0.0" This reverts commit 417c71555f203f7eb70dc10b9a238ccf366ee7de. --- conda/environments/all_cuda-129_arch-aarch64.yaml | 2 +- conda/environments/all_cuda-129_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-130_arch-aarch64.yaml | 2 +- conda/environments/all_cuda-130_arch-x86_64.yaml | 2 +- conda/recipes/cucim/meta.yaml | 4 ++-- dependencies.yaml | 6 +++--- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index b741ce78e..a4ddc65ce 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -11,7 +11,7 @@ dependencies: - cuda-cudart-dev - cuda-nvcc - cuda-version=12.9 -- cupy>=14.0.0 +- cupy>=13.6.0 - cxx-compiler - gcc_linux-aarch64=14.* - imagecodecs>=2021.6.8 diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 3cf5cd520..1eb3eee59 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -11,7 +11,7 @@ dependencies: - cuda-cudart-dev - cuda-nvcc - cuda-version=12.9 -- cupy>=14.0.0 +- cupy>=13.6.0 - cxx-compiler - gcc_linux-64=14.* - imagecodecs>=2021.6.8 diff --git a/conda/environments/all_cuda-130_arch-aarch64.yaml b/conda/environments/all_cuda-130_arch-aarch64.yaml index 819641f8a..e06fb7194 100644 --- a/conda/environments/all_cuda-130_arch-aarch64.yaml +++ b/conda/environments/all_cuda-130_arch-aarch64.yaml @@ -11,7 +11,7 @@ dependencies: - cuda-cudart-dev - cuda-nvcc - cuda-version=13.0 -- cupy>=14.0.0 +- cupy>=13.6.0 - cxx-compiler - gcc_linux-aarch64=14.* - imagecodecs>=2021.6.8 diff --git a/conda/environments/all_cuda-130_arch-x86_64.yaml b/conda/environments/all_cuda-130_arch-x86_64.yaml index 56539319c..8b21f3781 100644 --- a/conda/environments/all_cuda-130_arch-x86_64.yaml +++ b/conda/environments/all_cuda-130_arch-x86_64.yaml @@ -11,7 +11,7 @@ dependencies: - cuda-cudart-dev - cuda-nvcc - cuda-version=13.0 -- cupy>=14.0.0 +- cupy>=13.6.0 - cxx-compiler - gcc_linux-64=14.* - imagecodecs>=2021.6.8 diff --git a/conda/recipes/cucim/meta.yaml b/conda/recipes/cucim/meta.yaml index b4c596b8f..52ad682cc 100644 --- a/conda/recipes/cucim/meta.yaml +++ b/conda/recipes/cucim/meta.yaml @@ -52,7 +52,7 @@ requirements: - click - cuda-version ={{ cuda_version }} - cuda-cudart-dev - - cupy >=14.0.0 + - cupy >=13.6.0 - libcucim ={{ version }} - python - pip @@ -65,7 +65,7 @@ requirements: - cuda-cudart - numpy >=1.23,<3.0a0 - click - - cupy >=14.0.0 + - cupy >=13.6.0 - lazy_loader >=0.1 - libcucim ={{ version }} - python diff --git a/dependencies.yaml b/dependencies.yaml index 211a18b3e..9efe50fd6 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -241,7 +241,7 @@ dependencies: - scipy>=1.11.2 - output_types: conda packages: - - &cupy_unsuffixed cupy>=14.0.0 + - &cupy_unsuffixed cupy>=13.6.0 - libnvimgcodec>=0.6.0,<0.7.0 - nvimgcodec>=0.6.0,<0.7.0 specific: @@ -250,12 +250,12 @@ dependencies: - matrix: cuda: "12.*" packages: - - cupy-cuda12x>=14.0.0 + - cupy-cuda12x>=13.6.0 - nvidia-nvimgcodec-cu12>=0.6.0,<0.7.0 # fallback to CUDA 13 versions if 'cuda' is '13.*' or not provided - matrix: packages: - - cupy-cuda13x>=14.0.0 + - cupy-cuda13x>=13.6.0 - nvidia-nvimgcodec-cu13>=0.6.0,<0.7.0 test_python: common: From d85c839c9b42a40a3c107c96048f1702aa845089 Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 17:34:10 -0800 Subject: [PATCH 19/34] Revert "refactor: Remove unnecessary CUDA_INCLUDE_PATH from run script" This reverts commit e5248dfc98486c2090e3faaa599f6dc9caa789f6. --- run | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/run b/run index c68326ae0..3830d6f9f 100755 --- a/run +++ b/run @@ -561,11 +561,18 @@ test_python() { pushd "$TOP"/python/cucim # Set CUDA environment for CuPy JIT compilation - # CuPy automatically finds headers in ${CUDA_PATH}/targets/*/include if [ -n "${CONDA_PREFIX}" ]; then export CUDA_PATH="${CONDA_PREFIX}" export CUDA_HOME="${CONDA_PREFIX}" - echo "🔧 Set CUDA_HOME=${CUDA_HOME}" + # CuPy NVRTC needs to find CUDA headers in targets subdirectory + # Detect platform architecture for correct CUDA include path + CUDA_ARCH=$(uname -m) + if [ "${CUDA_ARCH}" = "aarch64" ]; then + export CUDA_INCLUDE_PATH="${CONDA_PREFIX}/targets/sbsa-linux/include" + else + export CUDA_INCLUDE_PATH="${CONDA_PREFIX}/targets/x86_64-linux/include" + fi + echo "🔧 Set CUDA_HOME=${CUDA_HOME} CUDA_INCLUDE_PATH=${CUDA_INCLUDE_PATH}" fi run_command py.test --cache-clear -vv \ --cov=cucim \ From 3dc14fe0e0a811e1e5fcac91cd46b8a44cb203ac Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 18:23:15 -0800 Subject: [PATCH 20/34] chore: Remove local environment-specific entries from .gitignore --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index bc75731c5..40256b287 100644 --- a/.gitignore +++ b/.gitignore @@ -156,6 +156,4 @@ 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 From 60874feb876841a93ac5a3e2c5969795afebaa6b Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 18:47:15 -0800 Subject: [PATCH 21/34] test: Rename cuslide_tests to cuslide2_tests and enable tests --- cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt | 4 ++-- .../cucim.kit.cuslide2/tests/CMakeLists.txt | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt b/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt index 01c7794a3..4c44c8861 100644 --- a/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt +++ b/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt @@ -309,8 +309,8 @@ add_subdirectory(benchmarks) # 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 + cuslide2_tests + # cuslide_benchmarks # Disabled - not yet implemented ) # Add nvimgcodec_dynlink to install targets if dynamic loading is enabled diff --git a/cpp/plugins/cucim.kit.cuslide2/tests/CMakeLists.txt b/cpp/plugins/cucim.kit.cuslide2/tests/CMakeLists.txt index 542cc9a81..ee868fb03 100644 --- a/cpp/plugins/cucim.kit.cuslide2/tests/CMakeLists.txt +++ b/cpp/plugins/cucim.kit.cuslide2/tests/CMakeLists.txt @@ -9,25 +9,25 @@ include(CTest) enable_testing() ################################################################################ -# Add executable: cuslide_tests +# Add executable: cuslide2_tests ################################################################################ -add_executable(cuslide_tests +add_executable(cuslide2_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 +set_target_properties(cuslide2_tests PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO ) -target_compile_features(cuslide_tests PRIVATE ${CUCIM_REQUIRED_FEATURES}) +target_compile_features(cuslide2_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 +target_compile_options(cuslide2_tests PRIVATE $<$:-Werror -Wall -Wextra>) +target_compile_definitions(cuslide2_tests PUBLIC CUSLIDE_VERSION=${PROJECT_VERSION} CUSLIDE_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} @@ -35,7 +35,7 @@ target_compile_definitions(cuslide_tests CUSLIDE_VERSION_PATCH=${PROJECT_VERSION_PATCH} CUSLIDE_VERSION_BUILD=${PROJECT_VERSION_BUILD} ) -target_link_libraries(cuslide_tests +target_link_libraries(cuslide2_tests PRIVATE CUDA::cudart cucim::cucim @@ -47,7 +47,7 @@ target_link_libraries(cuslide_tests ) # Add headers in src -target_include_directories(cuslide_tests +target_include_directories(cuslide2_tests PUBLIC $ ) @@ -56,4 +56,4 @@ 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) +# catch_discover_tests(cuslide2_tests) From 73332f9af94cf2860babf21f2d4b9d03cadaa3b4 Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 19:15:39 -0800 Subject: [PATCH 22/34] Rename cuslide_benchmarks to cuslide2_benchmarks for consistency --- cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt | 30 ++++++++++++------- .../benchmarks/CMakeLists.txt | 14 ++++----- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt b/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt index 4c44c8861..906df175f 100644 --- a/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt +++ b/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt @@ -135,6 +135,10 @@ find_package(cucim CONFIG REQUIRED # Define compile options ################################################################################ +if(NOT BUILD_SHARED_LIBS) + set(BUILD_SHARED_LIBS ON) +endif() + ################################################################################ # Add library: cucim ################################################################################ @@ -155,9 +159,8 @@ message(STATUS "") message(STATUS "Build configured: Pure GPU-accelerated decoding with tests!") message(STATUS "=============================================================") - -# Use SHARED library (needed for tests to link against it, but still loaded via dlopen at runtime) -add_library(${CUCIM_PLUGIN_NAME} SHARED +# 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 @@ -170,6 +173,7 @@ add_library(${CUCIM_PLUGIN_NAME} SHARED # 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_manager.h ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.h) @@ -206,14 +210,17 @@ target_link_libraries(${CUCIM_PLUGIN_NAME} option(WITH_DYNAMIC_NVIMGCODEC "Use dynamic loading for nvImageCodec (dlopen at runtime)" ON) if(WITH_DYNAMIC_NVIMGCODEC) - + message(STATUS "============== Dynamic nvImageCodec Loading ==============") + set(nvimgcodec_INCLUDE_DIR ${CMAKE_CURRENT_LIST_DIR}/src/nvimgcodec_dynlink) + include_directories(AFTER SYSTEM "${nvimgcodec_INCLUDE_DIR}") + # Use pre-generated stubs (faster builds, no Python clang dependency) # To regenerate: cd build-release && make dynlink_nvimgcodec_gen.cc # then copy to src/nvimgcodec_dynlink/nvimgcodec_stubs_generated.cc - + # Create dynamic loading wrapper library add_library(nvimgcodec_dynlink STATIC src/nvimgcodec_dynlink/nvimgcodec_wrap.cc @@ -221,14 +228,13 @@ if(WITH_DYNAMIC_NVIMGCODEC) ) set_target_properties(nvimgcodec_dynlink PROPERTIES POSITION_INDEPENDENT_CODE ON) target_include_directories(nvimgcodec_dynlink SYSTEM PRIVATE - ${CMAKE_CURRENT_LIST_DIR}/src/nvimgcodec_dynlink + ${nvimgcodec_INCLUDE_DIR} ${CUDAToolkit_INCLUDE_DIRS} ) target_link_libraries(nvimgcodec_dynlink PRIVATE ${CMAKE_DL_LIBS}) # Link cuslide2 against dynlink wrapper (instead of libnvimgcodec.so) target_link_libraries(${CUCIM_PLUGIN_NAME} PRIVATE nvimgcodec_dynlink) - target_include_directories(${CUCIM_PLUGIN_NAME} SYSTEM PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/nvimgcodec_dynlink) target_compile_definitions(${CUCIM_PLUGIN_NAME} PRIVATE CUCIM_HAS_NVIMGCODEC WITH_DYNAMIC_NVIMGCODEC) # Set rpath for runtime library search @@ -279,6 +285,7 @@ target_include_directories(${CUCIM_PLUGIN_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../cucim.kit.cuslide/src ${CMAKE_CURRENT_SOURCE_DIR}/src # Include cuslide2 src for nvimgcodec headers + ${CMAKE_CURRENT_SOURCE_DIR}/src ) # Do not generate SONAME as this would be used as plugin @@ -305,12 +312,11 @@ add_subdirectory(benchmarks) ################################################################################ # Install ################################################################################ -# NOTE: Disabled for infrastructure-only PR -# TODO: Uncomment when library target exists + set(INSTALL_TARGETS ${CUCIM_PLUGIN_NAME} - cuslide2_tests - # cuslide_benchmarks # Disabled - not yet implemented + cuslide2_tests + cuslide2_benchmarks ) # Add nvimgcodec_dynlink to install targets if dynamic loading is enabled @@ -362,4 +368,6 @@ install( 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 index 32c13ab73..1a9f93311 100644 --- a/cpp/plugins/cucim.kit.cuslide2/benchmarks/CMakeLists.txt +++ b/cpp/plugins/cucim.kit.cuslide2/benchmarks/CMakeLists.txt @@ -6,21 +6,21 @@ # ################################################################################ -# Add executable: cuslide_benchmarks +# Add executable: cuslide2_benchmarks ################################################################################ -add_executable(cuslide_benchmarks main.cpp config.h) +add_executable(cuslide2_benchmarks main.cpp config.h) #set_source_files_properties(main.cpp PROPERTIES LANGUAGE CUDA) # failed with CLI11 library -set_target_properties(cuslide_benchmarks +set_target_properties(cuslide2_benchmarks PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO ) -target_compile_features(cuslide_benchmarks PRIVATE ${CUCIM_REQUIRED_FEATURES}) +target_compile_features(cuslide2_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 +target_compile_options(cuslide2_benchmarks PRIVATE $<$:-Werror -Wall -Wextra>) +target_compile_definitions(cuslide2_benchmarks PUBLIC CUSLIDE_VERSION=${PROJECT_VERSION} CUSLIDE_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} @@ -28,7 +28,7 @@ target_compile_definitions(cuslide_benchmarks CUSLIDE_VERSION_PATCH=${PROJECT_VERSION_PATCH} CUSLIDE_VERSION_BUILD=${PROJECT_VERSION_BUILD} ) -target_link_libraries(cuslide_benchmarks +target_link_libraries(cuslide2_benchmarks PRIVATE cucim::cucim deps::googlebenchmark From b8ae2ddeb6508655e22265bbfd34f556f694bc0c Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 19:32:28 -0800 Subject: [PATCH 23/34] Fix pre-commit formatting: remove trailing whitespace --- cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt b/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt index 906df175f..319a4da6e 100644 --- a/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt +++ b/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt @@ -210,17 +210,17 @@ target_link_libraries(${CUCIM_PLUGIN_NAME} option(WITH_DYNAMIC_NVIMGCODEC "Use dynamic loading for nvImageCodec (dlopen at runtime)" ON) if(WITH_DYNAMIC_NVIMGCODEC) - + message(STATUS "============== Dynamic nvImageCodec Loading ==============") set(nvimgcodec_INCLUDE_DIR ${CMAKE_CURRENT_LIST_DIR}/src/nvimgcodec_dynlink) include_directories(AFTER SYSTEM "${nvimgcodec_INCLUDE_DIR}") - + # Use pre-generated stubs (faster builds, no Python clang dependency) # To regenerate: cd build-release && make dynlink_nvimgcodec_gen.cc # then copy to src/nvimgcodec_dynlink/nvimgcodec_stubs_generated.cc - + # Create dynamic loading wrapper library add_library(nvimgcodec_dynlink STATIC src/nvimgcodec_dynlink/nvimgcodec_wrap.cc @@ -315,8 +315,8 @@ add_subdirectory(benchmarks) set(INSTALL_TARGETS ${CUCIM_PLUGIN_NAME} - cuslide2_tests - cuslide2_benchmarks + cuslide2_tests + cuslide2_benchmarks ) # Add nvimgcodec_dynlink to install targets if dynamic loading is enabled From 4360b5ce872465272b503131e46968666e32399b Mon Sep 17 00:00:00 2001 From: cdinea Date: Wed, 3 Dec 2025 20:00:22 -0800 Subject: [PATCH 24/34] Remove nvimgcodec_manager.h from CMakeLists.txt (file was removed in earlier commit) --- cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt b/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt index 319a4da6e..18858d493 100644 --- a/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt +++ b/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt @@ -173,7 +173,6 @@ add_library(${CUCIM_PLUGIN_NAME} # 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_manager.h ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/cuslide/nvimgcodec/nvimgcodec_tiff_parser.h) From be1eb7745ed3934b7803ea1f985e8e08e9e79b31 Mon Sep 17 00:00:00 2001 From: cdinea Date: Thu, 4 Dec 2025 10:17:02 -0800 Subject: [PATCH 25/34] refactor: Remove unused codec library dependencies --- cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt b/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt index 18858d493..bddf8f3ca 100644 --- a/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt +++ b/cpp/plugins/cucim.kit.cuslide2/CMakeLists.txt @@ -102,9 +102,6 @@ add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0) # TODO: create two library, on # Define dependencies ################################################################################ superbuild_depend(fmt) -superbuild_depend(libjpeg-turbo) # libjpeg-turbo should be located before libtiff as libtiff depends on libjpeg-turbo -superbuild_depend(libopenjpeg) -superbuild_depend(libtiff) superbuild_depend(catch2) superbuild_depend(openslide) superbuild_depend(googletest) @@ -112,7 +109,6 @@ superbuild_depend(googlebenchmark) superbuild_depend(cli11) superbuild_depend(pugixml) superbuild_depend(json) -superbuild_depend(libdeflate) superbuild_depend(nvimgcodec) ################################################################################ @@ -143,20 +139,17 @@ 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 " ✓ nvImageCodec (GPU JPEG/JPEG2000/deflate/LZW decoding)") +message(STATUS " ✓ pugixml (XML metadata)") message(STATUS " ✓ json (JSON metadata)") -message(STATUS " ✓ openslide (for testing)") +message(STATUS " ✓ openslide (testing)") message(STATUS " ✓ googletest, catch2, googlebenchmark, cli11 (testing)") message(STATUS "") -message(STATUS "Build configured: Pure GPU-accelerated decoding with tests!") +message(STATUS "NOTE: No codec libraries needed (libjpeg/libtiff/libdeflate)") +message(STATUS " All decoding handled by nvImageCodec GPU acceleration!") message(STATUS "=============================================================") # Add library - PURE nvImageCodec implementation (no CPU fallbacks) @@ -196,13 +189,8 @@ target_link_libraries(${CUCIM_PLUGIN_NAME} PRIVATE deps::fmt cucim::cucim - deps::libtiff - deps::libjpeg-turbo - deps::libopenjpeg - deps::libopenjpeg-lcms2 deps::pugixml deps::json - deps::libdeflate ) # nvImageCodec linking: dynamic (runtime) vs static (compile-time) From 798d326ef8a3eff2ec2bda4a0e9ea03b137db570 Mon Sep 17 00:00:00 2001 From: cdinea Date: Thu, 4 Dec 2025 10:24:49 -0800 Subject: [PATCH 26/34] style: Remove redundant license line from config.h header --- cpp/plugins/cucim.kit.cuslide2/benchmarks/config.h | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/benchmarks/config.h b/cpp/plugins/cucim.kit.cuslide2/benchmarks/config.h index 11071ce78..3357cabb4 100644 --- a/cpp/plugins/cucim.kit.cuslide2/benchmarks/config.h +++ b/cpp/plugins/cucim.kit.cuslide2/benchmarks/config.h @@ -1,5 +1,4 @@ /* - * Apache License, Version 2.0 * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION * SPDX-License-Identifier: Apache-2.0 */ From 085cb468513d5f36db7ee7cf3fffae07e3abf899 Mon Sep 17 00:00:00 2001 From: cdinea <123968711+cdinea@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:26:39 -0800 Subject: [PATCH 27/34] Update cpp/plugins/cucim.kit.cuslide2/tests/main.cpp Co-authored-by: Gregory Lee --- cpp/plugins/cucim.kit.cuslide2/tests/main.cpp | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/tests/main.cpp b/cpp/plugins/cucim.kit.cuslide2/tests/main.cpp index 1a24e09b5..7d9541b81 100644 --- a/cpp/plugins/cucim.kit.cuslide2/tests/main.cpp +++ b/cpp/plugins/cucim.kit.cuslide2/tests/main.cpp @@ -1,17 +1,6 @@ /* - * 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. + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 */ // #define CATCH_CONFIG_MAIN From 2a8c0b71de3fca1006431f6bb6bc8031e144ac97 Mon Sep 17 00:00:00 2001 From: cdinea <123968711+cdinea@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:27:16 -0800 Subject: [PATCH 28/34] Update cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp Co-authored-by: Gregory Lee --- .../cucim.kit.cuslide2/benchmarks/main.cpp | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp b/cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp index 01605dca0..45dacba95 100644 --- a/cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp +++ b/cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp @@ -1,17 +1,6 @@ /* - * 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. + * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 */ #include "config.h" From 5124f91801c3283620ace6a4f2589fbc71ddd3c5 Mon Sep 17 00:00:00 2001 From: cdinea Date: Thu, 4 Dec 2025 10:30:46 -0800 Subject: [PATCH 29/34] refactor: Remove incorrectly named config.cmake.in file --- .../cmake/cucim.kit.cuslide-config.cmake.in | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 cpp/plugins/cucim.kit.cuslide2/cmake/cucim.kit.cuslide-config.cmake.in 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 deleted file mode 100644 index 2b9bae2fc..000000000 --- a/cpp/plugins/cucim.kit.cuslide2/cmake/cucim.kit.cuslide-config.cmake.in +++ /dev/null @@ -1,15 +0,0 @@ -# -# 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() From 6d84512de26740e1ead95ceb13b1269e25aacb2a Mon Sep 17 00:00:00 2001 From: cdinea <123968711+cdinea@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:58:16 -0800 Subject: [PATCH 30/34] Update cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h Co-authored-by: Gregory Lee --- cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h index e8b936e8b..344787209 100644 --- a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2020, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. * SPDX-License-Identifier: Apache-2.0 */ From 053238d49e9494ae9af4280e94e1ba64398c21c6 Mon Sep 17 00:00:00 2001 From: cdinea <123968711+cdinea@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:58:46 -0800 Subject: [PATCH 31/34] Update cpp/plugins/cucim.kit.cuslide2/tests/config.h Co-authored-by: Gregory Lee --- cpp/plugins/cucim.kit.cuslide2/tests/config.h | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/tests/config.h b/cpp/plugins/cucim.kit.cuslide2/tests/config.h index 6767d9183..9e5ee1264 100644 --- a/cpp/plugins/cucim.kit.cuslide2/tests/config.h +++ b/cpp/plugins/cucim.kit.cuslide2/tests/config.h @@ -1,5 +1,4 @@ /* - * Apache License, Version 2.0 * SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION * SPDX-License-Identifier: Apache-2.0 */ From 6d2767d659397067a40fa46fd5d3fdceedfcc267 Mon Sep 17 00:00:00 2001 From: cdinea <123968711+cdinea@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:59:30 -0800 Subject: [PATCH 32/34] Update cpp/plugins/cucim.kit.cuslide2/tests/test_read_region.cpp Co-authored-by: Gregory Lee --- .../cucim.kit.cuslide2/tests/test_read_region.cpp | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/tests/test_read_region.cpp b/cpp/plugins/cucim.kit.cuslide2/tests/test_read_region.cpp index af808fd12..f62ef961c 100644 --- a/cpp/plugins/cucim.kit.cuslide2/tests/test_read_region.cpp +++ b/cpp/plugins/cucim.kit.cuslide2/tests/test_read_region.cpp @@ -1,17 +1,6 @@ /* - * 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. + * SPDX-FileCopyrightText: Copyright (c) 2020-2021, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 */ #include From 2f2283906f2e63916be040aa9adbe8e137e178c2 Mon Sep 17 00:00:00 2001 From: cdinea Date: Thu, 4 Dec 2025 11:13:53 -0800 Subject: [PATCH 33/34] style: Clean up main.cpp files for cuslide2egration. --- cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp | 2 +- cpp/plugins/cucim.kit.cuslide2/tests/main.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp b/cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp index 45dacba95..9e86d0b33 100644 --- a/cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp +++ b/cpp/plugins/cucim.kit.cuslide2/benchmarks/main.cpp @@ -23,7 +23,7 @@ #define XSTR(x) STR(x) #define STR(x) #x -//#include + CUCIM_FRAMEWORK_GLOBALS("cuslide.app") diff --git a/cpp/plugins/cucim.kit.cuslide2/tests/main.cpp b/cpp/plugins/cucim.kit.cuslide2/tests/main.cpp index 7d9541b81..56a1f647a 100644 --- a/cpp/plugins/cucim.kit.cuslide2/tests/main.cpp +++ b/cpp/plugins/cucim.kit.cuslide2/tests/main.cpp @@ -4,7 +4,7 @@ */ // #define CATCH_CONFIG_MAIN -// #include + // Implement main explicitly to handle additional parameters. #define CATCH_CONFIG_RUNNER From 75515df6fcce376b27c8945b1204e3f567095c4a Mon Sep 17 00:00:00 2001 From: cdinea Date: Thu, 4 Dec 2025 11:49:40 -0800 Subject: [PATCH 34/34] style: Add cuslide2-specific comments to cuslide.h. --- cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h index 344787209..b9f580b87 100644 --- a/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h +++ b/cpp/plugins/cucim.kit.cuslide2/src/cuslide/cuslide.h @@ -5,4 +5,8 @@ #ifndef CUSLIDE_CUSLIDE_H #define CUSLIDE_CUSLIDE_H + +// cuslide2: GPU-accelerated whole slide imaging with nvImageCodec +// This plugin provides pure GPU decoding for JPEG/JPEG2000 compressed WSI files + #endif // CUSLIDE_CUSLIDE_H